The Pattern Playbook

A college-friendly, interactive field guide to data-structures & algorithms interviews.
Recognize the pattern Apply the technique Implement with confidence
How to read this guide — the “Onion” method. Every pattern is taught in three click-to-expand layers. Layer 1 is the one-sentence mental model and the trigger that says “this is the pattern.” Layer 2 is the mechanics, sub-types, and reusable code template. Layer 3 is the subtle traps and expert insight. Read all the Layer 1s first to build intuition, then go deeper.
Take the 30-question quiz →
Tip: hover (or tap on mobile) any dotted term for a plain-English definition. There’s also a full glossary at the bottom.

Orientation: How Interviewers Think in Patterns

A DSA interview is rarely a test of whether you can invent an algorithm on the spot. It is a test of pattern recognition under time pressure. Strong candidates don’t see “a problem about painting houses”; they see “this is an optimization over a sequence that smells like optimal substructure — probably DP.” The surface story is a costume; underneath, only about a dozen skeletons recur.

For each family you’ll learn the trigger phrases that betray the pattern, the sub-types it splits into, a reusable code skeleton, the complexity profile interviewers expect, and the pitfalls that sink otherwise-correct solutions.

The Recognition Cheat-Sheet

In an interview, your first 60 seconds should map the prompt onto one of these rows.

If the prompt says……the trigger you should feel…reach for
“maximum/minimum,” “how many ways,” “can you reach” over choicesOverlapping sub-problems + optimal substructureDynamic Programming
“maximize/minimize” but a locally-best choice seems safeExchange argument; sortable by a keyGreedy
“sorted array,” “find the boundary,” “minimize the maximum”Monotonic search spaceBinary Search
“contiguous subarray/substring,” “window,” “at most K”A sliding constraint over a sequenceSliding Window
“pair that sums,” “in-place,” “sorted, two ends”Converging indicesTwo Pointers
“connected,” “shortest path,” “dependency,” “grid”Nodes + edges; reachability/orderGraph BFS/DFS/Union-Find
“all permutations/combinations/subsets,” “place N …”Build-and-undo a decision treeBacktracking
“split the input and combine,” “sort,” “count inversions”Recurse on halves, merge resultsDivide & Conquer
“top K,” “running median,” “merge K streams”Need the best element repeatedlyHeap / Priority Queue

A 90-Second Complexity Refresher

Interviewers expect you to state time and space complexity (Big-O) unprompted. Anchor every solution to this ladder, fastest to slowest:

O(1) O(log n) O(n) O(n log n) O(n²) O(2ⁿ) O(n!)
iRules of thumb for input size n (a modern laptop does ~10⁸ simple operations/second): n ≤ 12 → O(n!) backtracking is fine; n ≤ 20 → O(2ⁿ) bitmask DP; n ≤ 2000 → O(n²) DP; n ≤ 10⁵ → O(n log n) or better; n ≥ 10⁶ → O(n) or O(log n). Working backward from the constraint to the target complexity is itself a pattern-selection tool.

Pattern 1 — Greedy

A greedy algorithm builds a solution one step at a time, and at each step grabs the choice that looks best right now — never reconsidering. It’s the optimist of algorithms: it assumes a sequence of locally optimal moves lands on the globally optimal answer. The whole art is proving that optimism is justified for the specific problem.
Greedy works only when the problem has the greedy-choice property and optimal substructure. In practice you almost always (1) sort the input by a clever key, or (2) keep a heap to always pull the current best, then (3) sweep once making the obvious choice. If a clean sort key exists and a swap argument holds, greedy beats DP because it’s one pass instead of a table.
The senior-level skill is the exchange argument: to prove greedy is correct, assume an optimal solution differs from the greedy one, then show you can swap an element of the optimal for the greedy choice without making it worse — so greedy is at least as good. Classic trap: coin change is greedy for US coins but not for arbitrary coins (e.g. {1,3,4}, target 6: greedy picks 4+1+1 = 3 coins, optimal is 3+3 = 2).

