LearnThe interviewsstep 6

The coding round: going deeper

  • 90 min
  • 27 stops
  • 14 diagrams

Lesson 6 keeps the selector table, eight solved problems and the drills. This appendix holds the longer explanations and the other solved problems, in case a pattern in the selector is new to you or you want more practice. Problem statements are the author's own; every example is invented. Solutions to the ten transfer problems and to the drill set are in the next appendix lesson.

Pattern 1: two pointers

Context. The baseline checks every pair: n(n-1)/2 sums, which is quadratic. Sortedness gives information the baseline throws away.

Invariant. Every pair not yet ruled out lies inside [lo, hi].

Run the template on the sorted list 1, 3, 4, 6, 8, 11 with target 10. Each of the five steps discards one end for good: once 1 + 11 = 12 is too big, 11 can pair with nothing. The code returns 1-based positions, so the answer is (3, 4); an empty or one-element array returns null.

Code, 13 lines
class SortedPair {
  /** 1-based (i, j) with nums[i-1] + nums[j-1] == target; nums sorted. */
  static int[] find(int[] nums, int target) {
    int lo = 0, hi = nums.length - 1;
    while (lo < hi) {
      int s = nums[lo] + nums[hi];
      if (s == target) return new int[] {lo + 1, hi + 1};
      if (s < target) lo++;
      else hi--;
    }
    return null;
  }
}

Cost. O(n) time because every step moves an index and the two indices travel at most n steps between them; O(1) extra space. Where it breaks. If the array is not sorted, sorting costs O(n log n), and then you may prefer a hash set (O(n), O(n) space). Say that trade-off aloud; it is the kind of decision the interviewer is listening for.

Hack sheet: two pointers

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
CueSorted array or list; "pair", "triplet", "remove duplicates in place", "palindrome", "container".
Templateint lo = 0, hi = n - 1; while lo < hi, compare the sum with the target; too small moves lo up, too big moves hi down.
Fallback that still scoresThe double loop, O(n squared), is correct. For unsorted input a hash set of seen values is O(n) time and also scores.
TrapThe input is not actually sorted: sort first and say the O(n log n) cost, or switch to the hash set. For triplets, skip equal neighbours after sorting.
Follow-up"If it were already sorted, two pointers drop it to O(n)." For "return all pairs", skip equal values after each hit.
Too big, streaming or onlineA stream gets a hash set of seen values (O(n) memory, answers as items arrive). If the data is on disk and too big for memory, external-sort it, then walk two readers inward from both ends.

Worked: 3Sum, every triple that sums to zero

Problem. Return every distinct triple of values from an int array that sums to 0, each triple once. Cue. "Triplet", "three numbers that sum to", "no duplicate triples". Idea. Sort once. Fix the first value a[i] and run the pair template on the rest with target -a[i]. Skip a first value equal to the one before it; after each hit, move both pointers and skip second values equal to the one just used. Equal third values then fall away on their own, because the sum is fixed.

Code, 28 lines
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class ThreeSum {
  /** Every distinct triple that sums to 0, each in ascending order. */
  static List<List<Integer>> threeSum(int[] input) {
    int[] a = input.clone();
    Arrays.sort(a);                              // O(n log n), once
    List<List<Integer>> out = new ArrayList<>();
    for (int i = 0; i + 2 < a.length; i++) {
      if (i > 0 && a[i] == a[i - 1]) continue;   // same first value
      int lo = i + 1, hi = a.length - 1;
      while (lo < hi) {
        long sum = (long) a[i] + a[lo] + a[hi];  // no int overflow
        if (sum < 0) lo++;
        else if (sum > 0) hi--;
        else {
          out.add(List.of(a[i], a[lo], a[hi]));
          lo++;
          hi--;
          while (lo < hi && a[lo] == a[lo - 1]) lo++;  // same second
        }
      }
    }
    return out;
  }
}
INVENTED EXAMPLE 1 min

[2, -1, 0, -1, 1, -2] sorts to [-2, -1, -1, 0, 1, 2]. With -2 first, the pair walk finds (0, 2). With the first -1 it finds (-1, 2), then (0, 1). The second -1 is skipped as a first value, and 0 first finds nothing. The answer is [[-2, 0, 2], [-1, -1, 2], [-1, 0, 1]].

Cost. O(n squared) time: n first values, each with a linear pair walk; the sort adds O(n log n). O(1) extra space beyond the sorted copy and the output. Trap. Duplicates: without the skip after a hit, [-2, 0, 0, 2, 2] returns (-2, 0, 2) twice (a mutant of the listing, run on 2 Nov 2026). A second trap: three ints can overflow, and {MAX_VALUE, MAX_VALUE, 2} sums to 0 in int arithmetic, so the listing adds in a long. Fallback that still scores: the triple loop, O(n cubed), with each triple sorted into a set to drop repeats, said as such. If stuck: "I can find pairs with two pointers on a sorted array; fixing one value turns this into n pair problems." The harness compared the listing with an O(n cubed) brute force on 20,000 random arrays and checked that the input is left unchanged.

Next stop: Pattern 2: sliding window, 2 min

Pattern 2: sliding window

Context. The baseline tries every start and every end and checks each stretch: cubic if you recheck from scratch, quadratic if you are careful. But a stretch that has no repeats keeps having no repeats when you shrink it.

Invariant. The window [left, right] is valid after each step, and left never decreases.

pwwkewright = 1: window pw, best 2pwwkewright = 2: w repeats, left jumps to 2, window wpwwkewright = 4: window wke, best 3pwwkewright = 5: w repeats at 2, left jumps to 3, window kew
Figure A6.1. A sliding window on "pwwkew". The blue frame is the current window; the left edge only ever moves right.

Figure A6.1 follows "pwwkew". The version below stores the last index of every character, so a repeat moves left in one jump instead of one step at a time.

Code, 17 lines
import java.util.HashMap;
import java.util.Map;

class Window {
  static int longest(String s) {
    Map<Character, Integer> last = new HashMap<>(); // char -> last index
    int left = 0, best = 0;
    for (int right = 0; right < s.length(); right++) {
      char ch = s.charAt(right);
      Integer p = last.get(ch);
      if (p != null && p >= left) left = p + 1;
      last.put(ch, right);
      best = Math.max(best, right - left + 1);
    }
    return best;
  }
}

Cost. O(n) time (each index enters and leaves the window once), O(min(n, alphabet size)) space. Where it breaks. A window works when the rule is monotone: shrinking never turns a bad window good. "Subarray with sum exactly k" over numbers that can be negative is not monotone, which is why that problem belongs to the next pattern. The other common window keeps counts and shrinks while it is valid (minimum window substring) or while it is broken (at most k distinct); both are worked in another section of this appendix.

Hack sheet: sliding window

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"Longest", "shortest", "at most k", contiguous substring or subarray, with a rule that only gets worse as the window grows.
Templatefor (int right = 0; right < n; right++): add a[right]; while the window is broken, drop a[left] and move left up; record the best size.
Fallback that still scoresFor every start, extend until the rule breaks. O(n squared), correct.
TrapNegative numbers break a "shrink while the sum is too big" window; use prefix sums with a map. The "last seen" jump must never move left backwards: on "abba", at the final 'a' the map says index 0, but left is already 2, so the guard p >= left keeps it there. Without the guard the code returns 3 instead of 2 (the Java run confirms it). "Exactly k" is "at most k" minus "at most k - 1" for non-negative values.
Follow-up"If it were a stream, I keep only the window and its counts."
Too big, streaming or onlineA stream is the natural case: keep only the window and its counts, so memory is the window size, and the answer updates online. Say what bounds the window if the stream never repeats a character.

Next stop: Pattern 3: prefix sums, 2 min

Pattern 3: prefix sums

Context. Define the prefix sum P[i] as the total of the first i elements, with P[0] = 0. The sum of the elements from position i up to j - 1 is then P[j] - P[i].

Invariant. The map holds every prefix total before the current element, with how many times each occurred.

nums123prefix0136indexP0P1P2P3A sum over nums[i..j-1] is P[j] - P[i].k = 3. P2 - P0 = 3 - 0: subarray [1, 2].P3 - P2 = 6 - 3: subarray [3].The map counts each P[i] seen so far;end j asks: how many earlier P[i]equal P[j] - k?
Figure A6.2. Prefix sums of [1, 2, 3]. The arcs join two prefixes whose difference is k = 3, one arc per qualifying subarray.

Figure A6.2 draws the prefixes of [1, 2, 3] and the two arcs where the difference is 3.

Code, 16 lines
import java.util.HashMap;
import java.util.Map;

class Prefix {
  static int countEqualK(int[] nums, int k) {
    Map<Integer, Integer> seen = new HashMap<>(); // prefix sum -> times
    seen.put(0, 1);
    int total = 0, count = 0;
    for (int x : nums) {
      total += x;
      count += seen.getOrDefault(total - k, 0);
      seen.merge(total, 1, Integer::sum);
    }
    return count;
  }
}
INVENTED EXAMPLE 1 min

Input [1, 2, 3], k = 3. The map starts as {0: 1}. Element 1: total 1; looking for 1 - 3 = -2, absent; map gains 1. Element 2: total 3; looking for 0, present once, count becomes 1 (the subarray [1, 2]); map gains 3. Element 3: total 6; looking for 3, present once, count becomes 2 (the subarray [3]). Answer 2, which matches the two arcs in Figure A6.2.

Cost. O(n) time, O(n) space for the map. A brute force that sums every subarray is O(n squared). The "count subarrays with sum k" problem is not in either public list the author could open; it is included because it is the standard prefix-sum case.

Hack sheet: prefix sums

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"Number of subarrays with sum k", range-sum queries, "equal numbers of 0s and 1s", "product except self", many repeated range queries.
TemplateSeed seen.put(0, 1); for each x: total += x; count += seen.getOrDefault(total - k, 0); then seen.merge(total, 1, Integer::sum). Python: seen.get(total - k, 0).
Fallback that still scoresA running sum from each start, O(n squared). Or build the prefix array once and answer any range as pre[r + 1] - pre[l].
TrapForgetting the seed: on [1, 2, 3] with k = 3 the answer comes out 1 instead of 2 (the Java run confirms it). Updating the map before reading it counts a prefix against itself. Use long for total when sums can pass 2,147,483,647.
Follow-up"Many range queries: build the prefix array once, then each query is O(1)." Say honestly that the map costs O(n) space.
Too big, streaming or onlineThe map grows without bound on an endless stream, so say so and ask whether only a recent window matters (then drop old prefixes) or an approximate answer will do. For many range queries on a fixed array, the prefix array is already enough; with updates mixed in, see pattern 17.

Next stop: Pattern 4: intervals, 2 min

Pattern 4: intervals

Context. If you compare every range with every other you do quadratic work and still have to handle chains (A overlaps B, B overlaps C). Sorting by start removes the chains: after the sort, a range can only overlap the one just before it in the output, because everything earlier ends before the current start or has already been absorbed.

Invariant. The output so far is sorted, non-overlapping, and covers exactly what the input seen so far covers.

024681012141618[1, 3][2, 6][8, 10][15, 18]after sorting by start, merged:[1, 6][8, 10][15, 18]
Figure A6.3. Intervals [1, 3], [2, 6], [8, 10], [15, 18] on a number line (top) and the merged result (solid blue bars).

Figure A6.3 draws the four input ranges on a line and the three merged ranges under them.

Code, 20 lines
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Merge {
  static int[][] merge(int[][] intervals) {
    int[][] a = intervals.clone(); // sort a copy of the row references
    Arrays.sort(a, (x, y) -> Integer.compare(x[0], y[0]));
    List<int[]> out = new ArrayList<>();
    for (int[] iv : a) {
      int[] last = out.isEmpty() ? null : out.get(out.size() - 1);
      if (last != null && iv[0] <= last[1]) {
        last[1] = Math.max(last[1], iv[1]);
      } else {
        out.add(new int[] {iv[0], iv[1]});
      }
    }
    return out.toArray(new int[0][]);
  }
}

Cost. O(n log n) for the sort, O(n) for the output. The merge pass itself is O(n). The output lists are new ones, so the caller's input is left intact. "Minimum number of arrows to burst balloons" uses the same sort-then-sweep idea.

Hack sheet: intervals

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"Meeting rooms", "merge", "insert", "overlap", "minimum arrows or rooms".
TemplateSort by start (Arrays.sort(a, (x, y) -> Integer.compare(x[0], y[0]))); walk once; if iv[0] <= last[1], extend with Math.max, else add a new interval. Python: sorted(intervals).
Fallback that still scoresCompare every pair and join overlapping ones until nothing changes: slow, correct.
TrapForgetting to sort; shrinking an end when one interval sits inside another (use max); touching intervals, so ask whether [1, 2] and [2, 3] overlap; a comparator written as x[0] - y[0] overflows for large values, so use Integer.compare.
Follow-up"Bigger n: the sort dominates, O(n log n); if the list arrives already sorted by start, one pass is O(n)."
Too big, streaming or onlineIntervals arriving online: keep a TreeMap keyed by start so each insert is O(log n). Too many to hold in memory: external-sort by start, then one streaming pass that keeps only the current merged interval.

Next stop: Pattern 5: monotonic stack, 2 min

Pattern 5: monotonic stack

Context. The baseline scans forward from every day until it meets a warmer one: quadratic when temperatures only fall. The waste is that days still waiting for a warmer day are being rescanned by every day that follows them.

Invariant. The stack holds exactly the indices still waiting for an answer, with the coldest on top.

