FAANG Practice Problems
A curated archive of the interview questions that actually get asked — every problem worked end-to-end from brute force to optimal, with complexity analysis, key insight, and common variants.
Arrays & Strings
The workhorse category. Almost every interview opens here. Master two-pointer, sliding-window, prefix-sum, and hash-assisted scans and you will recognize half of all array questions on sight.
Pattern Cheat
| Signal | Pattern | Complexity |
|---|---|---|
| "find pair/triplet summing to target" | Sort + two pointers, or hash map | O(n log n) or O(n) |
| "contiguous subarray / substring" | Sliding window | O(n) |
| "product / sum of prefix" | Prefix-sum / prefix-product | O(n) |
| "k-th largest / smallest" | Heap / Quick-Select | O(n log k) / O(n) |
| "in-place shuffle / rotate" | Reverse in place / cycle walk | O(n) time O(1) space |
Problem 1: Two Sum — I / II / IIIeasy
Statement (I)
Given an array nums and a target, return indices i != j with nums[i] + nums[j] == target. Exactly one solution exists.
Approach 1 — Brute Force
Try every pair. Quadratic, but a great baseline to prove correctness before optimizing.
def two_sum_brute(nums, target):
n = len(nums)
for i in range(n):
for j in range(i + 1, n):
if nums[i] + nums[j] == target:
return [i, j]
return []
Two nested scans, constant extra memory.
Approach 2 — Hash Map (one pass)
For each element x, look up target - x. Store value -> index as you go so you only scan once.
def two_sum(nums, target):
seen = {}
for i, x in enumerate(nums):
if target - x in seen:
return [seen[target - x], i]
seen[x] = i
return []
Hash lookup is amortized O(1), so a single scan suffices.
Variant II — input is sorted
Use two pointers; no extra memory needed.
def two_sum_sorted(nums, target):
l, r = 0, len(nums) - 1
while l < r:
s = nums[l] + nums[r]
if s == target: return [l + 1, r + 1] # 1-indexed
if s < target: l += 1
else: r -= 1
return []
Sortedness lets monotone pointer moves eliminate half the space per step — here linearly.
Variant III — streaming design
Design add(x) and find(t) on a stream. Multiset lets add be O(1) and find linear; this matches the typical interview ask that trades one op for the other.
class TwoSumStream:
def __init__(self):
from collections import Counter
self.cnt = Counter()
def add(self, x):
self.cnt[x] += 1
def find(self, t):
for x, c in self.cnt.items():
need = t - x
if need == x and c >= 2: return True
if need != x and self.cnt.get(need, 0) > 0: return True
return False
Problem 2: Longest Substring Without Repeating Charactersmedium
Statement
Return the length of the longest substring of s in which no character repeats.
Approach 1 — Check Every Substring
def length_brute(s):
best = 0
for i in range(len(s)):
seen = set()
for j in range(i, len(s)):
if s[j] in seen: break
seen.add(s[j])
best = max(best, j - i + 1)
return best
Each start index walks forward; substring-reset is the waste.
Approach 2 — Sliding Window with Last-Seen Map
Keep left as window start. For each right, if s[right] was seen at index k >= left, jump left = k + 1. Else update best.
def length_of_longest(s):
last = {}
left = best = 0
for right, ch in enumerate(s):
if ch in last and last[ch] >= left:
left = last[ch] + 1
last[ch] = right
best = max(best, right - left + 1)
return best
Each char enters and exits the window at most once.
Problem 3: Container With Most Watermedium
Statement
Given heights, choose two indices forming a container; maximize min(h[i], h[j]) * (j - i).
Approach 1 — All Pairs
def max_area_brute(h):
best = 0
for i in range(len(h)):
for j in range(i + 1, len(h)):
best = max(best, min(h[i], h[j]) * (j - i))
return best
Approach 2 — Two Pointers
Start at both ends. The shorter wall caps the area; moving it inward is the only move that could improve things.
def max_area(h):
l, r, best = 0, len(h) - 1, 0
while l < r:
best = max(best, min(h[l], h[r]) * (r - l))
if h[l] < h[r]: l += 1
else: r -= 1
return best
Inward move on the shorter side is the greedy invariant.
Problem 4: Trapping Rain Waterhard
Statement
Given a histogram of heights, how much water can be trapped after rain?
Approach 1 — For each bar, scan both sides
def trap_brute(h):
ans = 0
for i in range(len(h)):
lmax = max(h[:i+1]); rmax = max(h[i:])
ans += min(lmax, rmax) - h[i]
return ans
Approach 2 — Precompute prefix/suffix max
def trap_prefix(h):
n = len(h)
L = [0]*n; R = [0]*n
cur = 0
for i in range(n): cur = max(cur, h[i]); L[i] = cur
cur = 0
for i in range(n-1, -1, -1): cur = max(cur, h[i]); R[i] = cur
return sum(min(L[i], R[i]) - h[i] for i in range(n))
Two linear scans build the bounding walls.
Approach 3 — Two Pointers (O(1) memory)
Whichever side has the smaller running max is the binding constraint on the other side's cell.
def trap(h):
l, r = 0, len(h) - 1
lmax = rmax = ans = 0
while l < r:
if h[l] < h[r]:
if h[l] >= lmax: lmax = h[l]
else: ans += lmax - h[l]
l += 1
else:
if h[r] >= rmax: rmax = h[r]
else: ans += rmax - h[r]
r -= 1
return ans
Two pointers replace the prefix arrays once we realize only the smaller side matters.
Problem 5: 3Summedium
Statement
Return all unique triplets summing to zero.
Approach 1 — Triple Loop + Set
def three_sum_brute(nums):
seen = set(); n = len(nums); nums.sort()
for i in range(n):
for j in range(i+1, n):
for k in range(j+1, n):
if nums[i]+nums[j]+nums[k] == 0:
seen.add((nums[i], nums[j], nums[k]))
return [list(t) for t in seen]
Approach 2 — Sort + Two Pointers
def three_sum(nums):
nums.sort()
res, n = [], len(nums)
for i in range(n - 2):
if nums[i] > 0: break
if i > 0 and nums[i] == nums[i-1]: continue
l, r = i + 1, n - 1
while l < r:
s = nums[i] + nums[l] + nums[r]
if s < 0: l += 1
elif s > 0: r -= 1
else:
res.append([nums[i], nums[l], nums[r]])
while l < r and nums[l] == nums[l+1]: l += 1
while l < r and nums[r] == nums[r-1]: r -= 1
l += 1; r -= 1
return res
sort then fix one element and two-pointer the rest.
Problem 6: Maximum Subarray (Kadane)medium
Statement
Find the contiguous subarray with the largest sum.
Approach — Kadane (DP collapsed)
def max_subarray(nums):
best = cur = nums[0]
for x in nums[1:]:
cur = max(x, cur + x)
best = max(best, cur)
return best
At each step, decide: extend the prior positive-sum window, or restart at x.
Problem 7: Product of Array Except Selfmedium
Statement
Return an array out where out[i] = product of nums[j] for j != i. No division allowed; O(n) time, O(1) extra.
Approach — Two Passes with Running Products
def product_except_self(nums):
n = len(nums)
out = [1] * n
left = 1
for i in range(n):
out[i] = left
left *= nums[i]
right = 1
for i in range(n - 1, -1, -1):
out[i] *= right
right *= nums[i]
return out
(output array excluded from space by convention).
Problem 8: Longest Palindromic Substringmedium
Statement
Return the longest palindromic substring of s.
Approach 1 — DP on (i,j)
dp[i][j] = s[i] == s[j] and (j - i < 2 or dp[i+1][j-1]).
def longest_palindrome_dp(s):
n = len(s)
dp = [[False]*n for _ in range(n)]
best = ""
for length in range(1, n+1):
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j] and (length < 3 or dp[i+1][j-1]):
dp[i][j] = True
if length > len(best): best = s[i:j+1]
return best
Approach 2 — Expand Around Center
def longest_palindrome(s):
def grow(l, r):
while l >= 0 and r < len(s) and s[l] == s[r]:
l -= 1; r += 1
return s[l+1:r]
best = ""
for i in range(len(s)):
for p in (grow(i, i), grow(i, i+1)):
if len(p) > len(best): best = p
return best
fastest path for practical n; Manacher is O(n) but rarely required live.
Problem 9: String to Integer (atoi)medium
Statement
Parse an integer from a string. Skip leading whitespace, optional sign, consume digits, stop at non-digit, clamp to 32-bit range.
Approach — State Machine
INT_MIN, INT_MAX = -2**31, 2**31 - 1
def my_atoi(s):
i, n = 0, len(s)
while i < n and s[i] == ' ': i += 1
sign = 1
if i < n and s[i] in '+-':
sign = -1 if s[i] == '-' else 1
i += 1
val = 0
while i < n and s[i].isdigit():
val = val * 10 + (ord(s[i]) - ord('0'))
if val * sign >= INT_MAX: return INT_MAX
if val * sign <= INT_MIN: return INT_MIN
i += 1
return val * sign
Problem 10: Rotate Imagemedium
Statement
Rotate an n×n matrix 90° clockwise, in place.
Approach — Transpose then Reverse Each Row
def rotate(M):
n = len(M)
for i in range(n):
for j in range(i + 1, n):
M[i][j], M[j][i] = M[j][i], M[i][j] # transpose
for row in M:
row.reverse()
Clockwise = transpose + horizontal flip. Counter-clockwise = transpose + vertical flip.
Linked Lists
Pointer mechanics under pressure. Almost every list problem decomposes to: dummy node, two pointers (slow/fast), in-place reversal, or hash-of-nodes.
Technique Primer
- Dummy node — removes head-vs-body edge cases.
- Slow/Fast — middle, cycle detection, Nth-from-end.
- In-place reverse — three-pointer flip: prev, curr, next.
- Node-map — when nodes need copying or cross-referencing.
Problem 11: Reverse Linked Listeasy
Statement
Reverse a singly linked list.
Iterative Three-Pointer
def reverse_list(head):
prev, curr = None, head
while curr:
nxt = curr.next
curr.next = prev
prev = curr
curr = nxt
return prev
Recursive
def reverse_rec(head):
if not head or not head.next: return head
new_head = reverse_rec(head.next)
head.next.next = head
head.next = None
return new_head
Stack depth = list length; can blow recursion limits on long lists.
Problem 12: Add Two Numbersmedium
Statement
Two numbers are stored as linked lists, digits reversed (least-significant first). Return the sum as a list.
Dummy + Carry
def add_two(l1, l2):
dummy = tail = ListNode(0)
carry = 0
while l1 or l2 or carry:
d1 = l1.val if l1 else 0
d2 = l2.val if l2 else 0
s = d1 + d2 + carry
carry, digit = divmod(s, 10)
tail.next = ListNode(digit); tail = tail.next
if l1: l1 = l1.next
if l2: l2 = l2.next
return dummy.next
Problem 13: Merge K Sorted Listshard
Statement
Merge k sorted lists of total n elements.
Approach 1 — Pairwise Merge
Merge pairs repeatedly until one list remains.
Approach 2 — Min-Heap of Heads
import heapq
def merge_k(lists):
h = []
for i, node in enumerate(lists):
if node: heapq.heappush(h, (node.val, i, node))
dummy = tail = ListNode(0)
while h:
_, i, node = heapq.heappop(h)
tail.next = node; tail = node
if node.next:
heapq.heappush(h, (node.next.val, i, node.next))
return dummy.next
Heap holds at most one node per list; tie-break on i to avoid comparing nodes.
Problem 14: LRU Cachemedium
Statement
Design an LRU cache supporting get and put in O(1).
Hash-Map + Doubly Linked List
class Node:
__slots__ = ("k","v","prev","next")
def __init__(self, k=0, v=0):
self.k, self.v = k, v
self.prev = self.next = None
class LRUCache:
def __init__(self, capacity):
self.cap = capacity
self.m = {}
self.head = Node() # most-recent side
self.tail = Node() # least-recent side
self.head.next = self.tail
self.tail.prev = self.head
def _remove(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def _add_front(self, node):
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
def get(self, k):
if k not in self.m: return -1
n = self.m[k]
self._remove(n); self._add_front(n)
return n.v
def put(self, k, v):
if k in self.m:
n = self.m[k]; n.v = v
self._remove(n); self._add_front(n); return
if len(self.m) == self.cap:
lru = self.tail.prev
self._remove(lru); del self.m[lru.k]
n = Node(k, v)
self.m[k] = n; self._add_front(n)
HashMap indexes a doubly linked list so both update sites are O(1).
Problem 15: Copy List With Random Pointermedium
Statement
Each node has val, next, and random. Deep-copy the list.
Approach 1 — Hash Map of Clones
def copy_random_list(head):
if not head: return None
m = {}
cur = head
while cur:
m[cur] = Node(cur.val); cur = cur.next
cur = head
while cur:
m[cur].next = m.get(cur.next)
m[cur].random = m.get(cur.random)
cur = cur.next
return m[head]
Approach 2 — Interleave Clones (O(1) extra)
Insert a clone after each original, wire randoms using orig.random.next, then un-weave.
impressive but rarely needed; state as optional.
Problem 16: Reorder Listmedium
Statement
Given L0 → L1 → … → Ln, reorder to L0 → Ln → L1 → Ln-1 → …
Find Middle, Reverse Tail, Merge
def reorder_list(head):
if not head or not head.next: return
# 1. middle
slow = fast = head
while fast.next and fast.next.next:
slow = slow.next; fast = fast.next.next
second = slow.next; slow.next = None
# 2. reverse second half
prev, curr = None, second
while curr:
nxt = curr.next; curr.next = prev
prev = curr; curr = nxt
# 3. weave
first, second = head, prev
while second:
t1, t2 = first.next, second.next
first.next = second
second.next = t1
first, second = t1, t2
three canonical sub-routines bolted together.
Trees & Graphs
Recursion, BFS/DFS, and graph modeling. The decisive skill here is recognising when an "array of strings" or "matrix" problem is really a graph in disguise.
Decision Shortcuts
| Question asks… | Go-to technique |
|---|---|
| shortest path (unweighted) | BFS from source |
| any path / all paths | DFS + backtrack |
| cycle / ordering in DAG | Topological sort (Kahn or DFS) |
| connected components | Union-Find or BFS flood |
| shortest path (weighted non-neg) | Dijkstra (min-heap) |
Problem 17: Binary Tree Level Order Traversalmedium
Statement
Return the node values grouped by level.
BFS with Level-Sized Loop
from collections import deque
def level_order(root):
if not root: return []
q = deque([root]); out = []
while q:
level = []
for _ in range(len(q)):
n = q.popleft()
level.append(n.val)
if n.left: q.append(n.left)
if n.right: q.append(n.right)
out.append(level)
return out
where w is max width.
Problem 18: Validate Binary Search Treemedium
Statement
Return True iff tree is a valid BST (strict inequality).
Approach 1 — In-Order Must Be Strictly Increasing
def is_valid_bst(root):
prev = [None]
def inorder(n):
if not n: return True
if not inorder(n.left): return False
if prev[0] is not None and n.val <= prev[0]: return False
prev[0] = n.val
return inorder(n.right)
return inorder(root)
Approach 2 — Bound Propagation
def is_valid_bst2(root, lo=float('-inf'), hi=float('inf')):
if not root: return True
if not (lo < root.val < hi): return False
return (is_valid_bst2(root.left, lo, root.val) and
is_valid_bst2(root.right, root.val, hi))
Problem 19: Lowest Common Ancestormedium
Statement
Find LCA of two nodes in a binary tree (both exist).
Post-Order One-Pass (Binary Tree)
def lca(root, p, q):
if not root or root is p or root is q: return root
l = lca(root.left, p, q)
r = lca(root.right, p, q)
if l and r: return root
return l or r
Two non-null children means root is the split; else propagate the single found side.
BST variant — O(h) without recursion
def lca_bst(root, p, q):
while root:
if p.val < root.val and q.val < root.val: root = root.left
elif p.val > root.val and q.val > root.val: root = root.right
else: return root
Problem 20: Serialize and Deserialize Binary Treehard
Statement
Round-trip an arbitrary binary tree through a string.
Pre-Order with Null Sentinels
SEP, NULL = ",", "#"
def serialize(root):
out = []
def go(n):
if not n: out.append(NULL); return
out.append(str(n.val)); go(n.left); go(n.right)
go(root)
return SEP.join(out)
def deserialize(data):
it = iter(data.split(SEP))
def go():
v = next(it)
if v == NULL: return None
n = TreeNode(int(v))
n.left = go(); n.right = go()
return n
return go()
Pre-order lets the reader reconstruct via the same recursion pattern.
Problem 21: Binary Tree Max Path Sumhard
Statement
Path = any node-to-node path through the tree; maximize its sum. Values may be negative.
Post-Order, Track Global Max
def max_path_sum(root):
best = [float('-inf')]
def gain(n):
if not n: return 0
l = max(0, gain(n.left))
r = max(0, gain(n.right))
best[0] = max(best[0], n.val + l + r)
return n.val + max(l, r)
gain(root)
return best[0]
The return value is the "extendable" half; the global includes both sides forking at the node.
Problem 22: Clone Graphmedium
Statement
Given a reference to a node in a connected undirected graph, return a deep copy.
DFS with Visited Map
def clone_graph(node):
if not node: return None
m = {}
def dfs(u):
if u in m: return m[u]
c = Node(u.val); m[u] = c
for v in u.neighbors: c.neighbors.append(dfs(v))
return c
return dfs(node)
Problem 23: Course Schedule I / IImedium
Statement
I — can you finish all courses? II — return a valid order. Both reduce to cycle detection / topological sort.
Kahn's Algorithm (BFS over indegrees)
from collections import defaultdict, deque
def find_order(n, prerequisites):
g = defaultdict(list); indeg = [0] * n
for a, b in prerequisites: # b -> a
g[b].append(a); indeg[a] += 1
q = deque(i for i in range(n) if indeg[i] == 0)
out = []
while q:
u = q.popleft(); out.append(u)
for v in g[u]:
indeg[v] -= 1
if indeg[v] == 0: q.append(v)
return out if len(out) == n else [] # [] = cycle
DFS variant (gray/white/black)
def find_order_dfs(n, prerequisites):
g = defaultdict(list)
for a, b in prerequisites: g[b].append(a)
WHITE, GRAY, BLACK = 0, 1, 2
state = [WHITE]*n; out = []
def dfs(u):
if state[u] == GRAY: return False # cycle
if state[u] == BLACK: return True
state[u] = GRAY
for v in g[u]:
if not dfs(v): return False
state[u] = BLACK; out.append(u); return True
for i in range(n):
if not dfs(i): return []
return out[::-1]
Problem 24: Word Ladderhard
Statement
Shortest transformation from begin to end where each step changes one letter and each intermediate word is in the dictionary.
BFS over Pattern Buckets
from collections import defaultdict, deque
def ladder_length(begin, end, words):
if end not in words: return 0
buckets = defaultdict(list)
L = len(begin)
for w in words | {begin}:
for i in range(L):
buckets[w[:i]+"*"+w[i+1:]].append(w)
q = deque([(begin, 1)]); seen = {begin}
while q:
w, d = q.popleft()
if w == end: return d
for i in range(L):
key = w[:i]+"*"+w[i+1:]
for nb in buckets[key]:
if nb not in seen:
seen.add(nb); q.append((nb, d + 1))
buckets[key] = [] # never revisit this bucket
return 0
pattern bucketing collapses neighbours from O(N) to O(L).
Problem 25: Number of Islandsmedium
Statement
Count connected groups of '1' in a binary grid (4-neighborhood).
DFS Flood-Fill
def num_islands(grid):
if not grid: return 0
R, C = len(grid), len(grid[0])
def dfs(r, c):
if r < 0 or r >= R or c < 0 or c >= C or grid[r][c] != '1': return
grid[r][c] = '0'
dfs(r+1,c); dfs(r-1,c); dfs(r,c+1); dfs(r,c-1)
cnt = 0
for r in range(R):
for c in range(C):
if grid[r][c] == '1':
cnt += 1; dfs(r, c)
return cnt
worst-case recursion.
Union-Find variant
Useful when cells can turn land on/off over time (see LeetCode 305 "Islands II").
Problem 26: Pacific Atlantic Water Flowmedium
Statement
Return cells from which water can flow to both oceans. Top+left edges touch Pacific; bottom+right touch Atlantic. Water flows to equal-or-lower neighbours.
Reverse BFS from Each Ocean
def pacific_atlantic(h):
if not h: return []
R, C = len(h), len(h[0])
pac, atl = set(), set()
def dfs(r, c, visited):
visited.add((r, c))
for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)):
nr, nc = r+dr, c+dc
if 0 <= nr < R and 0 <= nc < C and (nr,nc) not in visited and h[nr][nc] >= h[r][c]:
dfs(nr, nc, visited)
for c in range(C): dfs(0, c, pac); dfs(R-1, c, atl)
for r in range(R): dfs(r, 0, pac); dfs(r, C-1, atl)
return [list(p) for p in pac & atl]
reverse flow lets you avoid recomputing per-cell.
Problem 27: Alien Dictionaryhard
Statement
Given a list of words sorted by an unknown alphabet, recover the letter order.
Build Edges from Adjacent Pair, Then Topo Sort
from collections import defaultdict, deque
def alien_order(words):
g = defaultdict(set); indeg = {c: 0 for w in words for c in w}
for a, b in zip(words, words[1:]):
for x, y in zip(a, b):
if x != y:
if y not in g[x]:
g[x].add(y); indeg[y] += 1
break
else:
if len(a) > len(b): return "" # invalid prefix case
q = deque(c for c, d in indeg.items() if d == 0)
out = []
while q:
c = q.popleft(); out.append(c)
for n in g[c]:
indeg[n] -= 1
if indeg[n] == 0: q.append(n)
return "".join(out) if len(out) == len(indeg) else ""
where C = total characters; letter alphabet is bounded so graph is tiny.
Dynamic Programming
Recurrence first, memoization second, tabulation third. The sequence of steps — in that order — is what interviewers watch for.
DP Problem-Solving Ladder
- Identify the state: what parameters pin down a subproblem?
- Write the recurrence: relate state to smaller states.
- Spot the base cases and the answer cell.
- Implement top-down (memo) to verify; rewrite bottom-up to optimize space.
Problem 28: Climbing Stairseasy
Statement
Ways to climb n stairs taking 1 or 2 at a time.
Fibonacci with O(1) Memory
def climb(n):
a, b = 1, 1
for _ in range(n):
a, b = b, a + b
return a
Problem 29: House Robber I & IImedium
Statement (I)
Maximize sum without taking adjacent elements.
Two-State DP
def rob(nums):
prev = curr = 0
for x in nums:
prev, curr = curr, max(curr, prev + x)
return curr
House Robber II (circular)
Run linear-rob on nums[1:] and nums[:-1]; take max. Circularity means index 0 and n-1 can't both be chosen.
def rob_circular(nums):
if len(nums) == 1: return nums[0]
def linear(a):
prev = curr = 0
for x in a:
prev, curr = curr, max(curr, prev + x)
return curr
return max(linear(nums[1:]), linear(nums[:-1]))
Problem 30: Longest Increasing Subsequencemedium
Statement
Length of the longest strictly increasing subsequence.
Approach 1 — DP O(n^2)
def lis_dp(nums):
n = len(nums)
dp = [1] * n
for i in range(n):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp or [0])
Approach 2 — Patience Sorting (Binary Search)
from bisect import bisect_left
def lis(nums):
tails = []
for x in nums:
i = bisect_left(tails, x)
if i == len(tails): tails.append(x)
else: tails[i] = x
return len(tails)
tails[i] = smallest possible tail of an increasing subseq of length i+1.
Problem 31: Longest Common Subsequencemedium
Statement
Length of LCS of two strings.
2-D DP
def lcs(a, b):
m, n = len(a), len(b)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if a[i-1] == b[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
, can reduce to O(min(m,n)) with rolling rows.
Problem 32: Edit Distancehard
Statement
Minimum insertions/deletions/substitutions to turn a into b.
Levenshtein DP
def edit_distance(a, b):
m, n = len(a), len(b)
dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(m+1): dp[i][0] = i
for j in range(n+1): dp[0][j] = j
for i in range(1, m+1):
for j in range(1, n+1):
if a[i-1] == b[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = 1 + min(dp[i-1][j], # delete
dp[i][j-1], # insert
dp[i-1][j-1]) # substitute
return dp[m][n]
.
Problem 33: Coin Change I & IImedium
Statement (I)
Fewest coins summing to amount, or -1.
Unbounded Knapsack DP
def coin_change(coins, amount):
INF = amount + 1
dp = [0] + [INF] * amount
for a in range(1, amount + 1):
for c in coins:
if c <= a:
dp[a] = min(dp[a], dp[a-c] + 1)
return -1 if dp[amount] == INF else dp[amount]
Coin Change II — count ways (order-independent)
def coin_ways(coins, amount):
dp = [0]*(amount+1); dp[0] = 1
for c in coins: # outer = coin
for a in range(c, amount+1): # inner = amount
dp[a] += dp[a-c]
return dp[amount]
return dp[amount]
Problem 34: Word Breakmedium
Statement
Can s be segmented into dictionary words?
DP on Prefix
def word_break(s, words):
wset = set(words)
n = len(s)
dp = [False] * (n + 1); dp[0] = True
max_len = max(map(len, wset), default=0)
for i in range(1, n + 1):
for j in range(max(0, i - max_len), i):
if dp[j] and s[j:i] in wset:
dp[i] = True; break
return dp[n]
where L = longest dict word.
Problem 35: Partition Equal Subset Summedium
Statement
Can the array be split into two equal-sum subsets?
Subset-Sum with Bitset
def can_partition(nums):
s = sum(nums)
if s % 2: return False
target = s // 2
dp = 1 # bit 0 set = sum 0 reachable
for x in nums:
dp |= dp << x
return (dp >> target) & 1 == 1
Python ints are arbitrary-precision so the bitset is a single integer.
Problem 36: Best Time to Buy/Sell Stock I–IVhard
I — single transaction
def max_profit_1(prices):
best, lo = 0, float('inf')
for p in prices:
lo = min(lo, p); best = max(best, p - lo)
return best
II — unlimited transactions
def max_profit_2(prices):
return sum(max(0, b - a) for a, b in zip(prices, prices[1:]))
III — at most two transactions
def max_profit_3(prices):
b1 = b2 = float('-inf'); s1 = s2 = 0
for p in prices:
b1 = max(b1, -p)
s1 = max(s1, b1 + p)
b2 = max(b2, s1 - p)
s2 = max(s2, b2 + p)
return s2
IV — at most k transactions
def max_profit_k(k, prices):
n = len(prices)
if k >= n // 2: return max_profit_2(prices)
buy = [float('-inf')] * (k + 1)
sell = [0] * (k + 1)
for p in prices:
for j in range(1, k + 1):
buy[j] = max(buy[j], sell[j-1] - p)
sell[j] = max(sell[j], buy[j] + p)
return sell[k]
Problem 37: Regular Expression Matchinghard
Statement
Match s against a pattern with . (any single) and * (zero-or-more of previous).
2-D DP
def is_match(s, p):
m, n = len(s), len(p)
dp = [[False]*(n+1) for _ in range(m+1)]
dp[0][0] = True
for j in range(1, n+1):
if p[j-1] == '*': dp[0][j] = dp[0][j-2]
for i in range(1, m+1):
for j in range(1, n+1):
if p[j-1] == '*':
dp[i][j] = dp[i][j-2] # zero copies
if p[j-2] == '.' or p[j-2] == s[i-1]:
dp[i][j] |= dp[i-1][j] # one more
elif p[j-1] == '.' or p[j-1] == s[i-1]:
dp[i][j] = dp[i-1][j-1]
return dp[m][n]
?/* — different recurrence), minimum-cost regex match, NFA compilation.Problem 38: Decode Waysmedium
Statement
Number of ways to decode a digit string where '1'→'A' … '26'→'Z'.
Two-Variable DP
def num_decodings(s):
if not s or s[0] == '0': return 0
prev2, prev1 = 1, 1
for i in range(1, len(s)):
curr = 0
if s[i] != '0': curr += prev1
two = int(s[i-1:i+1])
if 10 <= two <= 26: curr += prev2
prev2, prev1 = prev1, curr
return prev1
Fibonacci-shaped recurrence with zero/out-of-range guards.
Heap & Stack
Monotonic stacks for "next greater/smaller" and heaps for "top K / running median". These two structures solve a surprisingly large slice of interview asks.
When to reach for what
- Top-K in a stream → min-heap of size K.
- Running median → two heaps.
- Next greater / previous smaller → monotonic stack.
- Max sub-rectangle in histogram → monotonic stack with bounds.
Problem 39: Kth Largest Element in an Arraymedium
Statement
Return the k-th largest number in an unsorted array.
Sort
def kth_largest_sort(nums, k):
return sorted(nums, reverse=True)[k-1]
Min-Heap of Size K
import heapq
def kth_largest_heap(nums, k):
h = []
for x in nums:
heapq.heappush(h, x)
if len(h) > k: heapq.heappop(h)
return h[0]
Quick-Select (average linear)
import random
def quickselect(nums, k):
k = len(nums) - k # k-th largest = index k in ascending
def select(l, r):
pivot = nums[random.randint(l, r)]
lo, hi = l, r
i = l
while i <= hi:
if nums[i] < pivot: nums[lo], nums[i] = nums[i], nums[lo]; lo += 1; i += 1
elif nums[i] > pivot: nums[hi], nums[i] = nums[i], nums[hi]; hi -= 1
else: i += 1
if k < lo: return select(l, lo - 1)
if k > hi: return select(hi + 1, r)
return nums[k]
return select(0, len(nums) - 1)
Problem 40: Merge K Sorted Lists (Heap Variant)hard
Same problem as Module 2 — re-examined through the heap lens. See Problem 13 for the code; here the mental model is that the min-heap is exactly the frontier of smallest unmerged heads.
Problem 41: Find Median from Data Streamhard
Statement
Support add_num and find_median efficiently on a growing stream.
Two Heaps
import heapq
class MedianFinder:
def __init__(self):
self.lo = [] # max-heap (negated)
self.hi = [] # min-heap
def add_num(self, x):
heapq.heappush(self.lo, -x)
heapq.heappush(self.hi, -heapq.heappop(self.lo))
if len(self.hi) > len(self.lo):
heapq.heappush(self.lo, -heapq.heappop(self.hi))
def find_median(self):
if len(self.lo) > len(self.hi):
return -self.lo[0]
return (-self.lo[0] + self.hi[0]) / 2
Invariant: len(lo) == len(hi) or len(lo) == len(hi)+1, and every element in lo ≤ every element in hi.
Problem 42: Top K Frequent Elementsmedium
Bucket Sort on Frequency
from collections import Counter
def top_k_frequent(nums, k):
cnt = Counter(nums)
buckets = [[] for _ in range(len(nums) + 1)]
for x, c in cnt.items():
buckets[c].append(x)
out = []
for f in range(len(buckets) - 1, 0, -1):
out.extend(buckets[f])
if len(out) >= k: return out[:k]
return out
Frequencies are bounded by n, so buckets are dense.
Heap alternative
import heapq
from collections import Counter
def top_k_frequent_heap(nums, k):
c = Counter(nums)
return [x for x, _ in heapq.nlargest(k, c.items(), key=lambda kv: kv[1])]
Problem 43: Task Schedulermedium
Statement
Minimum CPU intervals to finish tasks, where each same-type task must be separated by cooldown n.
Count-Based Formula
from collections import Counter
def least_interval(tasks, n):
c = Counter(tasks).values()
m = max(c)
n_max = sum(1 for v in c if v == m)
return max(len(tasks), (m - 1) * (n + 1) + n_max)
Place most-frequent first; count idles; formula falls out by counting "full frames" of size n+1.
Heap variant (works with richer constraints)
import heapq
from collections import Counter, deque
def least_interval_heap(tasks, n):
h = [-v for v in Counter(tasks).values()]
heapq.heapify(h)
q = deque() # (avail_time, neg_count)
t = 0
while h or q:
t += 1
if h:
c = heapq.heappop(h) + 1
if c < 0: q.append((t + n, c))
if q and q[0][0] == t:
heapq.heappush(h, q.popleft()[1])
return t
Problem 44: Min Stackeasy
Statement
Support push/pop/top plus getMin in O(1).
Pair Each Element with Running Min
class MinStack:
def __init__(self):
self.st = [] # (value, running_min)
def push(self, x):
m = x if not self.st else min(x, self.st[-1][1])
self.st.append((x, m))
def pop(self): self.st.pop()
def top(self): return self.st[-1][0]
def getMin(self): return self.st[-1][1]
Problem 45: Daily Temperaturesmedium
Statement
For each day, days until a warmer temperature (0 if never).
Monotonic Stack of Pending Indices
def daily_temperatures(T):
n = len(T); out = [0]*n
st = []
for i, t in enumerate(T):
while st and T[st[-1]] < t:
j = st.pop()
out[j] = i - j
st.append(i)
return out
Each index pushed and popped once.
Problem 46: Largest Rectangle in Histogramhard
Monotonic Increasing Stack + Sentinel
def largest_rectangle(h):
st = []; best = 0
h = h + [0] # sentinel forces flush
for i, x in enumerate(h):
while st and h[st[-1]] > x:
j = st.pop()
left = st[-1] if st else -1
best = max(best, h[j] * (i - left - 1))
st.append(i)
return best
When a bar is popped, the rectangle spanning from the previous stacked index to i is exactly its height×width.
Binary Search
Not just for sorted arrays. The real power is binary search on the answer: anything monotone can be searched.
Universal Template
def bsearch(lo, hi, feasible):
# returns smallest x in [lo, hi] with feasible(x) == True
while lo < hi:
mid = (lo + hi) // 2
if feasible(mid): hi = mid
else: lo = mid + 1
return lo
If feasible is monotone (False..False..True..True), this converges to the transition.
Problem 47: Classic + Lower/Upper Boundeasy
Lower bound (first index ≥ x)
def lower_bound(a, x):
lo, hi = 0, len(a)
while lo < hi:
mid = (lo + hi) // 2
if a[mid] < x: lo = mid + 1
else: hi = mid
return lo
Upper bound (first index > x)
def upper_bound(a, x):
lo, hi = 0, len(a)
while lo < hi:
mid = (lo + hi) // 2
if a[mid] <= x: lo = mid + 1
else: hi = mid
return lo
Problem 48: Search in Rotated Sorted Arraymedium
Identify Sorted Half, Then Bisect
def search_rotated(a, t):
lo, hi = 0, len(a) - 1
while lo <= hi:
mid = (lo + hi) // 2
if a[mid] == t: return mid
if a[lo] <= a[mid]: # left sorted
if a[lo] <= t < a[mid]: hi = mid - 1
else: lo = mid + 1
else: # right sorted
if a[mid] < t <= a[hi]: lo = mid + 1
else: hi = mid - 1
return -1
Problem 49: Find Minimum in Rotated Sorted Arraymedium
Compare to Rightmost
def find_min(a):
lo, hi = 0, len(a) - 1
while lo < hi:
mid = (lo + hi) // 2
if a[mid] > a[hi]: lo = mid + 1
else: hi = mid
return a[lo]
Problem 50: Find Peak Elementmedium
Climb Upslope
def find_peak(a):
lo, hi = 0, len(a) - 1
while lo < hi:
mid = (lo + hi) // 2
if a[mid] < a[mid + 1]: lo = mid + 1
else: hi = mid
return lo
With sentinels -∞ on either side, binary search on the slope is provably correct.
Problem 51: Koko Eating Bananasmedium
Statement
Minimum eat-rate k so Koko finishes piles in h hours; k bananas per hour, no stacking piles.
Binary Search on Rate
import math
def min_eating_speed(piles, h):
def feasible(k):
return sum(math.ceil(p / k) for p in piles) <= h
lo, hi = 1, max(piles)
while lo < hi:
mid = (lo + hi) // 2
if feasible(mid): hi = mid
else: lo = mid + 1
return lo
where M = max pile.
Problem 52: Capacity to Ship Packages in D Daysmedium
Binary Search on Capacity
def ship_within_days(weights, D):
def feasible(cap):
days, cur = 1, 0
for w in weights:
if cur + w > cap: days += 1; cur = 0
cur += w
return days <= D
lo, hi = max(weights), sum(weights)
while lo < hi:
mid = (lo + hi) // 2
if feasible(mid): hi = mid
else: lo = mid + 1
return lo
Problem 53: Median of Two Sorted Arrayshard
Partition Binary Search
def find_median_sorted(A, B):
if len(A) > len(B): A, B = B, A
m, n = len(A), len(B)
lo, hi = 0, m
half = (m + n + 1) // 2
while lo <= hi:
i = (lo + hi) // 2
j = half - i
aL = float('-inf') if i == 0 else A[i-1]
aR = float('inf') if i == m else A[i]
bL = float('-inf') if j == 0 else B[j-1]
bR = float('inf') if j == n else B[j]
if aL <= bR and bL <= aR:
if (m + n) % 2: return max(aL, bL)
return (max(aL, bL) + min(aR, bR)) / 2
if aL > bR: hi = i - 1
else: lo = i + 1
Binary search partitions A so that left-halves of A and B together are half of the merged array.
Problem 54: Split Array Largest Sumhard
Binary Search on Max-Subarray-Sum
def split_array(nums, m):
def feasible(cap):
parts, cur = 1, 0
for x in nums:
if cur + x > cap: parts += 1; cur = 0
cur += x
return parts <= m
lo, hi = max(nums), sum(nums)
while lo < hi:
mid = (lo + hi) // 2
if feasible(mid): hi = mid
else: lo = mid + 1
return lo
Backtracking
Search trees, pruning, state restoration.
N-Queens
Place N queens on N×N board with no two attacking. Classic backtracking: place row by row, prune on column + diagonal conflicts.
def solve(n):
cols, d1, d2 = set(), set(), set()
out = []
def bt(r, board):
if r == n: out.append(board[:]); return
for c in range(n):
if c in cols or r-c in d1 or r+c in d2: continue
cols.add(c); d1.add(r-c); d2.add(r+c)
board.append(c); bt(r+1, board); board.pop()
cols.remove(c); d1.remove(r-c); d2.remove(r+c)
bt(0, []); return out
Branching factor pruned by constraint sets; space is stack + 3 sets.
Subsets & Permutations
def subsets(nums):
out = [[]]
for x in nums:
out += [s + [x] for s in out]
return out
def permute(nums):
if not nums: return [[]]
return [[x] + p for i, x in enumerate(nums) for p in permute(nums[:i]+nums[i+1:])]
Every subset/permutation costs O(N) to materialize.
Combination Sum
def combSum(cands, target):
cands.sort(); out = []
def bt(i, path, rem):
if rem == 0: out.append(path[:]); return
for j in range(i, len(cands)):
if cands[j] > rem: break
path.append(cands[j]); bt(j, path, rem-cands[j]); path.pop()
bt(0, [], target); return out
Bit Manipulation
XOR tricks, bit counting, masking.
Single Number (XOR)
All elements appear twice except one. XOR cancels pairs: x ^ x = 0, 0 ^ y = y.
def single(nums):
r = 0
for x in nums: r ^= x
return r
Number of 1 Bits (Hamming Weight)
def popcount(n):
c = 0
while n: n &= n-1; c += 1
return c
Trick: n & (n-1) clears lowest set bit. Iterations = popcount.
Counting Bits 0..n
def count_bits(n):
dp = [0]*(n+1)
for i in range(1, n+1): dp[i] = dp[i >> 1] + (i & 1)
return dp
DP on i >> 1: last bit from i & 1.
Power of Two / Missing Number
def is_pow2(n): return n > 0 and (n & (n-1)) == 0
def missing(nums):
# XOR trick: XOR all indices + values
r = len(nums)
for i, x in enumerate(nums): r ^= i ^ x
return r
Sliding Window & Two Pointers
Monotonic window state, O(n) traversals.
Minimum Window Substring
from collections import Counter
def minWindow(s, t):
need = Counter(t); missing = len(t)
i = lo = 0; hi = -1
for j, c in enumerate(s, 1):
if need[c] > 0: missing -= 1
need[c] -= 1
if missing == 0:
while need[s[i]] < 0: need[s[i]] += 1; i += 1
if hi == -1 or j - i < hi - lo: lo, hi = i, j
need[s[i]] += 1; missing += 1; i += 1
return s[lo:hi] if hi > -1 else ''
Each char entered/exited window at most twice.
Find All Anagrams / Permutation in String
def findAnagrams(s, p):
from collections import Counter
need = Counter(p); window = Counter()
res = []
for i, c in enumerate(s):
window[c] += 1
if i >= len(p):
d = s[i-len(p)]
window[d] -= 1
if window[d] == 0: del window[d]
if window == need: res.append(i-len(p)+1)
return res
Max Sliding Window (Monotonic Deque)
from collections import deque
def maxSliding(nums, k):
q = deque(); out = []
for i, x in enumerate(nums):
while q and nums[q[-1]] < x: q.pop()
q.append(i)
if q[0] == i-k: q.popleft()
if i >= k-1: out.append(nums[q[0]])
return out
Each index pushed/popped at most once.
Sorting & Interval Patterns
Interval merge, schedule, partition.
Merge Intervals
def merge(ivs):
ivs.sort()
out = [ivs[0]]
for s, e in ivs[1:]:
if s <= out[-1][1]: out[-1][1] = max(out[-1][1], e)
else: out.append([s, e])
return out
Meeting Rooms I/II
def canAttend(ivs):
ivs.sort()
return all(ivs[i-1][1] <= ivs[i][0] for i in range(1, len(ivs)))
def minRooms(ivs):
import heapq
ivs.sort()
h = []
for s, e in ivs:
if h and h[0] <= s: heapq.heappop(h)
heapq.heappush(h, e)
return len(h)
Min-heap tracks earliest-ending ongoing meeting.
Sort Colors (Dutch Flag)
def sortColors(nums):
lo, hi, i = 0, len(nums)-1, 0
while i <= hi:
if nums[i] == 0: nums[i], nums[lo] = nums[lo], nums[i]; lo += 1; i += 1
elif nums[i] == 2: nums[i], nums[hi] = nums[hi], nums[i]; hi -= 1
else: i += 1
Mock Interview Walkthroughs
Full 45-minute mock scripts with phantom interviewer dialog.
Mock 1 — "Valid Parentheses" extended to multi-bracket + custom pair
Interviewer: Given a string of brackets (), [], {}, return true if valid. Follow-up: support custom pair mapping.
You: Clarifying questions — are characters outside bracket set ignored? Length bound? Empty string valid? Confirm all three.
You: I'll use a stack. Push every opener; on closer, check top matches. At end, stack must be empty.
def valid(s):
pair = {')':'(', ']':'[', '}':'{'}
st = []
for c in s:
if c in pair.values(): st.append(c)
elif c in pair:
if not st or st.pop() != pair[c]: return False
return not st
You: O(n) time, O(n) space. Test cases: "", "()", "()[]{}", "(]", "([)]", "{[]}" — run through each mentally. Confirm each passes/fails as expected.
Interviewer follow-up: What if valid pairs come as config?
def valid2(s, pairs):
closers = {c: o for o, c in pairs.items()}
st = []
for c in s:
if c in pairs: st.append(c)
elif c in closers:
if not st or st.pop() != closers[c]: return False
return not st
Mock 2 — "Top K Frequent Elements"
Interviewer: Given int array and k, return k most frequent. What if the stream is infinite?
You: Finite case — Counter + heap of size k. O(n log k).
from collections import Counter
import heapq
def topK(nums, k):
c = Counter(nums)
return heapq.nlargest(k, c, key=c.get)
You: For streaming — Count-Min Sketch + min-heap of k current top, update on each event. Accept some error; share bounded RAM constraint.
Cheat Sheet & Pattern Recognition
Which technique fits which problem shape.
Pattern → Approach Table
| Signal in the problem | Reach for… |
|---|---|
| "sorted array", "find pair" | Two pointers |
| "longest / shortest contiguous" | Sliding window |
| "kth smallest / top k" | Heap (size k) |
| "next greater / prev smaller" | Monotonic stack |
| "min/max over all subarrays" | Monotonic deque |
| "count ways / min cost to reach" | DP (1D or 2D) |
| "shortest path", "level order" | BFS |
| "cycle / connectivity" | DFS / Union-Find |
| "interval scheduling" | Sort by end + greedy |
| "sorted range query" | Binary search on answer |
| "permutations / combinations / valid configs" | Backtracking |
| "bit at position / XOR magic" | Bit manipulation |
| "range sum / prefix sum" | Prefix array / BIT |
| "tree path sum / LCA" | Post-order DFS with return |
Complexity Ceiling by n
- n ≤ 10: brute O(n!) or O(2^n) fine — backtracking welcomed.
- n ≤ 20: O(2^n · n) bitmask DP OK.
- n ≤ 500: O(n³) Floyd-Warshall / matrix DP.
- n ≤ 5,000: O(n²) — most DP.
- n ≤ 10⁵: O(n log n) — sort / heap / BIT / segment tree.
- n ≤ 10⁶: O(n) — linear, two-pointer, sliding window.
- n ≥ 10⁹: O(log n) — binary search.
Answer Framework
- Clarify — input bounds, edge cases (empty, one element, negatives, duplicates, overflow).
- Restate — say the problem back in your own words.
- Examples — walk one happy-path + one edge case.
- Brute — state it, quote complexity.
- Optimize — name the pattern you recognize; why this sub-problem has optimal substructure.
- Code — structure first (function signature, variables), then fill in.
- Test — trace through one example + one edge case.
- Complexity — time + space, justified.
- Follow-ups — scale (n > 10⁹, distributed), variants (streaming, 2D).