Sub-types you will meet

  • Interval scheduling / activity selection. Maximize non-overlapping intervals. Key: sort by end time, take the earliest finisher.
  • Interval merging & minimum resources. Merge overlaps or count max simultaneous overlap (meeting rooms). Key: sort by start, sweep with a heap of end times.
  • Sorting by a ratio / deadline. Job sequencing, fractional knapsack. Key: sort by value/weight or by deadline.
  • Greedy on a heap. Repeatedly combine the two best elements (Huffman coding, “connect ropes,” task schedulers).
  • Greedy + invariant sweep. Jump Game, Gas Station, Candy — maintain a running “reach” or “tank.”

The reusable skeleton

Greedy almost always collapses to sort, then sweep maintaining one running quantity:

PYTHON · greedy template
def greedy_template(items):
    # 1) Choose the sort key that makes the best choice obvious.
    items.sort(key=lambda x: x.end)   # earliest finish time

    chosen, frontier = [], float('-inf')  # frontier = end-time of last chosen interval.
    # float('-inf') is "negative infinity": a starting value smaller than any real
    # number, so the very first interval always passes the check below.
    for it in items:
        if it.start >= frontier:    # valid if it starts after the last one ended
            chosen.append(it)        # take it (earliest-finishing = greedy pick)
            frontier = it.end        # advance: next item must start >= here
    return chosen                   # one pass; O(n log n) total (sort dominates)

Worked intuition — Activity Selection

Why sort by end time? Finishing earliest leaves the most room for everything after it. Sorting these intervals by end time picks A → C → E (three activities); grabbing a long interval early would block more of the timeline.

A B C D E picked skipped
!Greedy traps. (1) Spend 20 seconds inventing a small adversarial input — does a counterexample exist? (2) Is the sort key right (start vs. end vs. length give different answers)? (3) Decide tie-breaks explicitly. (4) If you can’t justify it with an exchange argument, switch to DP.

Complexity: dominated by the sort — O(n log n) time, O(1)–O(n) space.

Practice — Warm-up: Assign Cookies; Lemonade Change; Best Time to Buy/Sell Stock II. Core: Jump Game I/II; Gas Station; Non-overlapping Intervals; Merge Intervals; Meeting Rooms II; Task Scheduler; Partition Labels. Stretch: Candy; Min Arrows to Burst Balloons; Reorganize String; Hand of Straights; Connect Sticks (Huffman); IPO.

Pattern 2 — Dynamic Programming

Dynamic programming is recursion with memory. You take a problem whose brute-force solution explores an exponential tree of choices, notice the same sub-problems appear over and over, and cache each answer so it’s computed once. DP = “brute-force recursion” + “stop recomputing what you already know.”
Two ingredients license DP: overlapping sub-problems and optimal substructure. The workflow: (1) define the state — the minimal facts that identify a sub-problem; (2) write the transition — how a state is built from smaller states; (3) set base cases; (4) choose top-down memoization or bottom-up tabulation; (5) read the answer off the table. Most of the difficulty is step 1 — choosing the state.
Experts think in DP dimensions: 1-D (House Robber), 2-D (edit distance, knapsack, grid paths), interval DP (state is a range [i,j]), and bitmask DP. The real senior move is state-space reduction: a 2-D table that only depends on the previous row collapses to one row, turning O(nW) space into O(W).

The five-step recipe

1 · State 2 · Transition 3 · Base case 4 · Memo / Table 5 · Answer + optimize

Sub-types you will meet

  • Linear / 1-D DP. Fibonacci, Climbing Stairs, House Robber, Decode Ways, Maximum Subarray (Kadane), Word Break.
  • Knapsack family. 0/1 knapsack, subset-sum, partition equal subset, coin change, target sum. State: dp[i][cap].
  • Two-sequence DP. Longest Common Subsequence, Edit Distance, wildcard matching. State: dp[i][j] over two strings.
  • Grid DP. Unique Paths, Minimum Path Sum, Dungeon Game.
  • Interval DP. Matrix-Chain, Burst Balloons, Palindrome Partitioning II.
  • Sequence / LIS. Longest Increasing Subsequence, Russian Doll Envelopes.
  • Bitmask DP. Travelling Salesman, “Shortest Path Visiting All Nodes” (n ≤ 20).