daytemp073174275371469572676773before day 5 (72)day 2 (75)day 3 (71)day 4 (69)after day 5day 2 (75)day 5 (72)72 pops 69, 71ans[4] = 1, ans[3] = 2The stack is alwayscolder going up,so the top is thecoldest waiting day.
Figure A6.4. Monotonic stack on the daily-temperatures input. Reading day 5 (72 degrees) pops the two colder days waiting below it.

Figure A6.4 freezes the moment day 5 (72 degrees) arrives: it pops days 4 and 3, which were colder, and then sits on top of day 2 (75).

Code, 17 lines
import java.util.ArrayDeque;
import java.util.Deque;

class Temps {
  static int[] daily(int[] temps) {
    int[] ans = new int[temps.length];
    Deque<Integer> stack = new ArrayDeque<>(); // indices still waiting
    for (int i = 0; i < temps.length; i++) {
      while (!stack.isEmpty() && temps[stack.peek()] < temps[i]) {
        int j = stack.pop();
        ans[j] = i - j;
      }
      stack.push(i);
    }
    return ans;
  }
}

Cost. O(n) time even though there is a loop inside a loop: each index is pushed once and popped at most once, so the inner loop does at most n pops in total across the whole run. That sentence ("amortised, each element enters and leaves once") is the one to say aloud. O(n) space for the stack in the worst case.

Hack sheet: monotonic stack

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"Next greater or smaller element", "daily temperatures", "largest rectangle", "stock span".
Templatewhile (!stack.isEmpty() && temps[stack.peek()] < x): pop and fill that answer; then push the index.
Fallback that still scoresFor each position scan right until a larger value, O(n squared), correct.
TrapStore indices, not values, when you need distances. Strict versus non-strict comparison decides how equal values behave. The hard version, largest rectangle in a histogram, is worked in another section of this appendix.
Follow-up"Each index is pushed once and popped at most once, so the nested loop is O(n) in total."
Too big, streaming or onlineThe stack is already online: each new value answers the waiting indices it beats as it arrives. Memory is the waiting indices, up to n when values only fall.

Next stop: Pattern 6: BFS and DFS on a grid, a graph or a tree, 3 min

Pattern 6: BFS and DFS on a grid, a graph or a tree

Context. A grid is a graph in disguise: each cell is a node, and its four neighbours are its edges. Both searches visit every reachable node once.

Invariant. Every node in the queue or already popped is marked seen; every node still unmarked has not been reached.

