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.

12 Modules • 80+ Problems • 150+ Code Walkthroughs • Pattern Recognition First

Module 1

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

SignalPatternComplexity
"find pair/triplet summing to target"Sort + two pointers, or hash mapO(n log n) or O(n)
"contiguous subarray / substring"Sliding windowO(n)
"product / sum of prefix"Prefix-sum / prefix-productO(n)
"k-th largest / smallest"Heap / Quick-SelectO(n log k) / O(n)
"in-place shuffle / rotate"Reverse in place / cycle walkO(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.

Input: nums = [2,7,11,15], target = 9 Output: [0,1]
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
Two Sum hash-map scan
Common variants: 3Sum, 4Sum, Two Sum <= K, Two Sum BST (use in-order iterators).
Problem 2: Longest Substring Without Repeating Charactersmedium
Statement

Return the length of the longest substring of s in which no character repeats.

Input: s = "abcabcbb" Output: 3 ("abc")
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.

Sliding window on "abcabcbb"
Variants: longest substring with at most K distinct chars (mod 9), longest repeating char replacement (window + majority count).
Problem 3: Container With Most Watermedium
Statement

Given heights, choose two indices forming a container; maximize min(h[i], h[j]) * (j - i).

Input: height = [1,8,6,2,5,4,8,3,7] Output: 49
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.

Variants: Trapping Rain Water (below), largest rectangle in histogram (monotonic stack).
Problem 4: Trapping Rain Waterhard
Statement

Given a histogram of heights, how much water can be trapped after rain?

Input: height = [0,1,0,2,1,0,1,3,2,1,2,1] Output: 6
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.

Rain Water heights with trapped cells
Variants: Trapping Rain Water II (priority queue on boundary), container with most water (pair, not histogram).
Problem 5: 3Summedium
Statement

Return all unique triplets summing to zero.

Input: nums = [-1,0,1,2,-1,-4] Output: [[-1,-1,2], [-1,0,1]]
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.

Variants: 3Sum Closest (track best diff), 3Sum Smaller (count triplets), 4Sum (one more outer loop).
Problem 6: Maximum Subarray (Kadane)medium
Statement

Find the contiguous subarray with the largest sum.

Input: [-2,1,-3,4,-1,2,1,-5,4] Output: 6 (subarray [4,-1,2,1])
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.

Variants: Maximum Product Subarray (track min too), Circular Subarray (total − min subarray), Maximum Sum Circular, Best Sightseeing Pair.
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).

Variants: prefix-sum variant, prefix-XOR, range-product queries (segment tree).
Problem 8: Longest Palindromic Substringmedium
Statement

Return the longest palindromic substring of s.

Input: "babad" Output: "bab" or "aba"
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.

Variants: Manacher's algorithm, count palindromic substrings, shortest palindrome by prepend (KMP on rev+s).
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

Variants: valid number (LeetCode 65 — more states), integer to English words, Roman-to-integer.
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.

Variants: rotate 180/270, rectangular rotate (transpose then new array), spiral order traversal.
Module 2

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.

Variants: reverse between [m,n], reverse nodes in k-group, reverse doubly linked list.
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

Variants: digits stored MSB first (reverse both first, or use stack), subtract two numbers, multiply two numbers.
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.

Variants: merge k sorted arrays, smallest range covering k lists.
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).

LRU eviction on a capacity-3 cache
Variants: LFU cache, LRU with TTL, LRU with size-weighted entries (e.g. byte cap), concurrent LRU.
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.

Variants: deep clone binary tree with parent/sibling pointers, clone graph (mod 3).
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.

Module 3

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 pathsDFS + backtrack
cycle / ordering in DAGTopological sort (Kahn or DFS)
connected componentsUnion-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.

Variants: zig-zag level order (reverse alternate), right-side view (last of each level), bottom-up level order.
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
Variants: Tarjan offline LCA (Union-Find), LCA with parent pointers (path-climb), LCA of deepest leaves.
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.

Variants: serialize N-ary tree, serialize BST compactly (no nulls needed with bounds), serialize graph.
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)

Variants: BFS clone, copy directed graph, clone graph with weighted edges.
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]
Course Schedule — Kahn's BFS over indegrees
Variants: alien dictionary (char-ordering topo), minimum height trees (peel leaves), parallel courses (layered BFS).
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).

Variants: Word Ladder II (return all shortest paths — BFS + backtrack), minimum genetic mutation.
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").

Variants: Number of Distinct Islands (canonical shape hash), Islands II (online), Number of Closed Islands.
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.

Module 4

Dynamic Programming

Recurrence first, memoization second, tabulation third. The sequence of steps — in that order — is what interviewers watch for.

DP Problem-Solving Ladder

  1. Identify the state: what parameters pin down a subproblem?
  2. Write the recurrence: relate state to smaller states.
  3. Spot the base cases and the answer cell.
  4. 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

Variants: min-cost climbing stairs, steps 1/2/3, decode ways (restricted).
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]))
Variants: House Robber III (tree DP), delete-and-earn (rob on counts).
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.

LIS tails array on [10,9,2,5,3,7,101,18]
Variants: number of LIS (Fenwick-tree count DP), LIS with minimum swap reconstruction, Russian Doll Envelopes.
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.

Variants: shortest common supersequence, min deletions to make equal, longest palindromic subsequence (LCS of s and reversed s).
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]

.

Edit-distance grid for "horse" -> "ros"
Variants: one-edit-away (linear scan), read/write/delete with different costs, Hirschberg's linear-space.
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.

Variants: Word Break II (return all segmentations), concatenated words (count).
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]

Variants: wildcard matching (?/* — 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.

Module 5

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)

Variants: K closest points, top-K frequent, wiggle sort (median pivot).
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.

Variants: sliding-window median (multiset), streaming quantiles, approximate histogram.
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])]
Variants: top-K words (tie-break lex), K most repeated over time window.
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.

Variants: next greater element I/II (circular), stock span, trapping rain water with stack.
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.

Variants: maximal rectangle in binary matrix (row-wise histograms), max square in matrix (DP).
Module 6

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

Variants: with duplicates (worst O(n)), find minimum (below).
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

MODULE 7

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
MODULE 8

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
MODULE 9

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.

MODULE 10

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
MODULE 11

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.

MODULE 12

Cheat Sheet & Pattern Recognition

Which technique fits which problem shape.

Pattern → Approach Table

Signal in the problemReach 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

  1. Clarify — input bounds, edge cases (empty, one element, negatives, duplicates, overflow).
  2. Restate — say the problem back in your own words.
  3. Examples — walk one happy-path + one edge case.
  4. Brute — state it, quote complexity.
  5. Optimize — name the pattern you recognize; why this sub-problem has optimal substructure.
  6. Code — structure first (function signature, variables), then fill in.
  7. Test — trace through one example + one edge case.
  8. Complexity — time + space, justified.
  9. Follow-ups — scale (n > 10⁹, distributed), variants (streaming, 2D).