Two skeletons: top-down and bottom-up

Each placeholder name below is defined in the comments — that’s the part most beginners get stuck on.

PYTHON · the placeholders demystified
# LEGEND — replace each name with your problem's concrete logic:
#   state          = facts that identify a sub-problem, e.g. (index, capacity_left)
#   is_base(s)     = True when s needs no more recursion (e.g. capacity == 0)
#   base_value(s)  = the answer at a base case (e.g. 0)
#   WORST          = the starting "nothing yet" value: -inf if maximizing, +inf if minimizing
#   choices(s)     = the decisions available now, e.g. [skip item, take item]
#   better(a,b)    = max if maximizing, min if minimizing
#   cost(c)        = the immediate value/penalty of making choice c (e.g. value[i])
#   next_state(s,c)= the SMALLER sub-problem you move to after choosing c

from functools import lru_cache
def solve_topdown(n, data):
    @lru_cache(maxsize=None)              # the cache = memoization
    def dp(state):
        if is_base(state): return base_value(state)
        best = WORST
        for choice in choices(state):
            best = better(best, cost(choice) + dp(next_state(state, choice)))
        return best
    return dp(initial_state)

Worked intuition — 0/1 Knapsack table

dp[i][w] = best value using the first i items with capacity w. Either skip item i, or take it if it fits. Each row depends only on the row above — that’s the hook for the O(W) space optimization. (Items here: weight 2/value 3, weight 3/value 4, weight 2/value 1.)

w →012345
000000
item1003333
item2003447
item3003447
!DP failure modes. (1) Under-specified state (forgetting a variable the answer depends on). (2) Wrong iteration order in bottom-up (for 0/1 knapsack with a 1-D array, iterate capacity descending). (3) Off-by-one in the empty-prefix row/column. (4) To return the actual choice (not just its value), store parent pointers and backtrack.

Complexity: (number of states) × (work per transition). 1-D ≈ O(n); knapsack O(nW) — pseudo-polynomial; two-sequence O(nm); interval DP O(n³); bitmask O(2ⁿ·n).

Practice — Warm-up: Climbing Stairs; House Robber; Maximum Subarray. Core: Coin Change I/II; Word Break; LIS; LCS; Edit Distance; Unique Paths; Partition Equal Subset Sum; Decode Ways. Stretch: Burst Balloons; Buy/Sell Stock III & IV; Regex Matching; Stone Game; Russian Doll Envelopes.

Pattern 3 — Divide & Conquer

Divide and conquer solves a problem by splitting it into smaller independent sub-problems of the same type, solving each (usually recursively), and combining their answers. Three verbs: divide, conquer, combine. Merge sort is the canonical picture — halve the array, sort each half, merge.
The decisive question is where the real work lives — in the split or the combine? Merge sort does trivial splitting and heavy merging; quicksort the reverse. The cost is captured by a recurrence T(n) = a·T(n/b) + f(n), and the Master Theorem reads the Big-O straight off a, b, and f(n). Unlike DP, D&C sub-problems don’t overlap, so there’s nothing to memoize.
D&C shines when a linear merge can extract global info neither half sees alone — counting cross-pair inversions, the closest pair of points straddling the dividing line, or the max subarray crossing the midpoint. Beware unbalanced splits (quicksort’s O(n²) worst case) — randomization restores balance.

The Master Theorem in one box

For T(n) = a·T(n/b) + f(n) — where a = number of recursive sub-calls, b = shrink factor, f(n) = divide/combine cost — compare f(n) against nlog_b a:

CaseConditionResult
1 — leaves dominatef(n) = O(nlog_b a − ε)Θ(nlog_b a)
2 — balancedf(n) = Θ(nlog_b a)Θ(nlog_b a · log n)
3 — root dominatesf(n) = Ω(nlog_b a + ε)Θ(f(n))

ε is just a small positive number; it means f(n) is polynomially smaller/larger, not merely equal. Examples: merge sort T(n)=2T(n/2)+Θ(n) → Case 2 → Θ(n log n). Binary search T(n)=T(n/2)+Θ(1) → Θ(log n).

The reusable skeleton

