FAANG Interview Prep
Foundations, Memory & Complexity
Big-O notation, time/space analysis, memory layoutRecursion Complete Guide
Base cases, call stack, tail recursion, memoizationArrays & Array ADT
Static/dynamic arrays, operations, amortized analysisStrings
Pattern matching, string algorithms, encoding, manipulationMatrices
2D arrays, sparse matrices, matrix operations, traversalsLinked Lists
Singly, doubly, circular lists, pointer manipulationStack
LIFO, push/pop, expression evaluation, backtrackingQueue
FIFO, circular queue, deque, priority queueTrees
Binary trees, traversals, expression trees, threaded treesBST & Balanced Trees
Search, insert, delete, AVL, red-black, B-treesHeaps, Sorting & Hashing
Min/max heaps, heapsort, hash tables, collision handlingGraphs, DP, Greedy & Backtracking
BFS, DFS, shortest paths, dynamic programming, optimizationIntroduction to Arrays
Arrays are the most fundamental data structure in computer science—a contiguous block of memory that stores elements of the same type. Understanding arrays deeply is essential because they form the foundation for implementing many other data structures.
Static vs Dynamic Arrays
Arrays come in two flavors: static (fixed-size) and dynamic (resizable). Understanding both is crucial for interview success.
| Aspect | Static Array | Dynamic Array |
|---|---|---|
| Size | Fixed at creation | Grows/shrinks automatically |
| Memory | Stack or heap | Always heap |
| Allocation | Compile-time or one-time | Runtime (may reallocate) |
| Append | N/A (fixed) | Amortized \(O(1)\) |
| Python | array.array() | list |
| C++ | int arr[n] or std::array | std::vector |
| Java | int[] | ArrayList |
Address Calculation
For a 1D array, the memory address of element at index i is calculated as:
Address(A[i]) = Base_Address + (i × Element_Size)
Array ADT (Abstract Data Type)
The Array ADT defines the operations we can perform on an array, independent of implementation. Let's build a complete Array ADT class in all three languages.
Array ADT Operations
display()- Show all elementsappend(x)- Add element at endinsert(index, x)- Insert at positiondelete(index)- Remove at positionsearch(x)- Find element (linear/binary)get(index)- Get element at positionset(index, x)- Update elementmax(),min(),sum(),avg()- Aggregate operationsreverse()- Reverse in placeis_sorted()- Check if sorted
Insert & Delete Operations
Understanding insertion and deletion is crucial—these operations form the basis for many interview questions.
Insertion at Different Positions
Inserting into an array requires shifting elements to make room. Inserting at the end (append) is \(O(1)\) amortized — no shifting needed, just place at the next available index. Inserting at the beginning is \(O(n)\) — every existing element must shift right by one position. Inserting at index \(k\) is \(O(n-k)\). This shifting cost is the fundamental tradeoff of arrays vs linked lists: arrays give \(O(1)\) random access but \(O(n)\) insertion/deletion in the middle.
# Insert Operations Visualization
class ArrayInsert:
"""Demonstrates different insertion scenarios"""
def __init__(self, elements):
self.arr = list(elements)
def insert_at_beginning(self, element):
"""Insert at index 0. Time: O(n) - shift all elements"""
# Shift all elements right by 1
self.arr = [element] + self.arr
return self.arr
def insert_at_end(self, element):
"""Insert at last index. Time: O(1) amortized"""
self.arr.append(element)
return self.arr
def insert_at_index(self, index, element):
"""Insert at specific index. Time: O(n-index)"""
# Manual implementation showing the shift
new_arr = []
for i in range(len(self.arr) + 1):
if i < index:
new_arr.append(self.arr[i])
elif i == index:
new_arr.append(element)
else:
new_arr.append(self.arr[i-1])
self.arr = new_arr
return self.arr
def insert_in_sorted(self, element):
"""Insert maintaining sorted order. Time: O(n)"""
# Find correct position
pos = 0
for i in range(len(self.arr)):
if self.arr[i] < element:
pos = i + 1
else:
break
# Insert at position
self.arr.insert(pos, element)
return self.arr
# Test insertions
arr = ArrayInsert([10, 20, 30, 40, 50])
print(f"Original: {arr.arr}")
# Insert at beginning - O(n)
arr = ArrayInsert([10, 20, 30, 40, 50])
print(f"Insert 5 at beginning: {arr.insert_at_beginning(5)}")
# Insert at end - O(1)
arr = ArrayInsert([10, 20, 30, 40, 50])
print(f"Insert 60 at end: {arr.insert_at_end(60)}")
# Insert at index 2 - O(n-2)
arr = ArrayInsert([10, 20, 30, 40, 50])
print(f"Insert 25 at index 2: {arr.insert_at_index(2, 25)}")
# Insert in sorted array - O(n)
arr = ArrayInsert([10, 20, 30, 40, 50])
print(f"Insert 35 in sorted: {arr.insert_in_sorted(35)}")
Deletion at Different Positions
Deletion is the mirror of insertion: removing from the end is \(O(1)\) (just decrement size), but removing from the beginning or middle requires shifting all subsequent elements left to fill the gap — \(O(n)\) worst case. An important optimization: if order doesn't matter, swap the target element with the last element and delete from the end — achieving \(O(1)\) deletion at any position.
# Delete Operations Visualization
class ArrayDelete:
"""Demonstrates different deletion scenarios"""
def __init__(self, elements):
self.arr = list(elements)
def delete_at_beginning(self):
"""Delete at index 0. Time: O(n) - shift all elements"""
if not self.arr:
return None
deleted = self.arr[0]
self.arr = self.arr[1:]
return deleted, self.arr
def delete_at_end(self):
"""Delete at last index. Time: O(1)"""
if not self.arr:
return None
deleted = self.arr.pop()
return deleted, self.arr
def delete_at_index(self, index):
"""Delete at specific index. Time: O(n-index)"""
if index < 0 or index >= len(self.arr):
return None, self.arr
deleted = self.arr[index]
# Manual shift to show operation
new_arr = []
for i in range(len(self.arr)):
if i != index:
new_arr.append(self.arr[i])
self.arr = new_arr
return deleted, self.arr
def delete_by_value(self, value):
"""Delete first occurrence of value. Time: O(n)"""
for i in range(len(self.arr)):
if self.arr[i] == value:
deleted = self.arr[i]
self.arr = self.arr[:i] + self.arr[i+1:]
return deleted, self.arr
return None, self.arr
# Test deletions
arr = ArrayDelete([10, 20, 30, 40, 50])
print(f"Original: {arr.arr}")
# Delete at beginning - O(n)
arr = ArrayDelete([10, 20, 30, 40, 50])
deleted, result = arr.delete_at_beginning()
print(f"Delete at beginning: {result}, Deleted: {deleted}")
# Delete at end - O(1)
arr = ArrayDelete([10, 20, 30, 40, 50])
deleted, result = arr.delete_at_end()
print(f"Delete at end: {result}, Deleted: {deleted}")
# Delete at index 2 - O(n-2)
arr = ArrayDelete([10, 20, 30, 40, 50])
deleted, result = arr.delete_at_index(2)
print(f"Delete at index 2: {result}, Deleted: {deleted}")
# Delete by value - O(n)
arr = ArrayDelete([10, 20, 30, 40, 50])
deleted, result = arr.delete_by_value(30)
print(f"Delete value 30: {result}, Deleted: {deleted}")
Searching Algorithms
Linear Search
Linear search examines each element sequentially until the target is found or the array ends. Works on any array (sorted or unsorted).
| Case | Time Complexity | When |
|---|---|---|
| Best | \(O(1)\) | Element at first position |
| Average | O(n/2) = \(O(n)\) | Element in middle |
| Worst | \(O(n)\) | Element at last or not found |
Binary Search
Binary search requires a sorted array and works by repeatedly dividing the search space in half. This is a fundamental algorithm you must master for FAANG interviews.
Binary Search Interview Patterns
These advanced patterns appear frequently in FAANG interviews.
Python - First/Last Occurrence & Rotated Array
# Advanced Binary Search Patterns
def find_first_occurrence(arr, target):
"""Find first occurrence in sorted array with duplicates."""
left, right = 0, len(arr) - 1
result = -1
while left <= right:
mid = left + (right - left) // 2
if arr[mid] == target:
result = mid
right = mid - 1 # Keep searching left
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return result
def search_rotated_array(arr, target):
"""Search in rotated sorted array - Classic FAANG question!"""
left, right = 0, len(arr) - 1
while left <= right:
mid = left + (right - left) // 2
if arr[mid] == target:
return mid
# Left half is sorted
if arr[left] <= arr[mid]:
if arr[left] <= target < arr[mid]:
right = mid - 1
else:
left = mid + 1
# Right half is sorted
else:
if arr[mid] < target <= arr[right]:
left = mid + 1
else:
right = mid - 1
return -1
# Test
arr = [10, 20, 30, 30, 30, 40, 50]
print(f"First occurrence of 30: index {find_first_occurrence(arr, 30)}")
rotated = [40, 50, 60, 10, 20, 30]
print(f"Rotated array: {rotated}")
print(f"Search 10: index {search_rotated_array(rotated, 10)}")
2D Arrays & Matrices
2D arrays (matrices) are essential for many problems: grid traversals, dynamic programming, image processing, and graph adjacency matrices.
# 2D Array Fundamentals
# Creating 2D arrays in Python
# Method 1: List comprehension (CORRECT)
rows, cols = 3, 4
matrix = [[0 for _ in range(cols)] for _ in range(rows)]
print("Empty 3x4 matrix:")
for row in matrix:
print(row)
# Method 2: Direct initialization
matrix = [
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
]
print("\nInitialized matrix:")
for row in matrix:
print(row)
# WRONG WAY (creates references, not copies!)
wrong_matrix = [[0] * cols] * rows
wrong_matrix[0][0] = 1 # This changes ALL rows!
print("\nWrong initialization (all rows share reference):")
for row in wrong_matrix:
print(row)
# Accessing elements
print(f"\nElement at [1][2]: {matrix[1][2]}") # Output: 7
# Iterating 2D arrays
print("\nRow-wise iteration:")
for i in range(len(matrix)):
for j in range(len(matrix[0])):
print(f"matrix[{i}][{j}] = {matrix[i][j]}")
Row-Major vs Column-Major Ordering
Since memory is fundamentally 1-dimensional, 2D arrays must be linearized. Two conventions exist: Row-major (C, Java, Python) stores rows contiguously — elements in the same row are adjacent in memory. Column-major (Fortran, MATLAB, R) stores columns contiguously. This matters enormously for performance: traversing in the "natural" order of your language gives cache hits; traversing perpendicular to it causes cache misses. In C/Java, iterate rows in the outer loop; in Fortran/MATLAB, iterate columns first.
Address formula (row-major): For an \(m \times n\) matrix stored at base address \(B\), element \([i][j]\) is at \(B + (i \times n + j) \times \text{element\_size}\). Column-major swaps the roles: \(B + (j \times m + i) \times \text{element\_size}\).
Understanding memory layout is crucial for performance optimization and interview questions about index calculation.
Memory Layout
- Row-Major (C, Python, Java): Elements of each row stored contiguously
- Column-Major (Fortran, MATLAB): Elements of each column stored contiguously
# Row-Major vs Column-Major Address Calculation
def row_major_address(base, i, j, num_cols, element_size):
"""
Calculate address in row-major order.
Formula: Base + (i * num_cols + j) * element_size
"""
return base + (i * num_cols + j) * element_size
def column_major_address(base, i, j, num_rows, element_size):
"""
Calculate address in column-major order.
Formula: Base + (j * num_rows + i) * element_size
"""
return base + (j * num_rows + i) * element_size
# Example: 3x4 matrix
num_rows, num_cols = 3, 4
base_address = 1000
element_size = 4 # 4 bytes for integer
print("3x4 Matrix Address Calculation")
print("=" * 50)
# Create visual matrix
matrix = [[i * num_cols + j for j in range(num_cols)] for i in range(num_rows)]
print("\nLogical matrix indices:")
for row in matrix:
print(row)
# Row-major layout
print("\nRow-Major Order (C, Python, Java):")
print("Memory: ", end="")
for i in range(num_rows):
for j in range(num_cols):
addr = row_major_address(base_address, i, j, num_cols, element_size)
print(f"[{i},{j}]:{addr}", end=" ")
print()
# Column-major layout
print("\nColumn-Major Order (Fortran, MATLAB):")
print("Memory: ", end="")
for j in range(num_cols):
for i in range(num_rows):
addr = column_major_address(base_address, i, j, num_rows, element_size)
print(f"[{i},{j}]:{addr}", end=" ")
print()
# Performance implication
print("\n" + "=" * 50)
print("Performance Implication:")
print("Row-major: Access row-by-row for cache efficiency")
print("Column-major: Access column-by-column for cache efficiency")
2D Array Operations
# Common 2D Array Operations
import copy
class Matrix:
"""Matrix class with common operations"""
def __init__(self, data):
self.data = [row[:] for row in data] # Deep copy
self.rows = len(data)
self.cols = len(data[0]) if data else 0
def display(self):
"""Display matrix"""
for row in self.data:
print([f"{x:3}" for x in row])
print()
def transpose(self):
"""
Transpose matrix (swap rows and columns).
Time: O(rows × cols)
"""
result = [[self.data[j][i] for j in range(self.rows)]
for i in range(self.cols)]
return Matrix(result)
def rotate_90_clockwise(self):
"""
Rotate 90° clockwise.
Pattern: Transpose + Reverse each row
Time: O(n²) for n×n matrix
"""
# Transpose
transposed = [[self.data[j][i] for j in range(self.rows)]
for i in range(self.cols)]
# Reverse each row
for row in transposed:
row.reverse()
return Matrix(transposed)
def rotate_90_counter_clockwise(self):
"""
Rotate 90° counter-clockwise.
Pattern: Transpose + Reverse each column
Time: O(n²) for n×n matrix
"""
# Transpose
transposed = [[self.data[j][i] for j in range(self.rows)]
for i in range(self.cols)]
# Reverse the list of rows
transposed.reverse()
return Matrix(transposed)
def spiral_order(self):
"""
Return elements in spiral order.
Classic interview question!
Time: O(rows × cols)
"""
if not self.data:
return []
result = []
top, bottom = 0, self.rows - 1
left, right = 0, self.cols - 1
while top <= bottom and left <= right:
# Right
for j in range(left, right + 1):
result.append(self.data[top][j])
top += 1
# Down
for i in range(top, bottom + 1):
result.append(self.data[i][right])
right -= 1
# Left
if top <= bottom:
for j in range(right, left - 1, -1):
result.append(self.data[bottom][j])
bottom -= 1
# Up
if left <= right:
for i in range(bottom, top - 1, -1):
result.append(self.data[i][left])
left += 1
return result
# Test matrix operations
matrix = Matrix([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
print("Original Matrix:")
matrix.display()
print("Transpose:")
matrix.transpose().display()
print("Rotate 90° Clockwise:")
matrix.rotate_90_clockwise().display()
print("Rotate 90° Counter-Clockwise:")
matrix.rotate_90_counter_clockwise().display()
print(f"Spiral Order: {matrix.spiral_order()}")
# 4x4 spiral example
matrix4 = Matrix([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]
])
print("\n4x4 Matrix Spiral Order:")
matrix4.display()
print(f"Spiral: {matrix4.spiral_order()}")
LeetCode Practice Problems
Practice these array problems to build strong foundations. Solutions provided in Python, C++, and Java!
Easy 704. Binary Search
Given sorted array, return index of target or -1.
Easy 35. Search Insert Position
Find index to insert target in sorted array.
Medium 33. Search in Rotated Sorted Array
Search in a rotated sorted array with \(O(\log n)\) time.
Medium 54. Spiral Matrix
Return all elements of matrix in spiral order.
Medium 48. Rotate Image
Rotate n×n matrix 90° clockwise in-place.
# LeetCode 48 - Rotate Image
# Time: O(n²), Space: O(1)
def rotate(matrix):
n = len(matrix)
# Step 1: Transpose (swap matrix[i][j] with matrix[j][i])
for i in range(n):
for j in range(i + 1, n):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
# Step 2: Reverse each row
for row in matrix:
row.reverse()
matrix = [[1,2,3],[4,5,6],[7,8,9]]
print("Before:")
for row in matrix:
print(row)
rotate(matrix)
print("\nAfter 90° clockwise rotation:")
for row in matrix:
print(row)
# Output: [[7,4,1],[8,5,2],[9,6,3]]
Next in the Series
In Part 4: Strings, we’ll explore string algorithms — pattern matching (KMP, Rabin-Karp), manipulation techniques, and encoding — building on the array foundations from this part.