The three depth-first orders differ only in when the node is handled: preorder handles the node first (copying a tree), inorder between the subtrees (sorted order on a search tree), postorder last (a height or a sum from the children's answers).

SABCDElevel 0level 1level 2queue as it runs: [S] -> [A, B] -> [B, C, D] -> [C, D, E] -> [D, E] -> [E] -> []A node is marked seen when it is put in the queue, so D (reached from A and B) enters once.
Figure A6.5. Breadth-first search by levels. All nodes at distance d are processed before any at distance d + 1, which is why BFS finds the fewest steps.

Figure A6.5 shows breadth-first search by levels, with the queue written beneath. All nodes at distance d are taken before any at distance d + 1, so the first time BFS reaches a node is by a shortest path in steps; on a grid, "level" is "number of moves". The code below counts islands: each unseen land cell starts a new island, and a BFS from it marks the whole island.

Code, 32 lines
import java.util.ArrayDeque;
import java.util.Deque;

class Islands {
  static int count(char[][] grid) {
    if (grid.length == 0) return 0;
    int rows = grid.length, cols = grid[0].length, count = 0;
    boolean[][] seen = new boolean[rows][cols];
    int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    for (int r = 0; r < rows; r++) {
      for (int c = 0; c < cols; c++) {
        if (grid[r][c] != '1' || seen[r][c]) continue;
        count++;
        seen[r][c] = true;
        Deque<int[]> q = new ArrayDeque<>();
        q.add(new int[] {r, c});
        while (!q.isEmpty()) {
          int[] cur = q.poll();
          for (int[] d : dirs) {
            int ny = cur[0] + d[0], nx = cur[1] + d[1];
            if (ny >= 0 && ny < rows && nx >= 0 && nx < cols
                && grid[ny][nx] == '1' && !seen[ny][nx]) {
              seen[ny][nx] = true;
              q.add(new int[] {ny, nx});
            }
          }
        }
      }
    }
    return count;
  }
}

Cost. O(R x C) time and space for R rows and C columns: every cell is looked at a bounded number of times, and the seen table and queue are at most that size. The usual alternative is recursive DFS with the same count; its drawback is the call stack on a large grid (see the trap below), so the iterative form is safer. The same toolkit covers "maximum depth of a binary tree", "lowest common ancestor" and "right side view" (the last node of each BFS level); the public lists place those under this pattern.

Hack sheet: BFS and DFS

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"Fewest steps or moves" (BFS); "number of islands", "connected regions", "can I reach" (DFS or BFS); a grid; "word ladder".
TemplateDeque<int[]> q = new ArrayDeque<>(); q.add(start); seen[start] = true; poll; for each unseen neighbour mark it seen and add it.
Fallback that still scoresA DFS that finds any path scores for "does a path exist". For "shortest" it is not enough: say aloud that you only have a DFS and what BFS would change.
TrapMark a node seen when you push it, not when you pop it, or the same node is queued many times. ArrayDeque rejects null (the Java run throws NullPointerException), so queue indices or small arrays. Recursion depth: see below.
Follow-up"Bidirectional BFS halves the depth" (practitioner claim). For a huge grid, switch the recursive DFS to an explicit stack.
Too big, streaming or onlineA graph too big to store (a web crawl, a game state space): generate neighbours on the fly, bound the frontier, and say a Bloom filter can stand in for the seen-set at the price of dropping some nodes. On a huge grid use the explicit queue and a bit set.

The stack-depth trap. A recursive DFS goes one call deeper per cell on a snake-shaped region. The author ran a recursive DFS over an all-land 400 by 400 grid (160,000 cells) and it threw StackOverflowError on OpenJDK 25 with the default thread stack; the limit is a JVM setting, so it differs by platform and by frame size. In an online assessment, or on a skewed tree, use an explicit ArrayDeque as a stack (or the queue above) and a loop. The island count above already does that. Python: the default recursion limit is about 1,000 frames (3.9.6).

Next stop: Pattern 7: topological sort, 5 min

Pattern 7: topological sort

Context. Think of the pairs as arrows in a directed graph. You can finish everything exactly when the graph has no cycle, because a cycle is a set of tasks each waiting on another.

Invariant. Everything popped had all its prerequisites popped before it.

0123in-degree 0in-degree 1in-degree 1in-degree 2Kahn: queue [0]. Pop 0: counts of 1 and 2 drop to 0, queue [1, 2]. Pop 1: 3 drops to 1.Pop 2: 3 drops to 0, queue [3]. Pop 3. Four popped out of four: no cycle.Add an edge 3 -> 0 and node 0 never reaches in-degree 0: nothing is popped, cycle found.
Figure A6.6. A dependency graph (an arrow means "before") with in-degrees. Kahn's algorithm removes nodes with in-degree zero, layer by layer.

Figure A6.6 draws four tasks with in-degrees 0, 1, 1 and 2, and the Kahn run beneath. The code returns the order the nodes were popped in, or null when a cycle blocks some of them.

Code, 27 lines
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

class Courses {
  /** Each pair is {course, prerequisite}. Returns an order, or null. */
  static int[] order(int n, int[][] prereqs) {
    int[] indeg = new int[n];
    List<List<Integer>> adj = new ArrayList<>();
    for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
    for (int[] p : prereqs) {
      adj.get(p[1]).add(p[0]);
      indeg[p[0]]++;
    }
    Deque<Integer> q = new ArrayDeque<>();
    for (int i = 0; i < n; i++) if (indeg[i] == 0) q.add(i);
    int[] order = new int[n];
    int done = 0;
    while (!q.isEmpty()) {
      int v = q.poll();
      order[done++] = v;
      for (int w : adj.get(v)) if (--indeg[w] == 0) q.add(w);
    }
    return done == n ? order : null; // null means a cycle
  }
}

Cost. O(V + E) time and space, where V is the number of nodes and E the number of arrows: each node is queued once and each arrow is looked at once.

Hack sheet: topological sort

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"Prerequisites", "order of tasks", "can all courses be finished" (a cycle check), "build order".
TemplateCount in-degrees; queue the zeros; poll, append to order, lower each neighbour's count and queue new zeros; compare done with n.
Fallback that still scoresA DFS with three colours (unvisited, in progress, done) detects a cycle, and post-order gives an order.
TrapEdge direction: a pair [a, b] often means b comes first, so the arrow is b to a. done != n means a cycle.
Follow-up"Which tasks are stuck?" The nodes never popped are in a cycle or wait on one.
Too big, streaming or onlineEdges that stream in: keep in-degrees up to date and re-run only if a new edge could close a cycle. Say that online cycle detection on a directed graph is harder than the batch pass; do not bluff it.

Worked: the alien dictionary, an order from sorted words

Problem. Words in an unknown alphabet arrive in that alphabet's sorted order. Return one order of the letters that agrees with the list, or "" if none can. Cue. "Sorted in an unknown order", "derive the alphabet". Idea. Two adjacent words give at most one rule: at their first differing position, the upper word's letter comes first. Each rule is an arrow, and a topological order of the letters that appear is an answer. Two cases have none. A word followed by its own proper prefix ("abc" before "ab") breaks every order. A cycle among the rules leaves letters never popped: the template's done != n test.

Code, 43 lines
import java.util.ArrayDeque;
import java.util.Deque;

class Alien {
  /** A letter order under which the words are sorted, or "" if none.
   *  Words use 'a' to 'z'; every letter that appears is in the answer. */
  static String order(String[] words) {
    boolean[] used = new boolean[26];
    boolean[][] before = new boolean[26][26];    // before[u][v]: u < v
    int[] indeg = new int[26];
    for (String w : words)
      for (char c : w.toCharArray()) used[c - 'a'] = true;
    for (int i = 0; i + 1 < words.length; i++) {
      String a = words[i], b = words[i + 1];
      int k = 0, m = Math.min(a.length(), b.length());
      while (k < m && a.charAt(k) == b.charAt(k)) k++;
      if (k == m) {
        if (a.length() > b.length()) return "";  // "abc" before "ab"
        continue;                                // a prefix: no rule
      }
      int u = a.charAt(k) - 'a', v = b.charAt(k) - 'a';
      if (!before[u][v]) {                       // count an edge once
        before[u][v] = true;
        indeg[v]++;
      }
    }
    Deque<Integer> ready = new ArrayDeque<>();
    int letters = 0;
    for (int c = 0; c < 26; c++) {
      if (!used[c]) continue;
      letters++;
      if (indeg[c] == 0) ready.add(c);
    }
    StringBuilder out = new StringBuilder();
    while (!ready.isEmpty()) {
      int u = ready.poll();
      out.append((char) ('a' + u));
      for (int v = 0; v < 26; v++)
        if (before[u][v] && --indeg[v] == 0) ready.add(v);
    }
    return out.length() == letters ? out.toString() : "";  // a cycle
  }
}
INVENTED EXAMPLE 1 min

["ca", "cb", "ba"]: "ca" and "cb" first differ at a and b, so a before b; "cb" and "ba" differ at c and b, so c before b. The queue starts with a and c, and b comes last: "acb". ["ab", "ba", "aa"] gives a before b and then b before a, a cycle, so "". ["abc", "ab"] is "" at once; ["ab", "abc"] gives no rule and returns "abc".

Cost. O(C) to compare adjacent words, for C characters in all, plus O(26 x 26) for the arrow table, which is also the space. Trap. Only the first difference is a rule; later letters say nothing. Without the prefix check, ["abc", "ab"] returns "abc". With a table of arrows, count each arrow once: on ["ca", "cb", "da", "db"] the rule a before b appears twice, and counting it twice leaves b's in-degree stuck at 1, so the answer comes back "" instead of "acbd" (both mutants run on 2 Nov 2026). Letters in no rule still belong in the answer. Fallback that still scores: the three-colour DFS from the hack sheet, with the post-order reversed. If stuck: "Two adjacent sorted words tell me one thing, the first letter where they differ. That is an arrow, so this is a topological sort." The harness checked 5,000 random word lists, half sorted under a random hidden order, against every permutation of their letters: "" exactly when no order exists, and otherwise an order that sorts the list.

Next stop: Pattern 8: union-find (disjoint set), 4 min

Pattern 8: union-find (disjoint set)

Context. You could do a BFS from each unseen city, as in pattern 6. Union-find is the better tool when edges arrive one at a time, or when the question is "are these two already in the same group".

Invariant. Two cities are in the same group exactly when find gives them the same root; the group count equals the number of roots.

Union by size keeps trees flat, so three cities never form a chain: the smaller tree always hangs under the larger root. Four cities do give a two-step path. Figure A6.7 runs union(0, 1), union(2, 3), then union(0, 2): roots 1 and 3 both hold two cities, so the code keeps rb = 3 and sets parent[1] = 3, leaving 0 two steps from the root, parent = [1, 3, 3, 3]. One find(0) with path halving sets parent[0] = parent[1] = 3, giving [3, 3, 3, 3].

after union(0,1), union(2,3), union(0,2)after find(0) with path halving312030120 is two steps from root 30 now points straight at 3
Figure A6.7. A union-find group as a tree, built by the printed code with union by size. Arrows point to the parent; find(0) with path halving moves 0 up to the root, so the next find from 0 takes one step.
Code, 35 lines
class DisjointSet {
  private final int[] parent, size;
  DisjointSet(int n) {
    parent = new int[n];
    size = new int[n];
    for (int i = 0; i < n; i++) { parent[i] = i; size[i] = 1; }
  }
  int find(int x) {
    while (parent[x] != x) {
      parent[x] = parent[parent[x]]; // path halving
      x = parent[x];
    }
    return x;
  }
  /** Returns false if a and b were already in one group. */
  boolean union(int a, int b) {
    int ra = find(a), rb = find(b);
    if (ra == rb) return false;
    if (size[ra] > size[rb]) { int t = ra; ra = rb; rb = t; }
    parent[ra] = rb;           // smaller tree hangs under the larger
    size[rb] += size[ra];
    return true;
  }
}

class Provinces {
  static int count(int[][] isConnected) {
    int n = isConnected.length, comps = n;
    DisjointSet ds = new DisjointSet(n);
    for (int i = 0; i < n; i++)
      for (int j = i + 1; j < n; j++)
        if (isConnected[i][j] == 1 && ds.union(i, j)) comps--;
    return comps;
  }
}
INVENTED EXAMPLE 1 min

Three cities, matrix [[1, 1, 0], [1, 1, 1], [0, 1, 1]]: city 0 links to 1, and city 1 links to 2. Start with three groups. The loops visit the pairs in the order (0, 1), (0, 2), (1, 2). The pair (0, 1): roots 0 and 1 differ, so parent[0] = 1, two groups. The pair (0, 2) is not marked in the matrix and is skipped. The pair (1, 2): roots 1 and 2 differ; the group under 1 holds two cities and city 2 holds one, so the smaller hangs under the larger: parent[2] = 1, one group, root 1. The run prints parent = [1, 1, 1], not a chain. Answer 1. With the identity matrix of size 2 no pair is linked and the answer stays 2.

Cost. With an edge list, O(E x alpha(n)) time, where alpha is the inverse Ackermann function, below 5 for any n you will ever meet, so treat it as constant. With the full matrix as here, scanning all pairs is itself O(n squared), so the total is O(n squared x alpha(n)); say that rather than "linear". O(n) space for parent.

Hack sheet: union-find

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"Connected components", "are X and Y connected", edges arriving one by one, "redundant connection", "accounts merge".
Templateparent[i] = i for every i; find with path halving; union links the roots and returns false if they were already equal.
Fallback that still scoresLabel components once with a BFS or DFS: fine when no updates are mixed in with the queries.
TrapA union that returns false is the cycle signal. Compare roots from find, never raw parents. Union by size plus path halving gives the near-constant bound; path halving alone is O(log n) amortised, which is also enough in an interview (practitioner claim).
Follow-up"Edges arrive one at a time: each is one find and one union, almost constant time; the first union that returns false closes a cycle."
Too big, streaming or onlineUnion-find is already the online structure: edges stream in, memory is O(n). If the ids are too sparse for an array, key a HashMap by id instead.

Next stop: Pattern 9: binary search on the answer, 2 min

Pattern 9: binary search on the answer

Context. The answer is a number in a known range (a speed from 1 up to the largest pile). Checking one candidate is easy: at speed s a pile of p takes ceil(p / s) hours, so add them up.

Invariant. The true answer is always within [lo, hi].

mid 6lo 1, hi 11: 6 hours -> fits in 8 -> hi = 6mid 3lo 1, hi 6: 10 hours -> too slow -> lo = 4mid 5lo 4, hi 6: 8 hours -> fits -> hi = 5mid 4lo 4, hi 5: 8 hours -> fits -> hi = 4lo = hi = 4: the smallest speed that finishes four piles [3, 6, 7, 11] in 8 hours.
Figure A6.8. Binary search on the answer. The blue band is the range still possible; every check halves it, using the fact that a faster speed never takes longer.

Figure A6.8 runs it for piles 3, 6, 7, 11 and 8 hours: four checks narrow the range from 1 to 11 down to a single speed, 4.

Code, 23 lines
import java.util.function.IntPredicate;

class Speed {
  /** Smallest value in [lo, hi] with ok(value) true; ok is monotone. */
  static int minFeasible(int lo, int hi, IntPredicate ok) {
    while (lo < hi) {
      int mid = lo + (hi - lo) / 2;
      if (ok.test(mid)) hi = mid;
      else lo = mid + 1;
    }
    return lo;
  }

  static int minSpeed(int[] piles, int h) {
    int max = 0;
    for (int p : piles) max = Math.max(max, p);
    return minFeasible(1, max, s -> {
      long hours = 0;
      for (int p : piles) hours += (p + s - 1) / s; // ceiling division
      return hours <= h;
    });
  }
}

Cost. Each check is O(n); there are about log2(M) checks for the largest pile M, so O(n log M) time and O(1) space. A linear scan of every speed would be O(n x M). "Find peak element" also uses a binary search, steered by comparing a value with its neighbour.

Hack sheet: binary search on the answer

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"Minimum possible maximum", "smallest X such that it is possible", "ship within D days", "eating speed"; feasibility only goes one way.
TemplateThe minFeasible skeleton in the code above: lo, hi, a feasibility test ok(mid), hi = mid on success else lo = mid + 1.
Fallback that still scoresA linear scan over candidate answers calling ok; then say "feasibility is monotone, so this is a binary search" and write it.
TrapWrong bounds (for "split array", lo is the largest element and hi the total); an ok that is not monotone; lo = mid with floor division never terminates; (lo + hi) / 2 overflows an int once the sum passes 2,147,483,647, so write lo + (hi - lo) / 2. Arrays.binarySearch covers plain searches in a sorted array. Python: bisect.bisect_left.
Follow-up"A maximum instead of a minimum flips the two branches. If I cannot argue monotonicity in one sentence, it is not this pattern."
Too big, streaming or onlineA huge range (up to 10^18) is fine: use long and the same loop; the cost is the test, so ask how slow ok is. On a stream there is nothing to test against, so store the data or take a second pass.

Next stop: Pattern 10: heaps and k-way merge, 4 min

Pattern 10: heaps and k-way merge

Context. Sorting costs O(n log n) and gives you far more than you asked for. A heap is a tree stored in an array in which every parent is no larger (a min-heap) than its children, so the smallest item is always at the root, and inserting or removing costs O(log size).

Invariant. The heap holds the k largest items seen so far.

k = 2, stream 3, 2, 1, 5, 6, 4: the heap after each itemitem 33item 223item 1231 pushed then poppeditem 5352 poppeditem 6563 poppeditem 4564 pushed then poppedfinal heap56root 5 = 2nd largestparent <= children
Figure A6.9. A min-heap of size k = 2. The framed cell is the root, the smallest kept value; the heap never holds more than two numbers.

Figure A6.9 shows the heap after each item of the stream 3, 2, 1, 5, 6, 4 with k = 2. Java's PriorityQueue is a min-heap by default.

Code, 12 lines
import java.util.PriorityQueue;

class KthLargest {
  static int find(int[] nums, int k) {
    PriorityQueue<Integer> heap = new PriorityQueue<>(); // k largest so far
    for (int x : nums) {
      heap.add(x);
      if (heap.size() > k) heap.poll();
    }
    return heap.peek();
  }
}

Cost. O(n log k) time and O(k) space, against O(n log n) for a full sort. When k is close to n there is no gain; when n is huge and k is 10 the heap is a clear win.

Hack sheet: heaps and k-way merge

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"Top k", "k-th largest", "median of a stream", "merge k sorted lists", "schedule by earliest or cheapest".
TemplateA PriorityQueue; add, and poll when the size exceeds k; for a merge, add each list's head as {value, list, position}. Python: heapq.merge(*lists).
Fallback that still scoresConcatenate and sort, O(N log N). Say "a heap gives O(N log k)" and then write it if time allows. Python: heapq.nlargest(k, nums)[-1].
Trapnew PriorityQueue<>() is a min-heap; for a max-heap pass Collections.reverseOrder(). A PriorityQueue<int[]> without a comparator throws ClassCastException (the Java run shows it), and a subtraction comparator a - b overflows (Integer.MIN_VALUE - 1 wraps to MAX_VALUE), so use Integer.compare.
Follow-up"For a stream, the heap answers at any moment. If k is close to n, sorting is as good."
Too big, streaming or onlineTop k of a stream you cannot store is exactly what the size-k heap does, in O(k) memory. For a median, two heaps (MedianFinder, below). Too much for one machine: keep a heap per shard and merge the shard heaps.
Code, 23 lines
import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;

class MergeK {
  /** Merge sorted lists: heap entries are {value, list, position}. */
  static List<Integer> merge(List<List<Integer>> lists) {
    PriorityQueue<int[]> heap =
        new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
    for (int i = 0; i < lists.size(); i++)
      if (!lists.get(i).isEmpty())
        heap.add(new int[] {lists.get(i).get(0), i, 0});
    List<Integer> out = new ArrayList<>();
    while (!heap.isEmpty()) {
      int[] top = heap.poll();
      out.add(top[0]);
      List<Integer> src = lists.get(top[1]);
      if (top[2] + 1 < src.size())
        heap.add(new int[] {src.get(top[2] + 1), top[1], top[2] + 1});
    }
    return out;
  }
}

On [[1, 4, 7], [2, 5], [3, 6]] it returns [1, 2, 3, 4, 5, 6, 7], and the Java run compares it with sorting the concatenation on seeded random inputs. Cost O(N log k) for N items in k lists.

Median of a stream: two heaps. A heap hands over only its extreme, so the median needs two: a max-heap low for the smaller half and a min-heap high for the larger half, kept within one item in size. The median is then the top of low (odd count) or the mean of the two tops (even count). Each add sends the new item through low to high and moves one back if high grew larger; that routing keeps every item of low at most every item of high.

Code, 19 lines
import java.util.Collections;
import java.util.PriorityQueue;

class MedianFinder {
  private final PriorityQueue<Integer> low =
      new PriorityQueue<>(Collections.reverseOrder()); // smaller half
  private final PriorityQueue<Integer> high = new PriorityQueue<>();

  void add(int x) {
    low.add(x);
    high.add(low.poll());           // the top of the low half moves up
    if (high.size() > low.size()) low.add(high.poll());
  }

  double median() {
    return low.size() > high.size() ? low.peek()
        : (low.peek() + high.peek()) / 2.0;
  }
}

Stream 5, 2, 9: after 5, low holds [5] and the median is 5; 2 goes to low, 5 moves up, so low [2] and high [5] give 3.5; 9 goes to low, then to high, which now has two items to one, so 5 moves back and the median is 5 again. Cost O(log n) per add, O(1) per median, O(n) space. A sliding-window median needs deletions too; the usual route is lazy deletion in the same two heaps (named, not coded).

Next stop: Pattern 11: backtracking, 2 min

Pattern 11: backtracking

Context. When the answer is "all the ways to", there is no shortcut around producing them, but there is a disciplined way to do it. Build one candidate step by step.

Invariant. On entry to go() the path and the used marks describe the same partial candidate; on exit they are restored.

[]123same shape under 2same shape under 32332[1, 2, 3] emit[1, 3, 2] emitpath = [1]pick 2 (used[1] = True), recurse,then pop 2, used[1] = False,then try 3. Same list, no copies.
Figure A6.10. The search tree of permutations. Each level fixes one position; the leaf is emitted, then the walk returns up and tries the next sibling.

Figure A6.10 draws the tree for [1, 2, 3], with the branch under 1 expanded. The two leaves are the permutations [1, 2, 3] and [1, 3, 2]; the other two branches have the same shape.

Code, 26 lines
import java.util.ArrayList;
import java.util.List;

class Perms {
  static List<List<Integer>> all(int[] nums) {
    List<List<Integer>> out = new ArrayList<>();
    go(nums, new ArrayList<>(), new boolean[nums.length], out);
    return out;
  }

  private static void go(int[] nums, List<Integer> path, boolean[] used,
                         List<List<Integer>> out) {
    if (path.size() == nums.length) {
      out.add(new ArrayList<>(path)); // a copy, not the live path
      return;
    }
    for (int i = 0; i < nums.length; i++) {
      if (used[i]) continue;
      used[i] = true;
      path.add(nums[i]);
      go(nums, path, used, out);
      path.remove(path.size() - 1);
      used[i] = false;
    }
  }
}

Cost. n! results each of length n, so O(n x n!) time. For n = 3 that is 6 results and 18 numbers written; for n = 10 it is 3,628,800 results. That growth is why you ask about the input size before choosing this pattern. Space is O(n) for the recursion depth, plus the output.

Hack sheet: backtracking

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"All subsets, permutations, combinations", "N queens", "generate parentheses", n about 20 or less.
Templatego(i): at the end, save new ArrayList<>(path); otherwise try skipping and taking item i, undoing the take afterwards.
Fallback that still scoresFor subsets, build iteratively: start with [[]] and for each item add it to a copy of every existing subset. Correct, and it avoids recursion.
TrapSaving path instead of a copy (every saved answer ends up as the same emptied list, [[]] in the Java run); forgetting to undo; duplicate inputs need a sort and a skip-equal-sibling rule.
Follow-up"Prune early: abandon a branch the moment it cannot be valid." Say what n you assumed, since the count grows as 2^n or n!.
Too big, streaming or onlineThe answers cannot all be listed when n is large: ask for a count (DP or a formula) instead, or yield them one at a time through an iterator.
Code, 22 lines
import java.util.ArrayList;
import java.util.List;

class Subsets {
  static List<List<Integer>> all(int[] a) {
    List<List<Integer>> out = new ArrayList<>();
    pick(a, 0, new ArrayList<>(), out);
    return out;
  }

  private static void pick(int[] a, int i, List<Integer> path,
                           List<List<Integer>> out) {
    if (i == a.length) {
      out.add(new ArrayList<>(path));
      return;
    }
    pick(a, i + 1, path, out);           // skip a[i]
    path.add(a[i]);                      // take a[i]
    pick(a, i + 1, path, out);
    path.remove(path.size() - 1);        // undo
  }
}

The Java run checks that it returns 2^n distinct subsets for every n from 0 to 6.

Next stop: Pattern 12: dynamic programming, one-dimensional and two-dimensional, 5 min

Pattern 12: dynamic programming, one-dimensional and two-dimensional

Context. A dynamic-programming problem has two properties. The best answer for a big input is made from best answers for smaller inputs, and the same smaller inputs come up again and again in a naive recursion.

One-dimensional.

Invariant. When dp[x] is computed, every dp it reads is final.

Code, 14 lines
import java.util.Arrays;

class Coins {
  static int change(int[] coins, int amount) {
    int inf = amount + 1;               // larger than any real answer
    int[] dp = new int[amount + 1];
    Arrays.fill(dp, inf);
    dp[0] = 0;
    for (int a = 1; a <= amount; a++)
      for (int c : coins)
        if (c <= a && dp[a - c] + 1 < dp[a]) dp[a] = dp[a - c] + 1;
    return dp[amount] == inf ? -1 : dp[amount];
  }
}
INVENTED EXAMPLE 1 min

Coins [1, 3, 4], amount 6. Define: dp[a] is the fewest coins to make amount a, and amount 0 needs none. Filling a from 0 to 6 gives [0, 1, 2, 1, 1, 2, 2]. For a = 6: coin 1 gives dp[5] + 1 = 3, coin 3 gives dp[3] + 1 = 2, coin 4 gives dp[2] + 1 = 3, so 2 (3 + 3). Taking the biggest coin first would give 4 + 1 + 1, three coins, which is why this is not a greedy problem.

One-dimensional cost: O(amount x number of coins) time, O(amount) space. Coin change is a standard problem in the public study plans; "house robber" has the same shape with a different choice at each step.

Two-dimensional.

Invariant. Row i is filled from row i - 1 and from earlier cells of row i only.

For the strings "abcde" and "ace" the table has six rows and four columns (the extra row and column are the empty prefix), 24 cells in all, and three cells where a letter matches; the drawn table is in another section of this appendix.

Code, 46 lines
import java.util.Arrays;

class Lcs {
  static int table(String a, String b) {
    int m = a.length(), n = b.length();
    int[][] dp = new int[m + 1][n + 1];
    for (int i = 1; i <= m; i++)
      for (int j = 1; j <= n; j++)
        dp[i][j] = a.charAt(i - 1) == b.charAt(j - 1)
            ? dp[i - 1][j - 1] + 1
            : Math.max(dp[i - 1][j], dp[i][j - 1]);
    return dp[m][n];
  }

  /** The same table rolled into one row of b.length() + 1 cells. */
  static int rolled(String a, String b) {
    int[] row = new int[b.length() + 1];   // row[j] is dp[i][j]
    for (int i = 1; i <= a.length(); i++) {
      int diag = 0;                        // dp[i - 1][j - 1]
      for (int j = 1; j <= b.length(); j++) {
        int up = row[j];                   // dp[i - 1][j], about to go
        row[j] = a.charAt(i - 1) == b.charAt(j - 1)
            ? diag + 1
            : Math.max(up, row[j - 1]);    // row[j - 1] is dp[i][j - 1]
        diag = up;
      }
    }
    return row[b.length()];
  }

  /** The same answer top-down: plain recursion plus a memo table. */
  static int memo(String a, String b) {
    int[][] memo = new int[a.length() + 1][b.length() + 1];
    for (int[] row : memo) Arrays.fill(row, -1);
    return best(a, b, 0, 0, memo);
  }

  private static int best(String a, String b, int i, int j, int[][] memo) {
    if (i == a.length() || j == b.length()) return 0;
    if (memo[i][j] >= 0) return memo[i][j];
    int r = a.charAt(i) == b.charAt(j)
        ? 1 + best(a, b, i + 1, j + 1, memo)
        : Math.max(best(a, b, i + 1, j, memo), best(a, b, i, j + 1, memo));
    return memo[i][j] = r;
  }
}

Two-dimensional cost: O(m x n) time and space for strings of lengths m and n (here 5 x 3 = 15 cells of work); rolled keeps one row of the table plus the old diagonal cell in a variable, so passing the shorter string as b cuts the space to O(min(m, n)) with the same O(m x n) time. The trap in rolling is overwriting dp[i - 1][j - 1] before it is read, which is why diag is saved from up after each cell. The harness ran all three methods against each other on 5,000 random pairs of strings over a, b and c up to eight letters, and checked abcde and ace (3) and the empty string (0), on 2 Nov 2026.

Hack sheet: dynamic programming

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"Number of ways", "minimum cost", "can you reach", "longest ... subsequence"; a choice at each position and overlapping subproblems.
TemplateWrite the plain recursion, name the state in one sentence, add a memo table (an int[][] filled with -1, or a HashMap); if time allows offer the bottom-up table. Python: @functools.cache.
Fallback that still scoresThe plain recursion with the state named: correct though exponential, and often earns much of the credit if time runs out (practitioner claim).
TrapRecursion depth about n risks StackOverflowError (see the stack-depth trap above), so go bottom-up when n is large; a memo key must be immutable; use long when counts grow.
Follow-up"I can roll the table to O(min(m, n)) space." Then show it: Lcs.rolled above keeps one row and the old diagonal, and gives the same answer as table.
Too big, streaming or onlineTable too big: roll to one or two rows (O(min(m, n)) space), then say that reconstruction needs the full table, or a divide-and-conquer such as Hirschberg's (named, not coded).

The memoised-recursion shortcut. The Lcs class above also carries the top-down form: the plain recursion over (i, j) plus an int[][] memo filled with -1, so the recursion becomes the table without writing the table; it agrees with the table version in the Java run. Python: @functools.cache does the same in one line.

More families. The longest increasing subsequence, the 0/1 knapsack, edit distance and buy-sell-with-cooldown, each with the answer rebuilt and checked against a brute force, are worked in another section of this appendix. The hack sheet above covers all four: name the state in one sentence, write the recursion, then the table, then rebuild the items by walking the table back.

Next stop: Pattern 13: tries (prefix trees), 2 min

Pattern 13: tries (prefix trees)

Context. A list of words answers a prefix question by scanning every word. A trie spends memory to make each prefix a place: one node per distinct prefix, each node holding its children keyed by the next letter.

Invariant. The path from the root to a node spells exactly one prefix; the marker is on exactly the nodes that spell stored words.

applebatroot (no letter)Words stored: apple, app, batBlue ring = a word ends here (the "$" marker).search("app") walks a, p, p and finds the ring: True.startsWith("ap") only needs the path: True.search("ap") finds no ring: False.
Figure A6.11. A trie for "apple", "app" and "bat". Shared prefixes share nodes; the rings mark where a stored word ends.

Figure A6.11 stores "apple", "app" and "bat". The nodes for "app" and "apple" are on one path, so the ring on the second p says "app" is a stored word while the continuing path says it is also a prefix.

Code, 31 lines
class Trie {
  private final Trie[] next = new Trie[26];
  private boolean isWord;

  void insert(String word) {
    Trie node = this;
    for (char ch : word.toCharArray()) {
      if (node.next[ch - 'a'] == null) node.next[ch - 'a'] = new Trie();
      node = node.next[ch - 'a'];
    }
    node.isWord = true;                  // end-of-word marker
  }

  private Trie walk(String s) {
    Trie node = this;
    for (char ch : s.toCharArray()) {
      node = node.next[ch - 'a'];
      if (node == null) return null;
    }
    return node;
  }

  boolean search(String word) {
    Trie node = walk(word);
    return node != null && node.isWord;
  }

  boolean startsWith(String prefix) {
    return walk(prefix) != null;
  }
}

Cost. O(L) per insert, search or prefix query for a word of length L, independent of how many words are stored; O(total characters) space in the worst case, less when prefixes are shared.

Hack sheet: tries

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"Prefix", "autocomplete", "word search II", "longest common prefix", a dictionary of words with many prefix queries.
TemplateA node with a Trie[26] array of children (or a HashMap<Character, Trie> for a large alphabet), an end-of-word flag; walk the letters for both search and prefix queries.
Fallback that still scoresA set of words plus a startsWith scan, O(words) per query; or sort the words and binary-search the prefix. Say the trade-off.
TrapConfusing "is a prefix" with "is a word" (the end marker). Fixed arrays of 26 children cost memory per node and throw ArrayIndexOutOfBoundsException on a character outside a to z, so ask about the alphabet; a HashMap per node trades speed for space. Ask whether the dictionary is a list of strings or a stream, because that changes the memory plan.
Follow-up"To return every word with a prefix, walk to the prefix node and then search below it."
Too big, streaming or onlineMillions of words: shared prefixes already save space; compress single-child chains (a radix tree) or keep the dictionary on disk. Inserts can stream in, each O(L).

Next stop: Pattern 14: bit tricks, 2 min

Pattern 14: bit tricks

Context. The hash-map baseline counts every number: O(n) time and O(n) space. Bits give a shortcut.

Invariant. After each step x is the XOR of everything read so far.

Code, 11 lines
class Bits {
  static int single(int[] nums) {
    int x = 0;
    for (int n : nums) x ^= n;           // equal pairs cancel
    return x;
  }

  static boolean isPowerOfTwo(int n) {
    return n > 0 && (n & (n - 1)) == 0;  // parentheses: == binds tighter
  }
}
INVENTED EXAMPLE 1 min

Input [4, 1, 2, 1, 2]: x goes 0, 4, 5, 7, 6, 4. Answer 4, in O(1) space. Edge cases: a one-element list returns that element; a value that appears three times breaks the method, so ask.

Cost. O(n) time, O(1) space.

Hack sheet: bit tricks

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"Single number", "without extra space", "power of two", "count set bits", a subset search with n about 20 (a bitmask).
TemplateFold with XOR: r ^= x. Power of two: n > 0 && (n & (n - 1)) == 0. Set bits: Integer.bitCount(n). Python: bin(n).count("1").
Fallback that still scoresA HashMap of counts or a set, O(n) space. Say "XOR cancels pairs, so O(1) space" and then write the fold. Python: Counter(nums).
TrapIn Java == binds tighter than &, so n & (n - 1) == 0 does not compile (an int and a boolean); write the parentheses. An int is 32 bits and wraps on overflow, and a shift count is taken modulo 32, so shifting by 32 does nothing.
Follow-up"This needs every other value to appear exactly twice. If a value may appear three times, I would ask, because the method changes."
Too big, streaming or onlineThe XOR fold reads each value once with O(1) memory, so it already works on a stream or a file read once. Values spread over machines: XOR each machine's part, then XOR the results.

Next stop: Pattern 15: shortest paths with weights (Dijkstra, 0-1 BFS, k stops), 5 min

Pattern 15: shortest paths with weights (Dijkstra, 0-1 BFS, k stops)

Context. Breadth-first search finds the fewest edges, which is the cheapest path only when every edge costs the same. Once edges carry costs (minutes, rupees, effort), the fewest-edge path can be the dearest. Dijkstra's idea is to settle nodes in order of their best known distance: the closest unsettled node cannot be improved, because every other route to it starts from a node that is at least as far. That argument fails as soon as an edge cost is negative.

Invariant. When a node comes off the heap with an up-to-date distance, that distance is final.

412150dist 01dist 32dist 13dist 4Heap holds (distance, node). Pop (0, 0): push (4, 1) and (1, 2).Pop (1, 2): node 1 improves to 3, node 3 gets 6; push (3, 1) and (6, 3).Pop (3, 1): node 3 improves to 4; push (4, 3). Pop (4, 1) and (4, 3): the first is stale(node 1 is already 3), skip it; (4, 3) is current, settle node 3. Pop (6, 3): stale, skip.edge labels are costs; blue is the final shortest distance from node 0
Figure A6.12. Dijkstra on a four-node graph. The frontier is the heap: the cheapest unsettled node is always popped next, and a popped entry that is larger than the stored distance is stale and skipped.

Figure A6.12 runs it from node 0: node 2 is reached at 1 and improves node 1 from 4 to 3; node 1 then improves node 3 from 6 to 4. The stale heap entries (node 1 at 4, node 3 at 6) are skipped when they come up, because their distance is larger than the stored one. Final distances: 0, 3, 1, 4. The code has three methods: Dijkstra with a lazy-deletion heap, 0-1 BFS, and the fewest-stops variant.

Code, 72 lines
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
import java.util.PriorityQueue;

class Paths {
  /** Edges are {from, to, weight}, weights >= 0. MAX_VALUE = unreachable. */
  static long[] dijkstra(int n, int[][] edges, int src) {
    List<List<int[]>> adj = new ArrayList<>();
    for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
    for (int[] e : edges) adj.get(e[0]).add(new int[] {e[1], e[2]});
    long[] dist = new long[n];
    Arrays.fill(dist, Long.MAX_VALUE);
    dist[src] = 0;
    PriorityQueue<long[]> pq =
        new PriorityQueue<>((a, b) -> Long.compare(a[0], b[0]));
    pq.add(new long[] {0, src});               // {distance, node}
    while (!pq.isEmpty()) {
      long[] top = pq.poll();
      int u = (int) top[1];
      if (top[0] > dist[u]) continue;          // stale entry: skip
      for (int[] e : adj.get(u)) {
        long nd = top[0] + e[1];
        if (nd < dist[e[0]]) {
          dist[e[0]] = nd;
          pq.add(new long[] {nd, e[0]});
        }
      }
    }
    return dist;
  }

  /** Weights are only 0 or 1: a deque replaces the heap, O(V + E). */
  static int[] zeroOne(int n, int[][] edges, int src) {
    List<List<int[]>> adj = new ArrayList<>();
    for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
    for (int[] e : edges) adj.get(e[0]).add(new int[] {e[1], e[2]});
    int[] dist = new int[n];
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[src] = 0;
    Deque<Integer> dq = new ArrayDeque<>();
    dq.add(src);
    while (!dq.isEmpty()) {
      int u = dq.pollFirst();
      for (int[] e : adj.get(u)) {
        if (dist[u] + e[1] < dist[e[0]]) {
          dist[e[0]] = dist[u] + e[1];
          if (e[1] == 0) dq.addFirst(e[0]);    // free: front
          else dq.addLast(e[0]);
        }
      }
    }
    return dist;
  }

  /** Cheapest price from src to dst with at most k stops (k + 1 edges). */
  static int cheapest(int n, int[][] edges, int src, int dst, int k) {
    long inf = Long.MAX_VALUE / 2;
    long[] dist = new long[n];
    Arrays.fill(dist, inf);
    dist[src] = 0;
    for (int round = 0; round <= k; round++) {
      long[] next = dist.clone();              // read old, write new
      for (int[] e : edges)
        if (dist[e[0]] + e[2] < next[e[1]]) next[e[1]] = dist[e[0]] + e[2];
      dist = next;
    }
    return dist[dst] >= inf ? -1 : (int) dist[dst];
  }
}

0-1 BFS. When every edge costs 0 or 1, a deque replaces the heap: a free edge puts the neighbour at the front of the line, a costly one at the back, so the deque is always ordered by distance and the whole run is O(V + E). On edges 0 to 1 (cost 1), 0 to 2 (cost 0), 2 to 1 (cost 0) and 1 to 3 (cost 1), node 1 is reached free through node 2, and node 3 costs 1.

At most k stops. Plain Dijkstra is wrong here: the cheapest route to a node may have used up its stops, and keeping only that route throws away a dearer one that still has stops left. Instead, run k + 1 rounds; in each round every edge is relaxed against the distances from the round before, which is why the code relaxes into a copy. Without the copy, one round could chain several edges and the stop count would be wrong. With flights 0 to 1 (100), 1 to 2 (100) and 0 to 2 (500), the cheapest route from 0 to 2 costs 500 with no stops and 200 with one.

Cost. Dijkstra is O((V + E) log V) time and O(V + E) space: each edge pushes at most once. 0-1 BFS is O(V + E). The k-stop rounds cost O(k x E) time and O(V) space. Sums can pass an int, so distances are long in the Dijkstra method.

Hack sheet: shortest paths

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"Cheapest", "fastest", "minimum effort", a weighted graph or grid, "at most k stops", edge costs of only 0 or 1.
TemplateDijkstra: distance array at infinity, a heap of {distance, node}, skip an entry whose distance is larger than the stored one, relax each edge. 0-1 BFS: addFirst for cost 0, addLast for cost 1. k stops: k + 1 rounds over all edges into a copy.
Fallback that still scoresBellman-Ford: V - 1 passes over every edge, O(V x E), correct and it also handles negative edges. For equal costs, plain BFS.
TrapA negative edge breaks Dijkstra. Forgetting to skip stale entries stays correct but slow. A heap comparator that subtracts overflows; use Long.compare. Applying Dijkstra to a stop limit gives wrong answers (above).
Follow-up"I need the route, not just the cost": store parent[v] whenever a distance improves, and walk back from the target.
Too big, streaming or onlineA map too large to store is built on the fly and Dijkstra stops the moment the target comes off the heap; A* with a lower-bound guess, or searching from both ends, shrinks the search further (named, not coded). If edge costs change, rerun it; say that plainly.

Next stop: Pattern 16: sweep line, 4 min

Pattern 16: sweep line

Context. "How many meetings are on at once" or "the busiest hour" asks about overlap, not about merging. Comparing every pair is quadratic. The shift is to stop treating an interval as one object: it is two events, a start that adds one and an end that subtracts one. Sort the events, walk them once, and the running count is the number of intervals covering the current point.

Invariant. The running count equals the number of intervals that have started and not yet ended.

The first method sorts the starts and the ends separately, which gives the same sweep without building event objects. At each start, every meeting that has already ended is released first, so a meeting that ends at 10 and another that starts at 10 share a room (the <=). Ask the interviewer whether touching meetings overlap; the answer flips that one character. The second class is the online form: bookings arrive one at a time, and a TreeMap of deltas keeps them in time order.

Code, 44 lines
import java.util.Arrays;
import java.util.Map;
import java.util.TreeMap;

class Sweep {
  /** Most meetings running at once: sort starts and ends separately. */
  static int minRooms(int[][] meetings) {
    int n = meetings.length;
    int[] starts = new int[n], ends = new int[n];
    for (int i = 0; i < n; i++) {
      starts[i] = meetings[i][0];
      ends[i] = meetings[i][1];
    }
    Arrays.sort(starts);
    Arrays.sort(ends);
    int rooms = 0, best = 0, e = 0;
    for (int s = 0; s < n; s++) {
      while (e < n && ends[e] <= starts[s]) { // free by this start
        e++;
        rooms--;
      }
      rooms++;
      best = Math.max(best, rooms);
    }
    return best;
  }
}

/** Online: bookings arrive one at a time; +1 at a start, -1 at an end. */
class Calendar {
  private final TreeMap<Integer, Integer> delta = new TreeMap<>();

  /** End is exclusive; returns the largest overlap so far. */
  int book(int start, int end) {
    delta.merge(start, 1, Integer::sum);
    delta.merge(end, -1, Integer::sum);
    int running = 0, best = 0;
    for (Map.Entry<Integer, Integer> e : delta.entrySet()) {
      running += e.getValue();
      best = Math.max(best, running);
    }
    return best;
  }
}

A zero-length meeting such as [5, 5] occupies no room, so it adds nothing to the count (without the e < n test the loop would walk off the end of ends). With several zero-length meetings at one time, rooms can dip below zero, because each one's end is consumed before its own start is counted. The maximum is still right: within a run of equal starts the count only rises, and after the last of them it equals the meetings running at that moment (checked against a brute force on 20,000 random sets, zero-length meetings included). For meetings [[0, 30], [5, 10], [15, 20]] the starts are 0, 5, 15 and the ends 10, 20, 30: the count rises to 2 at 5, falls to 1 at 10, and is 2 again at 15, so two rooms. For [[7, 10], [2, 4]] it never passes 1. In Calendar, booking [10, 20), then [15, 25), then [20, 30) returns 1, 2, 2.

Cost. The two sorts are O(n log n); the sweep is O(n); space is O(n). Calendar.book rewalks the whole map, so it is O(n) per booking and O(n squared) over n bookings; a segment tree with lazy updates (named, not coded) makes each booking O(log n). If the online question is only yes or no (accept a booking when it overlaps nothing already booked), Booker in lesson 6 answers each one in O(log n) with floorEntry and ceilingEntry (Figure A6.16); a count of overlaps still needs the segment tree.

Hack sheet: sweep line

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"Meeting rooms", "maximum overlap", "busiest time", "skyline", "employees free at the same time", an interval list and a question about how many overlap.
TemplateSort the starts and the ends; for each start release every end <= it, then add one and record the maximum. Or build (time, +1/-1) events and sum them in order.
Fallback that still scoresFor each interval count how many others overlap it, O(n squared). Or a PriorityQueue of end times, popping the earliest end when a new meeting starts.
TrapTie-breaking at equal times: with an exclusive end, process ends before starts. Sorting the starts and ends together as pairs loses which end belongs to which start, which this method does not need but a later question might.
Follow-up"I also need the time of the maximum": record the point where the count first reaches a new best.
Too big, streaming or onlineBookings that arrive online need the TreeMap form, which is exact but O(n) per booking here; when the question is only whether a booking clashes, Booker in lesson 6 does it in O(log n). Too many intervals for memory: sort them externally by start, then sweep with a heap of the current ends.

Next stop: Pattern 17: Fenwick tree and segment tree, 2 min

Pattern 17: Fenwick tree and segment tree

Context. Prefix sums (pattern 3) answer "sum of a range" in O(1) from a table built once. Change one number and the table is wrong from that position to the end, so a rebuild costs O(n). When updates and queries are mixed, both need to be O(log n). A Fenwick tree does that for sums with very little code. A segment tree does it for any associative combine (minimum, maximum, greatest common divisor) at a little more code.

Invariant. Fenwick: tree[i] holds the sum of the i & -i elements that end at position i (counting from 1). Segment tree: each node holds the combine of its two children, and the leaves are the array.

The two listings (Fenwick and MinSegTree), the segment-tree figure and a worked run are in another section of this appendix. This pattern is recognise and explain only, and the hack sheet below carries both templates.

Cost. Each update or query is O(log n). Building the segment tree is O(n); building the Fenwick tree with n calls to add, as the appendix listing does, is O(n log n). Space is O(n). A change that touches a whole range (add 5 to positions 3 to 9) needs lazy propagation (named, not coded).

Hack sheet: Fenwick and segment trees

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"Range sum with updates", "range minimum with updates", many queries mixed with point changes, "count of smaller numbers after self" (a Fenwick tree over compressed values).
TemplateFenwick: for (i++; i < size; i += i & -i) tree[i] += delta to update, for (i++; i > 0; i -= i & -i) s += tree[i] to query. Segment tree: leaves at t[n..2n-1], parents by combine, query by walking the two ends inward.
Fallback that still scoresRecompute the range from scratch per query, O(n) each, correct when queries are few. Or the prefix array rebuilt after each update, O(n).
TrapThe Fenwick tree is 1-based inside: index 0 would loop forever because 0 & -0 is 0. The bottom-up segment tree needs an associative combine and a half-open range, so an inclusive right end is converted by adding one. Sums can pass an int, so the appendix listings use long.
Follow-up"Add to a whole range": lazy propagation on the segment tree. "Find the k-th element": descend the tree by sums.
Too big, streaming or onlineUpdates and queries already arrive online. A huge index range (up to 10^9) with few touched positions: compress the coordinates offline, or build nodes only when first touched (a dynamic segment tree, named, not coded).

Next stop: Pattern 18: quickselect, 3 min

Pattern 18: quickselect

Context. Pattern 10 finds the k-th largest in O(n log k) with a heap. If you only need the one value, you can do better on average, by borrowing the partition step of quicksort: choose a pivot, move the smaller values to its left, and see where the pivot lands. If it lands at position k, you are done; if it lands left of k, the answer is in the right part; otherwise in the left. Only one side is ever searched, so the work halves each time on average: n + n/2 + n/4 and so on, about 2n.

Invariant. The target position always lies inside [lo, hi], and everything left of lo is no larger than everything inside.

The code returns the k-th smallest, counting k from 1 (the k-th largest is the (n - k + 1)-th smallest). It works on a copy, because partitioning reorders the array, and it picks a random pivot so that no fixed input is the worst case.

Code, 31 lines
import java.util.Random;

class Select {
  private static final java.util.Random RNG = new java.util.Random();

  /** k-th smallest (k from 1), average O(n); works on a copy. */
  static int kthSmallest(int[] nums, int k) {
    int[] a = nums.clone();
    int lo = 0, hi = a.length - 1, target = k - 1;
    while (lo < hi) {
      int p = partition(a, lo, hi);
      if (p == target) return a[p];
      if (p < target) lo = p + 1;
      else hi = p - 1;
    }
    return a[lo];
  }

  private static int partition(int[] a, int lo, int hi) {
    swap(a, lo + RNG.nextInt(hi - lo + 1), hi); // random pivot to the end
    int pivot = a[hi], store = lo;
    for (int i = lo; i < hi; i++)
      if (a[i] < pivot) swap(a, i, store++);
    swap(a, store, hi);
    return store;
  }

  private static void swap(int[] a, int i, int j) {
    int t = a[i]; a[i] = a[j]; a[j] = t;
  }
}

On [3, 2, 1, 5, 6, 4], k = 2 returns 2 and k = 5 returns 5 (the second largest). The Java run compares it with sorting on seeded random arrays.

Cost. O(n) on average, O(n squared) in the worst case, and O(n) extra space for the copy. The simple partition used here slows on many equal values: with 60,000 equal values and k = 30,000 it took about 327 ms on the author's laptop against about 1 ms on random data. The cure is a three-way partition (less than, equal to, greater than the pivot), named here and not coded. A worst-case O(n) exists (median of medians) and is rarely worth writing in a round.

Hack sheet: quickselect

Table: Item, What to carry into the round (6 rows)
ItemWhat to carry into the round
Cue"K-th smallest or largest", "median", "without sorting", "average O(n)".
TemplateRandom pivot to the end; one pass moving smaller values left; swap the pivot into place; narrow lo or hi toward the target position.
Fallback that still scoresSort and index, O(n log n); or the size-k heap of pattern 10, O(n log k). Both are correct and easy to defend.
TrapIt reorders the caller's array, so copy it or say so. Off-by-one between k from 1 and positions from 0. A fixed first or last pivot degrades to O(n squared) on sorted input. Many equal values slow it (above).
Follow-up"Worst case O(n)?" Median of medians, named. "Top k, not just the k-th?" Quickselect leaves the k smallest on its left, unsorted.
Too big, streaming or onlineIt needs the whole array in memory and random access, so a stream gets the size-k heap instead. If values are bounded, one counting pass finds the k-th. Across machines, have each return counts for value ranges and narrow the range.

Next stop: Complexity at a glance, 1 min

Complexity at a glance

Context. Time and space here are derived by the author from the code above (the standard analysis, not a measurement on a machine). Costs of the lesson's methods:

Table: Method, Time, Space (9 rows)
MethodTimeSpace
Two pointers, window, prefix sums, stack, bitsO(n)O(1) to O(n)
Sort first (intervals, 3Sum after the sort, greedy)O(n log n); 3Sum O(n squared)O(n)
BFS, DFS, topological sort, union-findO(V + E)O(V)
DijkstraO((V + E) log V)O(V)
Binary search on the answerO(n log range)O(1)
Heap of size k; Fenwick tree; trie lookupO(n log k); O(log n) per query; O(length)O(k); O(n); O(total letters)
QuickselectO(n) average, O(n squared) worstO(n)
BacktrackingO(n!) or O(2 to the n) outputsO(depth)
DP over a tableO(states times transitions)O(states), or one row

Worked arithmetic. For n = 100,000 a quadratic method does 10,000,000,000 basic steps; an n log n method does about 1,660,964 (log2 of 100,000 is about 16.61), roughly 6,000 times fewer; a linear method does 100,000. The author's reading, not a measurement, is that a machine does tens to hundreds of millions of simple operations a second, fewer in an interpreted language: 10 billion steps is out of reach, under two million is comfortable. Factorial growth is a harder cliff: n = 10 gives 3,628,800 permutations, and n = 20 is hopeless. When you state a complexity, derive it loop by loop: the stack and the window both have an inner loop that looks quadratic and is not, because the inner pointer never goes backward.

Next stop: Choosing between neighbours, 2 min

Choosing between neighbours

Context. Several patterns answer questions with similar wording. The deciding facts are usually one property of the input or one property of the answer, and you can ask the interviewer about either. The table lists the commonest confusions with the question that separates them. It is the author's own guide, built from the cues above.

Table: If you are torn between, Ask yourself, Usually (11 rows)
If you are torn betweenAsk yourselfUsually
Sliding window / prefix sumsAre values all non-negative, so that growing the window only raises the sum, and shrinking only lowers it?Yes: window. No (negatives): prefix sums with a map.
Two pointers / hash setIs the input sorted, or may I sort it and does the order not matter?Sorted: two pointers, O(1) space. Unsorted and order matters: hash set.
BFS / DFSDo I need the fewest steps, or only reachability and components?Fewest steps: BFS. Reachability, components, tree shape: DFS.
Backtracking / DPMust I list the answers, or only count or optimise, with repeated sub-problems?List them: backtracking. Count or best, with repeats: DP.
Union-find / BFS for componentsDo edges arrive over time, or is it one static graph?Arriving or "same group?" queries: union-find. Static: either.
Heap / sortDo I need the whole order, or only the top k?Top k with small k: heap. Whole order: sort.
BFS / DijkstraDo edges have different costs?Equal costs: BFS. Different non-negative costs: Dijkstra. Costs only 0 or 1: 0-1 BFS.
Prefix sums / Fenwick treeDoes the array change between queries?Fixed: prefix sums. Changing: Fenwick or segment tree.
Heap / quickselectOne value, or a running answer as items arrive?One value from a full array: quickselect. Running or streaming: heap.
Interval merging / sweep lineDo I need the merged ranges, or how many overlap at once?Merged ranges: intervals. Overlap count: sweep line.
Binary search on answer / DPIs feasibility monotone in one number?Yes: binary search on that number.

Next stop: Closing the gaps: greedy scheduling and a count-min sketch, 4 min

Closing the gaps: greedy scheduling and a count-min sketch

Greedy: interval scheduling and the exchange argument

A greedy answer is only as good as its proof, and an interviewer will ask. The question: the most meetings one room can host. Sort by end time and take each meeting that starts at or after the last taken end.

Code, 19 lines
import java.util.Arrays;

class Greedy {
  /** Most meetings one room can host; touching ends are fine. */
  static int maxMeetings(int[][] iv) {
    int[][] s = iv.clone();
    Arrays.sort(s, (p, q) -> p[1] != q[1]       // earliest end first,
        ? Integer.compare(p[1], q[1])        // then earliest start
        : Integer.compare(p[0], q[0]));
    int count = 0, lastEnd = Integer.MIN_VALUE;
    for (int[] m : s) {
      if (m[0] >= lastEnd) {
        count++;
        lastEnd = m[1];
      }
    }
    return count;
  }
}

Why it is correct (exchange argument). Take any best schedule. Its first meeting ends no earlier than the meeting that ends first overall. Swap that first meeting for the earliest-ending one: it still fits before the second meeting, because it ends no later. The schedule is no worse. Repeat on what remains. So some best schedule begins with the greedy choice. Sorting by start time or by shortest length fails: one long meeting can block many short ones, and a short one can sit in the middle of two others that would both fit. The harness checks the rule against a brute force over every subset of up to eight meetings, 3,000 times. Sorting makes it O(n log n). One consistency note with the sweep in Pattern 16: there a zero-length meeting such as [5, 5] needs no room, so it never raises the room count; here it counts as one meeting you can attend, because it starts at or after the last end and sets the new end to the same time. The two are different questions (rooms needed against meetings attended), and both are right; ask whether a zero-length meeting is a meeting. The tie-break on start matters only for zero-length meetings: with [3, 5] and [5, 5] ending together, taking [5, 5] first would block [3, 5]; start order takes both. The harness checks the rule on touching and zero-length cases too, 5,000 random cases against every subset.

Other greedy questions follow the same two steps: pick the sort key, then give the swap argument in three sentences. Task scheduler and jump game are both of this kind; if you cannot produce the swap, you may have a heuristic, not a greedy.

Count-min sketch: top-k on a stream you cannot store

For top-k words on a 50 GB stream, Pattern 10's hack sheet keeps a heap per shard and merges the shard heaps, which still needs each shard's table of counts. When even that table will not fit, a count-min sketch keeps a small grid of counters: depth rows, width columns, one hash per row. Adding a key bumps one counter in each row; the estimate is the smallest of its counters. Collisions only add, so the estimate is never below the true count. The sizing rule, from the author's memory of Cormode and Muthukrishnan (2005), which was not re-read: width about e divided by epsilon and depth about ln of 1 divided by delta give an overcount of at most epsilon times the stream length N with probability at least 1 minus delta. Treat that as secondary: Wikipedia's count-min article gives the same width and depth (read 1 Nov 2026), but the paper itself could not be opened.

Code, 28 lines
class CMS {
  private final int[][] t;
  private final int w;
  private final long[] seed;

  CMS(int depth, int width) {
    t = new int[depth][width];
    w = width;
    seed = new long[depth];
    for (int i = 0; i < depth; i++) seed[i] = 0x9E3779B97F4A7C15L * (i + 1);
  }

  private int slot(String key, int row) {
    long h = key.hashCode() * 0xFF51AFD7ED558CCDL + seed[row];
    h ^= h >>> 33; h *= 0xC4CEB9FE1A85EC53L; h ^= h >>> 33;
    return (int) Math.floorMod(h, (long) w);
  }

  void add(String key) {
    for (int i = 0; i < t.length; i++) t[i][slot(key, i)]++;
  }

  int estimate(String key) {            // never below the true count
    int m = Integer.MAX_VALUE;
    for (int i = 0; i < t.length; i++) m = Math.min(m, t[i][slot(key, i)]);
    return m;
  }
}

The harness builds a skewed 20,000-item stream with 1,157 distinct keys at depth 4 and width 272 (about epsilon 0.01), asserts that no key is ever undercounted, and that fewer than one key in twenty is over by more than 0.01 times N. It printed keys 1157 over eps*N 0. For top-k keep a small heap of k candidates beside the sketch and compare each new estimate with the heap's smallest. Say what it cannot do: it cannot list the keys, delete, or give exact counts.

Next stop: Tests, naming, and changing code under a follow-up, 1 min

Tests, naming, and changing code under a follow-up

Four habits show in every answer that reads as senior, and each costs under a minute. First, name tests as sentences before writing them: "single task", "cycle", "ties". Second, write the check as a small function that throws, as the harnesses in this lesson do, so you can run it again after each change. Third, when a requirement changes, edit the smallest part and say which invariant still holds; the transcript above did this with the worker cap and the changed duration. Fourth, rename a variable the moment you misread it; from reads better than p and costs five seconds. Interviewers cannot see a plan that was not said, so say it. Figure A6.15 puts the two time budgets of lesson 6 side by side: one problem, and two problems in the same 45 minutes.

One problem (the lesson 6 table; the rehearsal card stops you at 29 and 36)clarifyapproachcodetrace, testsfollow-ups049293645 minTwo problems (the author's marks)introA: ask, code, traceB: askB: codetracecost, Qs051621333845 minHard stop on A at 16 (18 at the latest). Author's budgets, not a company standard.
Figure A6.15. Two time budgets for a 45-minute slot. With one problem the code block is under half the slot; with two, each problem keeps its own trace and the first has a hard stop at minute 16.

Next stop: Two follow-ups the earlier parts only named, 3 min

Two follow-ups the earlier parts only named

Islands II: land arrives one cell at a time

Counting islands with a flood fill after each added cell costs the whole grid every time. A new cell is either on already-land (no change), or it starts one island and merges with up to four neighbours. Union-find gives the count directly: add one for the new cell, subtract one for every union that joins two different islands. Pattern 8's union-find, with path halving, is reused.

Code, 37 lines
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Islands2 {
  private static int find(int[] p, int x) {
    while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; }   // path halving
    return x;
  }

  /** Island count after each land cell is added; repeats are harmless. */
  static List<Integer> counts(int h, int w, int[][] adds) {
    int[] p = new int[h * w];
    Arrays.fill(p, -1);                       // -1 means still water
    int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    int islands = 0;
    List<Integer> out = new ArrayList<>();
    for (int[] a : adds) {
      if (a[0] < 0 || a[0] >= h || a[1] < 0 || a[1] >= w)
        throw new IllegalArgumentException("cell off the grid");
      int id = a[0] * w + a[1];
      if (p[id] == -1) {
        p[id] = id;
        islands++;                            // new island, then merge
        for (int[] d : dirs) {
          int r = a[0] + d[0], c = a[1] + d[1];
          if (r < 0 || c < 0 || r >= h || c >= w || p[r * w + c] == -1)
            continue;
          int x = find(p, id), y = find(p, r * w + c);
          if (x != y) { p[x] = y; islands--; }
        }
      }
      out.add(islands);
    }
    return out;
  }
}

The harness replays 2,000 random sequences (with repeated cells) against a fresh flood fill after every step, and checks the sample: on a 3 by 3 grid, adding (0,0), (0,1), (1,2), (2,1), (1,1) gives 1, 1, 2, 3, 1. Cost: m additions in O(m log n) at worst with path halving alone (the bound Pattern 8 states), which is near linear in practice. The follow-up "a cell can turn back into water" breaks the structure, since union-find cannot split a set. The honest answers: if all the operations are known in advance, process them backwards so removals become additions (offline); online, either rebuild from the grid for each removal, or use union-find with rollback when removals undo the latest additions in order. Name these; do not code them unless asked.

Online booking: a sorted map with floor and ceiling

Pattern 16 counted overlaps, and its Calendar.book rewalks the whole map, O(n) per booking. A different question: accept a booking only if it overlaps nothing already accepted, and answer each one as it arrives. Keep the accepted bookings in a sorted map keyed by start. A new booking [s, e) can only clash with the booking that starts at or before s (does it run past s?) or the booking that starts at or after s (does it start before e?). Those two neighbours come from floorEntry and ceilingEntry, each O(log n) in the Java documentation's words for TreeMap, so a booking is O(log n) where a list scan is O(n). Figure A6.16 draws the two checks.

[10, 20)[30, 40)18..25 no20..30 yesfloorEntry(s): last start at or before s. ceilingEntry(s): first at or after s.
Figure A6.16. Two neighbour checks decide a booking. [18, 25) fails the floor test (the booking before it ends at 20, after 18); [20, 30) passes both tests because touching ends are allowed.
Code, 16 lines
import java.util.TreeMap;

class Booker {
  private final TreeMap<Integer, Integer> byStart = new TreeMap<>();

  /** Half-open [s, e). True and stored if it overlaps nothing booked. */
  boolean book(int s, int e) {
    if (s >= e) return false;                    // empty or backwards
    var before = byStart.floorEntry(s);          // last start <= s
    if (before != null && before.getValue() > s) return false;
    var after = byStart.ceilingEntry(s);         // first start >= s
    if (after != null && after.getKey() < e) return false;
    byStart.put(s, e);
    return true;
  }
}

The harness compares 3,000 random sequences of twelve bookings with a linear scan, and checks the touching case (10 to 20, then 20 to 30 is accepted; the one in between is not) and the empty booking. The rule on empty and backwards bookings (rejected here) is a question for the interviewer, as the zero-length note under Greedy says. Do not confuse this with the sweep: the sweep tells you how many rooms a known list needs; this decides yes or no online.

Next stop: A vague prompt and parsing, 5 min

A vague prompt and parsing

In this section

How to turn "write a calculator" into numbers you can build to; a tokenizer and a recursive-descent calculator with unary minus and brackets; and a no-execution dry-run drill. Matrices in place and wildcard matching are in another section of this appendix. Every listing was compiled and run on OpenJDK 25 in Java 21 source mode (java --source 21), with explicit AssertionError checks against a brute force or an oracle.

When the prompt is vague: five numbers and the assumptions you state

Parsing prompts arrive short. The senior move is to convert the sentence into limits before any code, and to say the assumptions aloud so a wrong one can be corrected in ten seconds rather than twenty minutes.

Asked to "write a calculator", ask five things in one breath: which operators, brackets and nesting, whole numbers or decimals and how big, what bad input should do, and how long the input is. Then state your assumptions before any code (here: division truncates toward zero; overflow and division by zero are errors; spaces allowed) and the design they lead to: a tokenizer, then one parsing function per precedence level. Asking takes about a minute; a wrong guess costs a rewrite. An invented dialogue of this exchange is in another section of this appendix.

Tokenizer and recursive-descent calculator

Brute force here is not a loop; it is the wrong shape. Scanning for the lowest-precedence operator and splitting works but rescans the string at every level, O(n squared) on deep nesting, and handles unary minus badly. The shape that works is a grammar with one function per level: expr calls term, term calls unary, and unary calls expr again inside brackets. Each function reads the level below it and loops on its own operators, which gives left-to-right grouping: 10-4-3 is 3, not 9.

The tokens carry their position, so an error can say where. Every arithmetic step uses the Exact methods, which throw ArithmeticException on overflow instead of wrapping (Oracle, Java SE 21 API, opened 2 Nov 2026). The one trap in the lexer is a number such as 99999999999999999999, which overflows while it is being read, so the digit loop uses them too. Division needs its own: v / r does not throw on Long.MIN_VALUE divided by -1, it returns Long.MIN_VALUE, and the Math page says so in those words, so plain v / r would break the overflow rule; Math.divideExact, which the page marks "Since: 18", throws instead. The lexer also tests digits with a plain '0' to '9' range: Character.isDigit accepts digits from other scripts, and for the Arabic-Indic one c - '0' is 1585, so a lexer using it reads that character as the number 1585 instead of rejecting it as a bad character (checked in the run on 2 Nov 2026).

Code, 90 lines
import java.util.ArrayList;
import java.util.List;

class Calc {
  enum Kind { NUM, PLUS, MINUS, STAR, SLASH, LP, RP }
  record Tok(Kind kind, long value, int pos) {}

  static boolean digit(char c) { return c >= '0' && c <= '9'; } // ASCII

  static List<Tok> lex(String s) {
    List<Tok> out = new ArrayList<>();
    int i = 0;
    while (i < s.length()) {
      char c = s.charAt(i);
      if (c == ' ') { i++; continue; }
      if (digit(c)) {
        int start = i;
        long v = 0;
        while (i < s.length() && digit(s.charAt(i))) {
          v = Math.addExact(Math.multiplyExact(v, 10L), s.charAt(i++) - '0');
        }
        out.add(new Tok(Kind.NUM, v, start));
        continue;
      }
      Kind k = switch (c) {
        case '+' -> Kind.PLUS;
        case '-' -> Kind.MINUS;
        case '*' -> Kind.STAR;
        case '/' -> Kind.SLASH;
        case '(' -> Kind.LP;
        case ')' -> Kind.RP;
        default -> throw new IllegalArgumentException("bad char at " + i);
      };
      out.add(new Tok(k, 0, i++));
    }
    return out;
  }

  // expr = term (('+'|'-') term)*   term = unary (('*'|'/') unary)*
  // unary = '-' unary | '(' expr ')' | NUM
  private final List<Tok> t;
  private int p = 0;
  private Calc(List<Tok> t) { this.t = t; }

  static long eval(String s) {
    Calc c = new Calc(lex(s));
    long v = c.expr();
    if (c.p != c.t.size())
      throw new IllegalArgumentException("junk at token " + c.p);
    return v;
  }

  private boolean at(Kind k) { return p < t.size() && t.get(p).kind() == k; }

  private long expr() {
    long v = term();
    while (at(Kind.PLUS) || at(Kind.MINUS)) {
      boolean plus = t.get(p++).kind() == Kind.PLUS;
      long r = term();
      v = plus ? Math.addExact(v, r) : Math.subtractExact(v, r);
    }
    return v;
  }

  private long term() {
    long v = unary();
    while (at(Kind.STAR) || at(Kind.SLASH)) {
      boolean mul = t.get(p++).kind() == Kind.STAR;
      long r = unary();
      if (!mul && r == 0) throw new ArithmeticException("divide by zero");
      // divideExact (Java 18+) throws on Long.MIN_VALUE / -1; v / r wraps
      v = mul ? Math.multiplyExact(v, r) : Math.divideExact(v, r);
    }
    return v;
  }

  private long unary() {
    if (at(Kind.MINUS)) { p++; return Math.negateExact(unary()); }
    if (at(Kind.LP)) {
      p++;
      long v = expr();
      if (!at(Kind.RP)) throw new IllegalArgumentException("missing )");
      p++;
      return v;
    }
    if (at(Kind.NUM)) return t.get(p++).value();
    throw new IllegalArgumentException("expected a number at token " + p);
  }
  // main and the test helpers are in the file that was run
}

Results from the file that was run: 1+2*3 is 7; (1+2)*3 is 9; -(2+3)*-4 is 20; 10-4-3 is 3; 100/7/2 is 7; --5 is 5; 7/-2 is -3. Eleven malformed inputs are rejected: nine ordinary ones (empty, 1+, (1, 1), 2 3, 1/0, a letter, a leading star, and the 20-digit number), plus (0-9223372036854775807-1)/-1, which is Long.MIN_VALUE divided by -1, and an Arabic-Indic digit one followed by +1. The bracket (0-9223372036854775807-1) on its own still evaluates to Long.MIN_VALUE. A further 20,000 random expressions were checked against a BigInteger evaluator (details in this appendix).

Cost. Lexing and parsing are one pass each, O(n) time. Space is O(n) for the token list, plus recursion depth equal to the bracket nesting; a line of 100,000 open brackets would overflow the stack, which is one sentence to say aloud and a reason to ask "short lines". An explicit-stack version (shunting-yard) removes that limit.

A dry run with no execution

Some rounds score a dry run: trace the code by hand without executing it. The habit is cheap. Drill: write your list reverse (drill 1) on paper, trace it on 1, 2, 3 with a column of three values per step (prev, head, next) and say the final list before you type. Do the same for your window maximum (drill 9) on [1, 3, -1, -3, 5] with k = 3: the deque of indexes after each step is [0], [1], [1, 2], [1, 2, 3], [4], and the output is 3, 3, 5. If your trace and your code disagree, the code is wrong about half the time and the trace the rest, which is why you do it twice.

Next stop: A question bank: common types the patterns did not solve, 6 min

A question bank: common types the patterns did not solve

Eleven common question types were named earlier in this lesson, or asked often enough to need one, but not solved by a pattern section. Two are worked here: grid paths, and reservoir sampling after it. The other nine are worked in full in another section of this appendix, each with a listing compiled and run with java --source 21 on 2 Nov 2026 against a brute force on random inputs; the table keeps the idea and the trap of each. An LRU cache is in the next section (and with threads in lesson 7), and partition into equal subsets is Pattern 12's knapsack asking whether half the total is reachable.

Table: Type, Idea and cost, Trap (9 rows)
TypeIdea and costTrap
Search in a rotated sorted arrayBinary search: one side of mid is always sorted, so test whether the target lies in it. O(log n).The test is a[lo] <= a[mid], not <: when lo equals mid the one-element half is sorted.
Trapping rain waterTwo pointers: whichever end is lower, the highest bar seen on that side is the binding wall. O(n), O(1) space.Moving the higher end; an int total.
Bracket matchingA stack: each closer must match the latest unmatched opener. O(n).Popping an empty stack when a closer comes first: test isEmpty before pop.
Grouping anagramsKey each word by its 26 letter counts. O(total letters).An int[] key compares by identity; turn it into a string.
String matching (KMP)A failure table of longest proper prefix that is also a suffix; the text pointer never moves back. O(n + m).Forgetting k = fail[k - 1] after a full match, which loses overlapping hits.
Minimum window, at most k distinctA window with a count of characters still owed; grow until valid, shrink while valid (or while broken, for k distinct). O(|s| + |t|).Recording the minimum after the shrink has made the window invalid.
Largest rectangle in a histogramPattern 5's stack of rising heights; a popped bar's walls are i and the new top. O(n).Leaving out the 0 bar at the end: [1, 2, 3] then gives 0, not 4.
Tree diameter, maximum path sumReturn the height (or best downward sum) and keep the best answer through each node in a field. O(n).Answering at the root only; starting the best sum at 0.
Copy a list with random pointers, clone a graphA map from original to copy, filled before recursing. O(n), or O(V + E).Adding the copy to the map after its neighbours, which recurses for ever on a cycle.

Grid paths: minimum sum and counting

Cue. A grid, moves only right or down, and "minimum path sum", "number of paths" or "with obstacles". Idea. A cell's best cost is its own cost plus the better of the cell above and the cell to its left. Row i needs only row i - 1 and the cell just filled to its left, so one row of the table is enough, as in Lcs.rolled.

Code, 29 lines
class GridPath {
  /** Cheapest top-left to bottom-right path, moving right or down.
   *  One row of the table is kept; grid is non-empty. */
  static long minPathSum(int[][] g) {
    int n = g[0].length;
    long[] row = new long[n];             // row[j]: best cost into (i, j)
    for (int i = 0; i < g.length; i++) {
      for (int j = 0; j < n; j++) {
        long best;
        if (i == 0 && j == 0) best = 0;
        else if (i == 0) best = row[j - 1];        // only from the left
        else if (j == 0) best = row[j];            // only from above
        else best = Math.min(row[j], row[j - 1]);  // above or left
        row[j] = best + g[i][j];
      }
    }
    return row[n - 1];
  }

  /** Paths through an m by n grid, right or down; exact to 34 x 34. */
  static long uniquePaths(int m, int n) {
    long[] row = new long[n];
    java.util.Arrays.fill(row, 1);       // the first row: one way each
    for (int i = 1; i < m; i++)
      for (int j = 1; j < n; j++)
        row[j] = Math.addExact(row[j], row[j - 1]);  // throws, no wrap
    return row[n - 1];
  }
}
INVENTED EXAMPLE 1 min

For the grid [[1, 4, 2], [2, 8, 1], [5, 1, 3]], the row after each pass is 1, 5, 7; then 3, 11, 8; then 8, 9, 11. The answer is 11, along 1, 4, 2, 1, 3. uniquePaths(3, 3) is 6.

Cost. O(R x C) time, O(C) space. The harness compared both methods with plain recursion on 3,000 random grids up to 4 by 4, and checked uniquePaths(34, 34) as 7,219,428,434,016,265,740, which still fits in a long. From 35 by 35 the count passes Long.MAX_VALUE, and Math.addExact throws ArithmeticException instead of wrapping to a wrong number, the lesson's overflow rule; the harness checks that 35 by 35 is the first square that throws. Fallback: the full table, or the memoised recursion of Pattern 12. Trap: the first row and first column have one neighbour each; with obstacles, a blocked cell sets row[j] = 0 when counting; and an int count overflows from 18 by 18 on (2,333,606,220 paths, in the same run).

Gave up / revisit when / signal (grid paths). Gave up: moves in all four directions, where cycles remove any fill order and the problem becomes Dijkstra (Pattern 15). Revisit when: diagonal or four-way moves, or negative cells. Signal: the candidate named the fill order and why one row is enough.

Reservoir sampling: k items from a stream of unknown length

Cue. "Pick k at random", "uniformly", and a stream or a list too long to store or count first. Idea. Keep the first k items. For item i (counting from 1) with i > k, draw j uniformly from 0 to i - 1; if j < k, item i replaces slot j. Why it is uniform, in two lines. Item i enters with chance k/i, and a later step j evicts it only by landing on its slot, chance (k/j)(1/k) = 1/j. So it ends in the sample with chance (k/i) x (i/(i + 1)) x ... x ((n - 1)/n) = k/n; the first k items enter with chance 1 and the product from k + 1 gives k/n too.

Code, 22 lines
import java.util.Iterator;
import java.util.Random;

class Reservoir {
  /** k items, uniform over a stream of unknown length n >= k. */
  static int[] sample(Iterator<Integer> stream, int k, Random rnd) {
    int[] keep = new int[k];
    int seen = 0;
    while (stream.hasNext()) {
      int x = stream.next();
      seen++;
      if (seen <= k) {
        keep[seen - 1] = x;                // the first k go in
      } else {
        int j = rnd.nextInt(seen);         // 0 to seen - 1
        if (j < k) keep[j] = x;            // in with chance k / seen
      }
    }
    if (seen < k) throw new IllegalArgumentException("fewer than k");
    return keep;
  }
}
INVENTED EXAMPLE 1 min

Stream 0 to 9, k = 3, 200,000 runs with new Random(42): each value should be picked 3/10 of the time, 60,000 times. The harness requires every count within 2 per cent of that; the worst gap was 0.55 per cent. It also checks k = 1 over five items (each count within 400 of 20,000 in 100,000 runs), a stream exactly k long, and that a shorter one throws.

Cost. O(n) time, one random draw per item after the first k; O(k) space. Trap. Drawing one short, from 0 to i - 2 (nextInt(seen - 1)): in the same 200,000 seeded runs the first three values were picked about 44,000 to 44,600 times each instead of 60,000 (a mutant, run on 2 Nov 2026). Fallback that still scores: give each item a random key and keep the k smallest keys in a heap of size k (Pattern 10): O(n log k), also uniform. If stuck: do k = 1 aloud on three items: keep item i with chance 1/i, so the first survives 1 x 1/2 x 2/3 = 1/3, the second 1/2 x 2/3 = 1/3, and the third 1/3.

Next stop: Small classes: an LRU cache, a time-keyed store and an iterator, 9 min

Small classes: an LRU cache, a time-keyed store and an iterator

Context. Lesson 7 builds caches and queues with threads. Inside a DSA round the same objects arrive as plain coding problems, "design a class whose methods each run in O(1)", often followed by "now make it thread-safe" (author's reading of the lesson 6 and lesson 7; no company publishes how often). They test something the pattern problems do not: whether you choose an invariant before typing and keep it true in every method. The LRU cache and the time-keyed store were compiled and run in one file with java --source 21 on OpenJDK 25 on 2 Nov 2026, each checked against a plain list model on 3,000 random runs of 30 to 40 operations and against the invented traces here; a hand-rolled LRU cache and a min stack from the same file are in another section of this appendix. The iterator was run on its own.

LRU cache, with the library

Cue. "Least recently used", "capacity", "get and put in O(1)". Idea. A hash map gives O(1) lookup but no order; a doubly linked list gives O(1) move-to-front and drop-from-the-back but no lookup. Keep both, pointing at the same nodes. Java's library already joins them: in Oracle's words, a LinkedHashMap built in access order is "well-suited to building LRU caches", and its removeEldestEntry hook "may be overridden to impose a policy for removing stale mappings automatically when new mappings are added" (Java SE 21 API, opened 2 Nov 2026). Say the library version first, in two lines, then offer to write the list yourself; the hand-rolled one is the version an interviewer usually asks to see (author's reading), and it is in another section of this appendix.

Code, 17 lines
import java.util.LinkedHashMap;
import java.util.Map;

/** Library version: access order plus an eviction hook. */
class LruMap<K, V> extends LinkedHashMap<K, V> {
  private final int cap;

  LruMap(int cap) {
    super(16, 0.75f, true);   // true: access order, not insertion
    this.cap = cap;
  }

  @Override
  protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    return size() > cap;      // asked after every insert
  }
}

Cost. O(1) average per get and put, from the hash map; O(capacity) space. Trap. The two-argument constructor keeps insertion order, so a get is not a use and the wrong entry is evicted; access order needs the third argument, true (checked in a run on 2 Nov 2026). A put to a key already present is a use too. By hand, the node must store its key: eviction starts from the list end and must also remove the map entry. Fallback that still scores: this version, with the two lines that matter said aloud (access order true; evict when size() > cap).

The follow-up: make it thread-safe. Which lock: one lock around the whole cache, either a synchronized wrapper or Collections.synchronizedMap, which Oracle names because the class "is not synchronized". A read-write lock does not help here, and saying why is the senior signal: every get reorders the list. Oracle puts it plainly: "In access-ordered linked hash maps, merely querying the map with get is a structural modification." So a get needs the write lock too, and a read-write lock becomes a slower mutex. To scale, split the keys into stripes, each a smaller cache with its own lock, chosen by the key's hash. The price is that eviction happens per stripe, so the cache is no longer exactly least recently used across all keys. Lesson 7 tests locked caches under real threads.

What contention costs was measured crudely on 2 Nov 2026 on an Apple M2 with 8 cores and OpenJDK 25: the hand-rolled cache (this appendix), capacity 10,000, keys drawn from 0 to 19,999, 80% gets, 4 million operations in all, three runs after a warm-up, timed with System.nanoTime rather than a benchmark harness such as JMH. The table gives total throughput across all threads, in million operations a second:

Table: Set-up, Run 1, Run 2, Run 3 (3 rows)
Set-upRun 1Run 2Run 3
1 thread, one lock11.58.414.2
4 threads, one lock5.06.24.9
4 threads, 8 stripes7.88.27.4

On this machine, four threads sharing one lock did less work in total than one thread alone: each operation is so short that passing the lock between cores costs more than the work. Striping won back part of the loss but did not beat one thread. The spread between runs is wide (8.4 to 14.2 for the same set-up), so read the order, not the numbers; they say nothing about other hardware. The sentence for the round: "One lock is correct. Under contention it serialises everything, and for work this cheap the handoff dominates, so I would measure before striping, and I would say that striping gives up exact LRU."

Gave up / revisit when / signal (LRU). Gave up: LFU (least frequently used, a map from frequency to list), expiry by time (the TTL cache in lesson 7's this appendix) and lock-free designs. Revisit when: the prompt says "frequency" or "expires". Signal: the map-plus-list invariant stated before typing, and the reason a read-write lock fails.

A time-keyed store

Cue. "set(key, value, time)", "get(key, time) returns the value set at the latest time at or before it", "a snapshot". Idea. Per key, a sorted map from time to value; get is a floor lookup. Oracle's TreeMap gives "worst-case log(n) time cost for the containsKey, get, put and remove operations", and its floorEntry "Returns a key-value mapping associated with the greatest key less than or equal to the given key, or null if there is no such key" (Java SE 21 API, opened 2 Nov 2026).

Code, 20 lines
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

/** set(k, v, t); get(k, t) is the value set at the latest t' <= t. */
class TimeMap {
  private final Map<String, TreeMap<Integer, String>> m = new HashMap<>();

  void set(String key, String value, int t) {
    m.computeIfAbsent(key, k -> new TreeMap<>()).put(t, value);
  }

  /** Returns "" when nothing was set for key at or before t. */
  String get(String key, int t) {
    TreeMap<Integer, String> h = m.get(key);
    if (h == null) return "";
    Map.Entry<Integer, String> e = h.floorEntry(t);
    return e == null ? "" : e.getValue();
  }
}
INVENTED EXAMPLE 1 min

set("price", "100", 5) and set("price", "120", 9). get("price", 4) returns "", since nothing was set yet; get at 5 returns "100"; get at 8 returns "100"; get at 30 returns "120"; get("cost", 30) returns "". The harness checks this trace and 3,000 random runs against a scan of every (key, time, value) ever set, where a second set at the same time replaces the first.

Cost. O(log n) per set and get, for n entries under the key; space for every entry ever set. If the prompt says times arrive in increasing order for each key, an array list with a binary search gives O(1) amortised set, the same O(log n) get and less memory per entry; ask before choosing. Trap. Two questions decide the code, so ask them: what get returns when nothing is set (here "", not null), and whether a second set at the same time replaces the first (here it does). Thread safety: a ConcurrentHashMap of ConcurrentSkipListMap. Oracle documents that on ConcurrentHashMap the "entire method invocation is performed atomically" for computeIfAbsent, so two threads cannot create two histories for one key, and that the skip list has "expected average log(n) time cost for the containsKey, get, put and remove operations", runs them "safely" across threads, and keeps floorEntry (Java SE 21 API, opened 2 Nov 2026). That version was not run under threads in this lesson. Fallback that still scores: a list of (time, value) pairs per key and a linear scan on get, O(n), stated as such.

Gave up / revisit when / signal (time-keyed store). Gave up: the snapshot array, which is the same idea with snapshot ids for times (per index, a sorted map from snapshot id to value), and trimming old history. Revisit when: values must expire or memory is capped. Signal: the floor lookup named as the reason for the sorted map, before any code.

An iterator: flatten a nested list, and the hasNext contract

Cue. "Design an iterator", "implement next and hasNext", "flatten", "peek". The contract. Oracle's Java SE 21 Iterator page says hasNext "Returns true if the iteration has more elements. (In other words, returns true if next() would return an element rather than throwing an exception.)", and that next throws NoSuchElementException "if the iteration has no more elements" (opened 2 Nov 2026). So hasNext may be called any number of times without losing an element, and next must work when hasNext was never called. Idea. A stack of iterators, one per open list. hasNext does the searching: it steps into lists and pops finished ones until it holds the next integer, then caches it. next hands the cached integer over once.

Code, 39 lines
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

/** The integers of a nested list, in order. Elements are Integer or
 *  List; hasNext finds the next integer and may be called repeatedly. */
class Flatten implements Iterator<Integer> {
  private final Deque<Iterator<?>> stack = new ArrayDeque<>();
  private Integer next;                    // found, not yet returned

  Flatten(List<?> root) {
    stack.push(root.iterator());
  }

  @Override
  public boolean hasNext() {
    while (next == null && !stack.isEmpty()) {
      Iterator<?> top = stack.peek();
      if (!top.hasNext()) {
        stack.pop();                       // this list is used up
        continue;
      }
      Object o = top.next();
      if (o instanceof Integer v) next = v;
      else stack.push(((List<?>) o).iterator());  // one level down
    }
    return next != null;
  }

  @Override
  public Integer next() {
    if (!hasNext()) throw new NoSuchElementException();
    Integer v = next;
    next = null;                           // hand it over once
    return v;
  }
}
INVENTED EXAMPLE 1 min

[1, [], [2, [3, []]], [[]], 4] yields 1, 2, 3, 4. After 1, hasNext steps into [] and out again, then into [2, [3, []]], and caches 2. After 3 it pops the finished lists, walks into [[]] and back out, and caches 4 from the top level. [[], [[]]] holds no integer, so hasNext is false at once.

Cost. O(1) amortised per call, since over the whole walk each list is pushed and popped once and each element read once; O(d) space for nesting depth d. Trap. A hasNext that only asks "is the stack non-empty?" says true for [[]], and next then has nothing to give. Another is a hasNext that moves past an element on every call, so asking twice skips one; the harness calls hasNext zero, one or two times before each next, on 5,000 random nested lists against a recursive walk, and checks that hasNext stays false after the end and that next then throws. The peeking variant (named, not coded) is the same cache: peek fills it if empty and returns it without clearing; next returns and clears it. Fallback that still scores: flatten everything into an ArrayList in the constructor and walk an index: O(n) memory instead of O(d), and say so. If stuck: "I will make hasNext do all the searching and cache what it finds; then next is three lines."

Next stop: Two hard ones, in outline: an interval DP and the median of two sorted arrays, 4 min

Two hard ones, in outline: an interval DP and the median of two sorted arrays

Context. Senior rounds reach a hard problem often enough that the partial-credit path needs rehearsing as well as the full answer (author's reading). Each item below gives the problem in the author's words, the idea, the cost, the trap and the sentence to say if the clock runs out before the code works. The listings, figures and traces are in another section of this appendix, where both classes were compiled and run in one file with java --source 21 on 2 Nov 2026.

Burst balloons: interval DP, choosing the last move

Problem. Balloons in a row carry numbers. Bursting balloon i earns left × i × right, using its neighbours at that moment, with a 1 beyond either end. Burst them all, in the order that earns the most. Limits (invented): up to 300 balloons, values 0 to 100. Cue. The order of removals matters, and each removal's value depends on what is next to it at the time. Idea. Choosing which balloon to burst first splits nothing, because its neighbours then become neighbours of each other. Choose instead the balloon k that bursts last inside an open interval (l, r): at that moment its neighbours are exactly l and r, and the balloons on each side were burst independently inside (l, k) and (k, r). Pad a 1 at each end; then dp[l][r] is the maximum over k of dp[l][k] + a[l]·a[k]·a[r] + dp[k][r], filled by growing the interval's length.

Cost. O(n3) time and O(n2) space. At n = 300 the inner step runs 4,545,100 times (arithmetic, counted by script). An int is safe: the total is at most n × 1003 = 3 × 108, under 231 (arithmetic). Trap. Thinking about the first balloon to burst: the two sides are not independent, because they become neighbours. A second trap: the loop must grow by length, so that dp[l][k] and dp[k][r] are filled before dp[l][r] reads them.

What I would say if I could not finish. "I would memoise on the set of balloons still standing, as a bitmask: for each standing balloon, burst it now, find its nearest standing neighbours and recurse. With the neighbour scan that is O(2n · n2), fine up to about n = 15 and hopeless at 300. The fix is to think about the last balloon instead of the first, because that makes the two sides independent intervals." The run file holds that fallback and checks the DP against it.

Gave up / revisit when / signal (interval DP). Gave up: palindrome partitioning with fewest cuts and matrix-chain order, which fill the same (l, r) table. Revisit when: a prompt merges or splits a sequence and the cost depends on both ends. Signal: the word "last" said before any code.

Median of two sorted arrays: binary search on the cut

Problem. Two sorted int arrays of sizes m and n, not both empty; return the median of all m + n values in O(log(min(m, n))) time. Cue. "Two sorted arrays", "median" or "k-th smallest", and a limit that rules out a linear merge. Idea. The median splits the merged values into a left half of (m + n + 1) / 2 values and the rest. Take i values from a and j = half − i from b. The cut is right when every left value is at most every right value, and because each array is sorted, two cross checks are enough: a[i−1] ≤ b[j] and b[j−1] ≤ a[i]. Binary search on i over the shorter array, with sentinels standing in for a missing side.

Cost. O(log(min(m, n))) time, O(1) space. Traps. Searching the longer array lets j go negative. hi starts at m, not m − 1, because the cut can fall after a's last value. The sum of the two middle values can overflow an int, so the average is taken in a long: the harness checks MAX_VALUE with MAX_VALUE, and MIN_VALUE with MAX_VALUE, which gives −0.5.

What I would say if I could not finish. "A merge walk gets the answer: two pointers advance (m + n) / 2 + 1 steps, keeping the last two values, O(m + n) time and O(1) space. The log version does better by searching where the cut falls in the shorter array instead of walking to it." Say it, and code the walk if the binary search does not work by minute 25 of the round.

Gave up / revisit when / signal (median). Gave up: the k-th smallest of two arrays by discarding k/2 values a step, and the median of k sorted arrays. Revisit when: the prompt asks for the k-th value, not the median. Signal: the two cross checks stated before any code.

Use it in step 6: The coding round, with or without AI