PYTHON · divide & conquer
# Half-open range convention: the active slice is arr[lo:hi]  (lo inclusive, hi exclusive)
def divide_and_conquer(arr, lo, hi):
    if hi - lo <= 1:                 # 0 or 1 element -> trivially solved
        return solve_trivial(arr, lo, hi)
    mid = (lo + hi) // 2            # split: left=arr[lo:mid], right=arr[mid:hi]
    left  = divide_and_conquer(arr, lo, mid)   # solve halves INDEPENDENTLY
    right = divide_and_conquer(arr, mid, hi)
    # combine reads ACROSS the midpoint (this is where the speedup lives):
    return combine(left, right, arr, lo, mid, hi)

Worked intuition — counting inversions

An inversion is a pair i<j with ai>aj (out of order). Brute force is O(n²); D&C counts them in O(n log n) by piggy-backing on merge sort: when merging two sorted halves, every time you pull an element from the right half while elements remain in the left, it forms an inversion with all of them at once.

!D&C traps. (1) Forgetting the cross-boundary case (the answer that straddles the midline is the whole point). (2) Unbalanced splits degrade quicksort/quickselect to O(n²) — use a random pivot. (3) Deep recursion can overflow the stack.

Complexity: balanced D&C with a linear combine is O(n log n); quickselect is O(n) average; binary search and fast exponentiation are O(log n).

Practice — Warm-up: Merge Sort; Binary Search; Pow(x,n); Majority Element. Core: Sort an Array; Kth Largest (quickselect); Maximum Subarray (D&C); Merge k Sorted Lists. Stretch: Count of Smaller Numbers After Self; Reverse Pairs; Count of Range Sum; The Skyline Problem; Closest Pair of Points.

Pattern 4 — Two Pointers & Sliding Window

Instead of nesting two loops (O(n²)) to examine pairs or ranges, you walk two indices through the data in a single pass. “Two pointers” usually means indices starting at opposite ends that converge; “sliding window” means a left and right index bounding a contiguous range that grows and shrinks. Both turn quadratic brute force into linear time.
Sliding-window mental model: right expands the window; whenever it violates a constraint, left contracts until it’s valid again. You keep a running summary (a sum, a count, a frequency map) so each step is O(1). Converging model: on a sorted array, if the pair’s sum is too small move left up, if too big move right down. The invariant: each pointer only moves forward, so total work is O(n).
Classify windows as fixed-size (length k given) vs. variable-size (smallest/largest window satisfying a predicate). The hard cases hinge on whether the validity test is monotonic. Non-monotonic max/min queries need a monotonic deque instead of a plain window.

Sub-types you will meet

  • Converging two pointers (sorted). Two Sum II, 3Sum, Container With Most Water, Trapping Rain Water, valid palindrome.
  • Fast/slow pointers. Linked-list cycle detection (Floyd), find the middle, Happy Number.
  • In-place partition. Remove Duplicates, Move Zeroes, Dutch National Flag (sort colors).
  • Fixed-size window. Max average subarray of length k.
  • Variable-size window. Longest Substring Without Repeating; Minimum Size Subarray Sum; At Most K Distinct; Minimum Window Substring.
  • Monotonic-deque window. Sliding Window Maximum; Shortest Subarray with Sum ≥ K.

The variable-size window skeleton

PYTHON · variable sliding window
def longest_valid_window(s):
    from collections import defaultdict
    count = defaultdict(int)   # char -> frequency in the window (missing keys default to 0)
    left = best = 0
    for right, ch in enumerate(s):
        count[ch] += 1                 # 1) EXPAND: pull s[right] into the window
        # is_valid(count) is YOUR yes/no test, e.g. "len(count) <= K" for "at most K distinct"
        while not is_valid(count):     # 2) CONTRACT while the window breaks the rule
            count[s[left]] -= 1
            if count[s[left]] == 0: del count[s[left]]  # keep len(count) = # distinct chars
            left += 1
        best = max(best, right - left + 1)   # 3) RECORD (window length = right-left+1)
    return best
!Window traps. (1) Converging two pointers need sorted data. (2) Decide whether you want the longest valid window (shrink only when invalid) or the shortest (shrink while still valid). (3) Update the window summary on both expand and contract. (4) Window length is right - left + 1.

Complexity: O(n) for a single pass; O(1) space for pure two-pointer, O(k) for a window backed by a map.

Practice — Warm-up: Two Sum II; Valid Palindrome; Move Zeroes; Squares of a Sorted Array. Core: 3Sum; Container With Most Water; Linked List Cycle; Sort Colors; Longest Substring Without Repeating; Permutation in String. Stretch: Trapping Rain Water; Minimum Window Substring; At Most K Distinct; Sliding Window Maximum; Subarrays with K Different Integers.

Pattern 5 — Graphs: BFS, DFS & Union-Find

A graph is just things (nodes) and relationships between them (edges). A huge share of interview problems are graphs in disguise: a grid, course prerequisites, a friend network. Once you see “entities + connections + a question about reachability, distance, ordering, or grouping,” you’re in graph territory, and three workhorses cover most of it: BFS, DFS, and Union-Find.
BFS explores level by level with a queue → shortest path in an unweighted graph. DFS dives deep with recursion/stack → connectivity, cycle detection, topological ordering. Union-Find answers “are these two in the same group?” and merges groups in near-constant time. For weighted shortest paths you upgrade BFS to Dijkstra; negative edges need Bellman-Ford.
Senior fluency is about representation (adjacency list for sparse graphs) and state design when nodes aren’t explicit (in Word Ladder the nodes are words). Union-Find’s two optimizations — union by rank and path compression — together give amortized α(n) (effectively constant); forgetting them makes it quadratic.

Sub-types you will meet

  • Grid / flood fill. Number of Islands, Flood Fill, Rotting Oranges (multi-source BFS), Surrounded Regions.
  • Connectivity & components. Connected Components, Accounts Merge, Redundant Connection (Union-Find).
  • Topological sort (DAGs). Course Schedule I/II, Alien Dictionary, build-order resolution.
  • Shortest path. Word Ladder (BFS), Network Delay Time (Dijkstra), Cheapest Flights (Bellman-Ford).
  • Cycle detection. Directed (3-color DFS) vs. undirected (Union-Find or parent-tracking DFS).
  • Bipartite / coloring. Is Graph Bipartite, Possible Bipartition.
  • MST. Kruskal (Union-Find) or Prim (heap): Min Cost to Connect All Points.

Three skeletons

PYTHON · BFS · DFS · Union-Find
from collections import deque
# graph = adjacency list: each node maps to a list of neighbors, e.g. {0:[1,2], 1:[0,3]}
def bfs(start, graph):
    q, seen = deque([(start, 0)]), {start}   # pre-add start so it's never re-queued
    while q:
        node, dist = q.popleft()
        if is_goal(node): return dist
        for nxt in graph[node]:
            if nxt not in seen:
                seen.add(nxt)                # mark on ENQUEUE, not dequeue
                q.append((nxt, dist + 1))
    return -1

class DSU:                              # Union-Find / Disjoint Set Union
    def __init__(self, n):
        self.parent = list(range(n)); self.rank = [0]*n  # rank ~ tree height
    def find(self, x):
        while self.parent[x] != x:
            self.parent[x] = self.parent[self.parent[x]]  # path halving (flattens tree)
            x = self.parent[x]
        return x
    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra == rb: return False          # already connected -> a cycle edge
        if self.rank[ra] < self.rank[rb]: ra, rb = rb, ra  # union by rank
        self.parent[rb] = ra
        if self.rank[ra] == self.rank[rb]: self.rank[ra] += 1
        return True

Worked intuition — BFS spreads in rings

BFS visits all nodes at distance 1, then all at distance 2, and so on — so the first time it reaches the goal, it has used the fewest edges.

S 1 1 2 2 2 3 first arrival = shortest path
!Graph traps. (1) Mark visited on enqueue, not dequeue. (2) Directed vs. undirected cycle detection are different algorithms. (3) Dijkstra gives wrong answers on negative edges — use Bellman-Ford. (4) Deep DFS can overflow Python’s stack — use an explicit stack. (5) Union-Find needs both path compression and rank.

Complexity: BFS/DFS O(V+E); Dijkstra O((V+E)·log V); Union-Find O(α(n)) ≈ O(1); topological sort O(V+E).

Practice — Warm-up: Flood Fill; Number of Islands; Find if Path Exists. Core: Rotting Oranges; Course Schedule I & II; Clone Graph; Word Ladder; Is Graph Bipartite; Pacific Atlantic Water Flow. Stretch: Alien Dictionary; Network Delay Time; Cheapest Flights Within K Stops; Min Cost to Connect All Points; Accounts Merge.

Pattern 6 — Backtracking

Backtracking is systematic, exhaustive trial-and-error. You build a candidate one decision at a time; whenever a partial choice can’t possibly lead to a valid answer, you undo it (backtrack) and try the next option. It’s depth-first search over the tree of all decision sequences, with the power to prune whole branches. When a problem asks for all permutations, combinations, subsets, or arrangements, backtracking is almost always the tool.
Every backtracking solution has the same anatomy: a choose–explore–unchoose loop. Choose an option (add to the path), explore by recursing, then unchoose (remove it) to restore state. A start index prevents reusing earlier elements in combinations; a used[] array prevents reuse in permutations. The base case appends a copy of the current path.
The expert difference is pruning power and duplicate handling. Strong pruning (N-Queens checking diagonals before recursing) turns a hopeless O(n!) search into something tractable. Handling duplicates cleanly (sort first, then skip same-value siblings at the same depth) is the most common follow-up. If the tree has overlapping sub-problems and you only need a count, memoize → it becomes DP.

Sub-types you will meet

  • Subsets / power set. Include-or-exclude each element. Subsets, Subsets II.
  • Combinations. Choose k of n, or all combos summing to a target. Combination Sum I/II/III.
  • Permutations. All orderings; with/without duplicates.
  • Partitioning. Palindrome Partitioning, Restore IP Addresses.
  • Grid / constraint search. Word Search, N-Queens, Sudoku Solver, Generate Parentheses.

The universal skeleton

PYTHON · choose · explore · unchoose
# choices = the INPUT list (e.g. nums). start = index this call may pick from.
def backtrack(start, path, results, choices):
    if is_complete(path):              # e.g. len(path)==k, or sum(path)==target
        results.append(path[:])        # append a COPY, not the live list!
        return
    for i in range(start, len(choices)):
        if not is_valid(choices[i], path): continue   # prune dead branches early
        if i > start and choices[i] == choices[i-1]: continue  # skip duplicates (sorted)
        path.append(choices[i])                  # 1) CHOOSE
        backtrack(i + 1, path, results, choices) # 2) EXPLORE (i+1 = no reuse; i = reuse)
        path.pop()                               # 3) UNCHOOSE (restore state)
    return results
!Backtracking traps. (1) Appending the live list instead of a copy makes every result point to the same (eventually empty) list — always path[:]. (2) Forgetting to undo state corrupts sibling branches. (3) Sort inputs and skip same-value siblings to avoid duplicate results. (4) i+1 for use-once, i for reuse, used[] for permutations. (5) Prune as early as possible.

Complexity: inherently exponential — subsets O(2ⁿ), permutations O(n·n!), combinations O(C(n,k)·k). Pruning lowers the constant, not the class. Acceptable only for small n.

Practice — Warm-up: Subsets; Combinations; Letter Combinations; Generate Parentheses. Core: Permutations I/II; Subsets II; Combination Sum I/II/III; Word Search; Palindrome Partitioning. Stretch: N-Queens; Sudoku Solver; Restore IP Addresses; Word Search II (Trie); Partition to K Equal Sum Subsets.

Pattern 7 — Binary Search, Heaps & Trees

This family is unified by one idea: exploit structure to avoid looking at everything. Binary search halves a monotonic search space each step (O(log n)). A heap keeps the single best element instantly available so you never sort the whole collection. A balanced BST keeps data ordered for fast range queries.
Binary search has two faces: searching a sorted array for a value, and the powerful “binary search on the answer” — when you can write a yes/no feasible(x) test that’s monotonic, you search the answer space for the boundary. Heaps excel at “top-K” and the two-heaps trick for a running median. Trees cover BST operations and in-order traversal (which yields sorted order).
The craft in binary search is getting boundaries provably right: pick the half-open lo<hi template and reuse it everywhere to avoid off-by-one infinite loops. For heaps, a bounded heap of size K gives O(n log k) (better than a full sort); quickselect beats both at O(n) average. For trees, in-order traversal of a BST is sorted — that unlocks Validate BST and Kth-Smallest.

Binary search on the answer — the mental shift

When feasible(x) is monotonic (all F’s then all T’s), binary-search for the boundary — the first T:

FFFFTTTT

The boundary (first T) is your answer. The array itself need not be sorted — only the yes/no test must flip once.

Sub-types you will meet

  • Classic binary search. Search Insert Position, Find First/Last Position, Search in Rotated Sorted Array.
  • Binary search on answer. Koko Eating Bananas, Capacity to Ship in D Days, Split Array Largest Sum, Median of Two Sorted Arrays.
  • Top-K / K-th with a heap. Kth Largest, Top K Frequent, K Closest Points.
  • Two heaps. Find Median from Data Stream, Sliding Window Median, IPO.
  • BST / tree. Validate BST, Kth Smallest in a BST, Lowest Common Ancestor.

Three skeletons

PYTHON · lower-bound search · top-K heap · two-heap median
# LOWER-BOUND BINARY SEARCH: first index where feasible(x) is True.
# Half-open [lo, hi): lo inclusive, hi exclusive. Loop ends when lo == hi.
def binary_search(lo, hi, feasible):
    while lo < hi:
        mid = lo + (hi - lo) // 2     # avoids integer overflow in Java/C++
        if feasible(mid): hi = mid       # answer is mid or to the LEFT
        else:             lo = mid + 1   # answer is strictly RIGHT
    return lo

import heapq
# TOP-K LARGEST: keep a MIN-heap of size k. The root is the weakest kept; evict it.
def top_k_largest(nums, k):
    heap = []                         # Python heapq is a min-heap
    for x in nums:
        heapq.heappush(heap, x)
        if len(heap) > k: heapq.heappop(heap)  # drop the current minimum
    return heap                       # the k largest (unordered)

# TWO HEAPS running median: lo = max-heap (store NEGATED), hi = min-heap.
class MedianFinder:
    def __init__(self): self.lo, self.hi = [], []
    def add(self, num):
        heapq.heappush(self.lo, -heapq.heappushpop(self.hi, num))  # route through hi
        if len(self.lo) > len(self.hi):
            heapq.heappush(self.hi, -heapq.heappop(self.lo))        # rebalance
    def median(self):
        if len(self.hi) > len(self.lo): return self.hi[0]
        return (self.hi[0] - self.lo[0]) / 2   # lo[0] is negative, so this ADDS the max of lo
!Binary-search / heap traps. (1) Pick one loop template (half-open lo<hi) and never mix. (2) Verify the predicate truly flips once. (3) Python’s heapq is min-only — negate values for a max-heap. (4) To keep the k largest, use a min-heap of size k. (5) Validate BST with value bounds, not just parent comparison.

Complexity: binary search O(log n); on-answer O(n log range); heap push/pop O(log n); top-K O(n log k); quickselect O(n) average; BST ops O(log n).

Practice — Warm-up: Binary Search; Search Insert Position; First Bad Version; Last Stone Weight. Core: Find First/Last Position; Search in Rotated Sorted Array; Koko Eating Bananas; Top K Frequent; K Closest Points; Kth Smallest in a BST; Validate BST. Stretch: Median of Two Sorted Arrays; Split Array Largest Sum; Find Median from Data Stream; Merge k Sorted Lists; Kth Smallest in a Sorted Matrix.

Meta-Strategy: A Unified Decision Process

Knowing seven patterns is necessary but not sufficient — you need a routing procedure that maps an unseen prompt to the right pattern in two minutes. Walk these questions top to bottom; the first “yes” is usually right, and the constraint size breaks ties.

Ask yourself…If yes →
Optimize max/min/count of ways?DP if choices overlap; Greedy if an exchange argument holds
Enumerate all valid arrangements?Backtracking
Contiguous range / pair in a sequence?Sliding Window / Two Pointers
Sorted data or monotonic answer space?Binary Search
Entities + relationships? reach / order / group?BFS / DFS / Union-Find
Splits into independent same-type halves?Divide & Conquer
Need the best element repeatedly / top-K?Heap / Priority Queue

How the families relate

BoundaryThe distinction to articulate
Greedy vs. DPBoth optimize. Greedy commits to a local choice (needs an exchange-argument proof); DP keeps all sub-results because choices interact. If greedy has a counterexample, it’s DP.
DP vs. BacktrackingBoth explore a decision tree. If sub-problems overlap and you need a count/optimum, memoize → DP. If you must list actual arrangements, enumerate → backtracking.
DP vs. Divide & ConquerBoth recurse. D&C sub-problems are independent (merge sort); DP sub-problems overlap (Fibonacci) and must be cached.
BFS vs. DijkstraSame skeleton; BFS for unweighted (queue), Dijkstra for weighted (min-heap). Dijkstra gives wrong answers on negative edges — use Bellman-Ford.
Two Pointers vs. Binary SearchBoth exploit sortedness. Two pointers sweep linearly from the ends; binary search jumps to the middle of a monotonic space.
Heap vs. SortingA full sort is O(n log n); a size-K heap gives top-K in O(n log k) and works on streams you can’t fully sort.

The interview communication protocol

Pattern recognition wins the algorithm; communication wins the offer. Narrate these six beats:

  • 1 · Clarify & restate. Confirm input ranges, edge cases (empty, single, duplicates, negatives), and exact output. Input sizes drive complexity targets.
  • 2 · Brute force first. State the naive solution and its complexity out loud — it shows you understand the problem before optimizing.
  • 3 · Identify the pattern. Name it and justify the trigger.
  • 4 · Design before coding. Define the state / invariant / pointers, the transition, base cases, and target complexity — then get a nod.
  • 5 · Code cleanly, narrate. Write the skeleton, talk through each line, keep names meaningful.
  • 6 · Test & analyze. Dry-run a small example and edge cases, then state final time/space complexity.
!The most common senior-loop failure is not a wrong answer — it’s silence. Even when stuck, narrate your hypotheses and what you’d try next. A working brute force you can explain beats a half-finished optimal solution you can’t.

One-Page Cheat-Sheet

PatternTrigger you feelTechnique / skeletonTypical complexity
Greedy“max/min” + a local choice seems safe; sortableSort by the right key, sweep once keeping one invariant; justify with an exchange argumentO(n log n)
Dynamic Programming“how many ways,” “max/min” over interacting choicesState → transition → base → memoize/tabulate → optimize spaceO(states × transition)
Divide & ConquerSplits into independent halves; “count across,” “sort”Divide, conquer halves, combine in an O(n) merge; Master TheoremO(n log n)
Two Pointers / WindowContiguous subarray/substring; “at most K”; sorted pairExpand right, shrink left on violation; or converge two endsO(n)
GraphsEntities + relationships; reach / order / group / gridBFS (shortest unweighted), DFS (connectivity/topo), Union-Find (grouping)O(V+E)
Backtracking“all” permutations / combinations / subsetschoose → explore → unchoose; prune early; append a copyO(2ⁿ) subsets; O(n·n!) perms
Binary Search / HeapSorted/monotonic answer; “top-K,” “K-th,” medianLower-bound search on a monotonic predicate; size-K heap; two heapsO(log n), O(n log k)

Constraint → complexity (work backward from n): n ≤ 12 → O(n!); n ≤ 20 → O(2ⁿ) bitmask DP; n ≤ 2000 → O(n²); n ≤ 10⁵ → O(n log n); n ≥ 10⁶ → O(n) or O(log n).

Test yourself → 30-question exam

Glossary — every bold term, in plain English

These are the same definitions that pop up when you hover the dotted terms above. Written for someone new to programming.

Prepared for Fernando · target role: Senior Software Engineer · Python reference implementations