DSA Notes · chapter 02 · Java Live Code

Trees, heaps, and tries

A tree is a connected acyclic graph: exactly one path between any two nodes. Recursion walks that path on the call stack. A queue walks it a level at a time. A size-k min-heap keeps only the candidates that can still be the answer. A trie shares prefixes so insert and search are a walk down letters. After the dry-run tables, the notebook rows are other candidates’ families — not a Job 10454435 list.

Prev: 01 Graphs · Next: 03 Backtracking

02

Six algorithms on trees

Inorder vs level-order, BST compare-and-walk, a path that can bend through a node, LCA by split, a size-k heap, then a trie. Every card has a simple Java version, an interview Java version, a tiny example, and a second example that does not skip a step. Shared node type for the tree cards:

class TreeNode {
    int val;
    TreeNode left, right;
    TreeNode(int v) { val = v; }
}

Binary tree traversals

Recognize it from

Print nodes left-to-right as in a BST. Walk a tree “level by level”. Zigzag / spiral level order. “Return values grouped by depth.” Anything that needs children of the whole current layer before the next layer.

What it actually does

Inorder is a recursive DFS: the call stack holds the path from the root down to the node you are about to visit. You fully finish the left subtree, visit the node, then fully finish the right subtree. There is no explicit Deque — Java’s stack frames are the stack. On a BST that visit order is sorted.

Level-order is BFS. You put the root in a queue. Each loop iteration peels off the current layer (q.size() at the start of the layer) and offers that node’s children. Trees have no cycles, so you do not need a visited set; a child is offered exactly once, from its parent. Mark-on-enqueue here just means “offer as soon as you see the child,” the same habit as graph BFS.

Simple — read this first

List<Integer> inorder(TreeNode root) {
    List<Integer> out = new ArrayList<>();
    walk(root, out);
    return out;
}

void walk(TreeNode n, List<Integer> out) {
    if (n == null) return;                    // empty child: this frame is done
    walk(n.left, out);                        // whole left subtree sits deeper on the call stack
    out.add(n.val);                           // visit only after left has returned
    walk(n.right, out);                       // right runs after this value is recorded
}

Interview — type this

List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> out = new ArrayList<>();
    if (root == null) return out;             // empty tree: zero levels, not one empty level
    Deque<TreeNode> q = new ArrayDeque<>();
    q.offer(root);                            // enqueue as soon as discovered; tree cannot re-visit
    while (!q.isEmpty()) {
        int sz = q.size();                    // freeze layer width; later offers belong to the next layer
        List<Integer> level = new ArrayList<>();
        for (int i = 0; i < sz; i++) {
            TreeNode n = q.poll();
            level.add(n.val);
            if (n.left != null) q.offer(n.left);
            if (n.right != null) q.offer(n.right);
        }
        out.add(level);
    }
    return out;
}
Example 1 — simple

Three nodes. Inorder should read like a sorted BST. Level-order should print the root alone, then both children.

2 / \ 1 3
Inorder on the 3-node tree
CallWhat happensout
walk(2)go left[]
walk(1)left is null, visit 1, right is null, return[1]
walk(2) resumesvisit 2, go right[1, 2]
walk(3)left null, visit 3, right null, return[1, 2, 3]
walk(2) returnsdone[1, 2, 3]

Inorder answer: 1, 2, 3. Level-order: queue starts [2] → layer [2] then offers 1 and 3 → layer [1, 3]. Answer: [[2], [1, 3]].

Example 2 — every step

Seven nodes. Every recursive call and every queue poll is in the tables. No skipped visits.

1 / \ 2 3 / \ / \ 4 5 6 7
Inorder — call stack down and returns up (left = deeper)
StackEventout after
1enter 1, call left[]
1 · 2enter 2, call left[]
1 · 2 · 4enter 4, left null[]
1 · 2 · 4visit 4, right null, return[4]
1 · 2visit 2, call right[4, 2]
1 · 2 · 5enter 5, left null[4, 2]
1 · 2 · 5visit 5, right null, return[4, 2, 5]
1 · 2right done, return[4, 2, 5]
1visit 1, call right[4, 2, 5, 1]
1 · 3enter 3, call left[4, 2, 5, 1]
1 · 3 · 6enter 6, left null, visit 6, right null, return[4, 2, 5, 1, 6]
1 · 3visit 3, call right[4, 2, 5, 1, 6, 3]
1 · 3 · 7enter 7, left null, visit 7, right null, return[4, 2, 5, 1, 6, 3, 7]
1 · 3right done, return[4, 2, 5, 1, 6, 3, 7]
1right done, return[4, 2, 5, 1, 6, 3, 7]

Inorder answer: 4, 2, 5, 1, 6, 3, 7.

Level-order BFS — every poll and offer
Layer szQueue (front → back)PollOfferLevel list
—[1]—root—
1[1]12, 3[1]
2[2, 3]24, 5[2]
2[3, 4, 5]36, 7[2, 3]
4[4, 5, 6, 7]4—[4]
4[5, 6, 7]5—[4, 5]
4[6, 7]6—[4, 5, 6]
4[7]7—[4, 5, 6, 7]
—[] empty, stop——[[1], [2, 3], [4, 5, 6, 7]]

Level-order answer: [[1], [2, 3], [4, 5, 6, 7]].

Time O(n) Space inorder O(h) Space BFS O(w)
Why this DS Inorder needs a stack of ancestors — the call stack is enough. Level-order needs the whole current layer in memory at once, so an explicit queue. Width w can be Θ(n) on a complete tree. An explicit stack inorder is the same algorithm as recursion; do not mix “visit then push children” (that is preorder) with inorder’s “push left spine, then pop and go right.”
Common mistakes
  • Capturing q.size() inside the inner loop so new children inflate the current layer.
  • Returning one empty list for a null root instead of an empty list of levels.
  • Writing inorder as visit-then-left — that is preorder. BST sorted order only happens if left finishes first.
ProblemPatternLinks
Distance K (BFS from target) tree BFS after you can walk to parent Java in ch.05 · cheat card · clue · R4 coding
Zigzag / level-order variants same queue; reverse odd layers if asked cheat card
Path-sum shape (DFS walk) same call-stack walk as inorder, different visit work cheat card · clue

BST search and insert

Recognize it from

“Left subtree < node < right subtree.” Search, insert, or validate a binary search tree. “Floor / ceil in a BST.” Convert sorted list / array into a BST. Anything that should be a compare-and-walk, not a full scan.

What it actually does

At each node you compare the key with n.val. Strictly smaller → go left. Strictly larger → go right. Equal → found. If you walk off a null child, the key is absent. Insert is the same walk; the null you would have fallen off becomes a new node, wired onto the parent that just compared.

Use Integer.compare so you never write v - n.val (that overflows). Duplicates: pick a policy and keep it — this page ignores equals on insert (no new node). Search still returns the first equal node.

Simple — read this first

TreeNode search(TreeNode n, int key) {
    if (n == null) return null;               // walked off a leaf: key is not here
    if (key == n.val) return n;
    if (key < n.val) return search(n.left, key);
    return search(n.right, key);
}

Interview — type this

TreeNode search(TreeNode n, int key) {
    while (n != null) {
        int c = Integer.compare(key, n.val);  // never key - n.val
        if (c == 0) return n;
        n = (c < 0) ? n.left : n.right;
    }
    return null;
}

TreeNode insert(TreeNode n, int key) {
    if (n == null) return new TreeNode(key);  // this null slot is the insertion point
    int c = Integer.compare(key, n.val);
    if (c < 0) n.left = insert(n.left, key);
    else if (c > 0) n.right = insert(n.right, key);
    // c == 0: keep the existing node (duplicate policy)
    return n;                                 // parent rebinds the child pointer
}
Example 1 — simple

Search 4 in a 5-node BST.

5 / \ 3 8 / \ 1 4
search(root, 4)
NodeInteger.compare(4, val)Go
54 < 5 → negativeleft
34 > 3 → positiveright
40found node 4

Answer: the node whose value is 4.

Example 2 — every step

Failed search that goes left, then right, then right. Key = 8.

10 / \ 4 14 \ 7 / 6
search(root, 8) — every comparison, including the null
NodeCompare 8 vs valNext pointer
108 < 10left → 4
48 > 4right → 7
78 > 7right → null (7 has no right child)
null—return null: 8 is not in the tree

Answer: not found. Insert of 8 would create a new node as 7.right — that is the same walk, replacing the final null with a node.

Time O(h) Space recursive O(h) Space iterative search O(1)
Why this DS The BST invariant lets you discard one subtree per compare, so you never scan all n nodes unless the tree is a stick (h = n). A HashMap would search in expected O(1) but would not give you ordered inorder, floor/ceil, or range queries. Balance (AVL / red-black) is how h stays log n — this card is the walk, not the rotation.
Common mistakes
  • Inserting without n.left = insert(...) / n.right = insert(...) — the new node is created and thrown away.
  • Using key - n.val for compare; Integer.MIN_VALUE minus a positive overflows.
  • Assuming a “BST” that is only a heap-shaped binary tree. Heap order is parent vs children, not left vs right.
ProblemPatternLinks
Zigzag + BST checks inorder increasing, or bound-walk validate cheat card
Linked list ↔ BST / flatten inorder wiring, not search cheat card
Equal-sum split of a tree subtree totals on a BST or binary tree cheat card

Tree DP — max path that can bend

Recognize it from

“Maximum path sum” where a path may start and end at any nodes (not only root-to-leaf). “Path that bends through a node.” Diameter (longest path, counting edges or nodes). “Gain you can still hand to your parent” vs “best path seen anywhere.”

What it actually does

Each node computes two different numbers. The return value is the best chain that still has a free end at this node, so the parent can extend it: n.val + max(leftGain, rightGain). The global best may bend: leftGain + node + rightGain, using both children. That bent path cannot be returned upward — the parent would fork.

A negative child cannot help any path that includes it, so both versions clamp that child gain to 0 (drop the child). That is the any-node variant. Leaf-to-leaf is a different spec: you must not clamp, because a leaf path cannot drop a child. This card is the any-node / bend version, matching the Live Code shape in the notebook’s related card.

Simple — read this first

int best = Integer.MIN_VALUE;

int maxPathSum(TreeNode root) {
    best = Integer.MIN_VALUE;
    gain(root);
    return best;
}

int gain(TreeNode n) {
    if (n == null) return 0;
    int L = gain(n.left);
    int R = gain(n.right);
    if (L < 0) L = 0;                         // negative child cannot improve a path
    if (R < 0) R = 0;
    int bend = L + n.val + R;                 // path that uses both sides, ends here
    if (bend > best) best = bend;             // record even if we do not return the bend
    return n.val + Math.max(L, R);            // parent may extend only one side
}

Interview — type this

class MaxPathAnyNode {
    private int best = Integer.MIN_VALUE;

    public int maxPathSum(TreeNode root) {
        best = Integer.MIN_VALUE;
        gain(root);
        return best;
    }

    private int gain(TreeNode n) {
        if (n == null) return 0;
        int L = Math.max(0, gain(n.left));    // may drop a negative branch
        int R = Math.max(0, gain(n.right));
        best = Math.max(best, L + n.val + R);
        return n.val + Math.max(L, R);
    }
}
Example 1 — simple

Three nodes, all positive. The bend through the root is the whole tree.

1 / \ 2 3
gain() post-order, best starts at Integer.MIN_VALUE
CallL (clamped)R (clamped)bend / bestReturn
gain(2)002 → best = 22
gain(3)003 → best = 33
gain(1)232+1+3 = 6 → best = 61+max(2,3) = 4

Answer: 6 (path 2–1–3). The value returned from the root (4) is not the answer; best is.

Example 2 — every step

The best path does not go through the root. Watch best jump to 42 at node 20, then refuse to be beaten by the root’s bend.

-10 / \ 9 20 / \ 15 7
Every gain() call, including null children, and the global best
CallLRbendbest afterReturn to parent
gain(9) left/right null00999
gain(15) leaves00151515
gain(7) leaves00715 (7 does not win)7
gain(20)max(0,15)=15max(0,7)=715+20+7 = 424220+15 = 35
gain(-10)max(0,9)=9max(0,35)=359+(-10)+35 = 3442 (34 loses)-10+35 = 25

Answer: 42, path 15–20–7. Root’s own bend is 34. Returning 25 from the root would be wrong if you treated the return value as the answer.

Time O(n) Space O(h)
Why this DS One post-order DFS is enough: a node only needs the two child gains. Extra arrays of “dp[node]” are the same numbers with more wiring. You need a global because the best bend may sit in a subtree the root never takes. Diameter is the same skeleton with 1 + leftHeight + rightHeight instead of sums.
Common mistakes
  • Returning the bend to the parent. That forks the path.
  • Clamping when the spec is leaf-to-leaf. A leaf path cannot drop a negative child.
  • Initializing best = 0. A tree of all negatives should return the largest single node, which is negative.
  • Forgetting to reset best if the method is called twice on one instance.
ProblemPatternLinks
Max path between leaves same post-order; do not clamp Java in ch.05 · cheat card
Tree path A/B/C family DFS path state, not necessarily a bend sum Java in ch.05 · cheat card · R4 coding
Most frequent subtree sum post-order return a number; map counts Java in ch.05 · cheat card
Diameter variants bend = heights, not values cheat card · clue
Path-sum like root-to-leaf or down-only, not a bend cheat card · clue

Lowest common ancestor

Recognize it from

“Lowest common ancestor of p and q.” Distance between two nodes (path = depth(p)+depth(q)−2·depth(lca)). Kth ancestor. “First node that sits on both paths to the root.”

What it actually does

Simple version: BFS (or DFS) to record each node’s parent, walk from p to the root collecting a set, then walk from q until you hit that set. The first hit is the LCA. That uses extra maps.

Interview version: one DFS. If the current node is null, p, or q, return it. Recurse left and right. If both sides return non-null, p and q split here — this node is the LCA. If only one side is non-null, bubble that answer up (the other subtree did not contain either node). Null returns mean “neither p nor q lives under me.” Both nodes are assumed present.

Simple — read this first

TreeNode lca(TreeNode root, TreeNode p, TreeNode q) {
    Map<TreeNode, TreeNode> par = new HashMap<>();
    Deque<TreeNode> dq = new ArrayDeque<>();
    par.put(root, null);
    dq.offer(root);                           // parent-map BFS; offer once from parent
    while (!par.containsKey(p) || !par.containsKey(q)) {
        TreeNode n = dq.poll();
        if (n.left != null) {
            par.put(n.left, n);
            dq.offer(n.left);
        }
        if (n.right != null) {
            par.put(n.right, n);
            dq.offer(n.right);
        }
    }
    Set<TreeNode> seen = new HashSet<>();
    while (p != null) { seen.add(p); p = par.get(p); }
    while (!seen.contains(q)) q = par.get(q);
    return q;
}

Interview — type this

TreeNode lca(TreeNode n, TreeNode p, TreeNode q) {
    if (n == null || n == p || n == q) return n;
    TreeNode L = lca(n.left, p, q);
    TreeNode R = lca(n.right, p, q);
    if (L != null && R != null) return n;     // p and q came from different sides
    return (L != null) ? L : R;               // bubble the non-null side, or null
}
Example 1 — simple

p and q are siblings. LCA is their parent.

3 / \ 5 1 p = 5, q = 1
Recursive LCA — siblings
CallBase?LRReturn
lca(5)n == p——5
lca(1)n == q——1
lca(3)no51both non-null → 3

Answer: node 3.

Example 2 — every step

p in the left subtree, q in the right. Null returns bubble up through a node that contains neither.

6 / \ 2 8 / \ \ 0 4 9 p = 0, q = 9
Every call, including null children (post-order returns)
CallL returnR returnThis returns
lca(null) under 0——null
lca(0)——0 (n == p), children not explored after the base case
lca(4)lca(null)=nulllca(null)=nullnull (4 is not p or q)
lca(2)0null0 (only left hit)
lca(9)——9 (n == q)
lca(8)null (no left)99
lca(6)09both non-null → 6

Answer: node 6. The interesting null is lca(4): that whole subtree reports “not here,” so 2 bubbles p upward until 6 sees q from the other side.

Time O(n) Space recursive O(h) Space parent-map O(n)
Why this DS Parent map is obvious and matches Distance K (you already needed parents). The split recursion uses the call stack as the path to the root and needs no extra map. Binary-lifting kth-ancestor is the same idea with a sparse table when you have many queries; one-shot LCA does not need it.
Common mistakes
  • Comparing values (n.val == p.val) when the tree can have duplicates — compare node references, or confirm unique values.
  • Stopping at the first of p or q without searching the other side. If q sits under p, p is the LCA; the base case n == p handles that. If you keep walking anyway you still return p, but you must not return null from a subtree that contains p.
  • Using BST LCA “go where they split on value” on a tree that is not a BST.
ProblemPatternLinks
Distance between two nodes depth(p)+depth(q)−2·depth(lca) Java in ch.05 · cheat card
Kth ancestor parent pointers / lifting, same spine as LCA Java in ch.05 · cheat card
Distance K parent map, then BFS Java in ch.05 · cheat card · clue · clue · R4 coding

Size-k min-heap — kth largest

Recognize it from

“Kth largest in a stream.” Top-k frequent. Merge k sorted lists (heap of heads). Connect ropes / combine garlands. Median of a stream (two heaps). “Keep only k candidates.”

What it actually does

A min-heap of size k stores the k largest values seen so far, with the smallest of those k on top. That top is the current kth largest. When a new value arrives, offer it; if size exceeds k, poll — you throw away the smallest, which cannot be among the k largest anymore.

Java’s PriorityQueue is a min-heap by default. For kth smallest you would invert to a max-heap of size k. Merge-k and ropes are the same DS: always pull the current minimum in O(log k) or O(log n).

Simple — read this first

int kthLargest(int[] a, int k) {
    Arrays.sort(a);                           // idea: order everything, pick from the right
    return a[a.length - k];                   // k=1 → last element = largest
}

Interview — type this

class KthLargest {
    private final int k;
    private final PriorityQueue<Integer> min;

    KthLargest(int k, int[] nums) {
        this.k = k;
        min = new PriorityQueue<>();          // min-heap; peek = smallest of the k largest
        for (int x : nums) add(x);
    }

    int add(int val) {
        min.offer(val);
        if (min.size() > k) min.poll();       // drop the value that can no longer be kth
        return min.peek();                    // stream query: kth among all adds so far
    }
}
Example 1 — simple

Stream of four numbers, k = 2. Heap shown with the minimum on the left.

k = 2, stream 4, 1, 7, 3
addoffersize > 2?pollheappeek (2nd largest)
44no (1)—[4]4
11no (2)—[1, 4]1
77yes (3)1[4, 7]4
33yes (3)3[4, 7]4

Answer after the stream: 4 (the two largest are 7 and 4). After only two numbers the peek is 1 — that is honest: with two values the 2nd largest is the min of them.

Example 2 — every step

Every offer and poll for [3, 2, 1, 5, 6, 4], k = 2.

KthLargest(2, nums) constructor, then the array is already consumed — shown as successive add
addheap beforeoffer → heapsizepollheap afterpeek
3[][3]1—[3]3
2[3][2, 3]2—[2, 3]2
1[2, 3][1, 3, 2]31[2, 3]2
5[2, 3][2, 3, 5]32[3, 5]3
6[3, 5][3, 5, 6]33[5, 6]5
4[5, 6][4, 6, 5]34[5, 6]5

Answer: 5 (largest = 6, 2nd = 5). Sorted unique view: 1,2,3,4,5,6 — index n−k agrees with the heap peek.

Time per add O(log k) n adds O(n log k) Space O(k)
Why this DS Sorting every query is O(n log n) per add. A size-k heap never stores the discarded small values. Quickselect is average O(n) for a static array but is awkward on a stream and worst-case quadratic unless you shuffle. Merge k lists: heap of k heads, not size-k of all nodes.
Common mistakes
  • Max-heap for kth largest — peek would be the largest, not the kth. You want the minimum of the k largest.
  • Polling when size is still ≤ k, or forgetting to poll when size is k+1.
  • Comparing heap entries with subtraction instead of Integer.compare when the payload is a node / pair.
  • Using a heap of size n when k is tiny — that is just sorting.
ProblemPatternLinks
Dynamic kth largest this exact size-k min-heap stream Java in ch.05 · cheat card · clue · R4 coding
Top-k frequent count map, then size-k heap of entries Java in ch.05 · cheat card · clue · clue
Merge k lists min-heap of k heads Java in ch.05 · cheat card · clue
Combine garlands / ropes always merge two smallest Java in ch.05 · cheat card · clue · R4 coding
Median stream two heaps, not size-k cheat card · clue

Trie insert and search

Recognize it from

Autocomplete. “Does this word exist, vs is it only a prefix.” Word dictionary with add/search. Replace words with a shortest root. Search-suggestions as you type. Anything that shares string prefixes.

What it actually does

A trie is a tree whose edges are characters. Insert walks the word, creating a child node when the letter is missing, then marks end = true on the last node. Search walks the same letters and succeeds only if every hop exists and that last node is an end. If you land on a node with end = false, you found a prefix of some inserted word, not the word itself.

Simple version stores children in a HashMap (any character). Interview version uses Node[26] for lowercase a–z and a shared walk helper so search and startsWith differ by one boolean.

Simple — read this first

class Trie {
    Map<Character, Trie> kids = new HashMap<>();
    boolean end;

    void insert(String w) {
        Trie n = this;
        for (int i = 0; i < w.length(); i++) {
            char c = w.charAt(i);
            n.kids.putIfAbsent(c, new Trie()); // create the hop if this prefix is new
            n = n.kids.get(c);
        }
        n.end = true;                          // word ends here; prefixes stay end=false
    }

    boolean search(String w) {
        Trie n = this;
        for (int i = 0; i < w.length(); i++) {
            n = n.kids.get(w.charAt(i));
            if (n == null) return false;       // missing letter: not in the trie
        }
        return n.end;
    }
}

Interview — type this

class Trie {
    static class Node {
        Node[] ch = new Node[26];
        boolean end;
    }
    private final Node root = new Node();

    void insert(String w) {
        Node n = root;
        for (int i = 0; i < w.length(); i++) {
            int x = w.charAt(i) - 'a';
            if (n.ch[x] == null) n.ch[x] = new Node();
            n = n.ch[x];
        }
        n.end = true;
    }

    boolean search(String w) {
        Node n = walk(w);
        return n != null && n.end;            // must be a marked word, not only a prefix
    }

    boolean startsWith(String p) {
        return walk(p) != null;               // landing on any node is enough
    }

    private Node walk(String s) {
        Node n = root;
        for (int i = 0; i < s.length(); i++) {
            int x = s.charAt(i) - 'a';
            if (n.ch[x] == null) return null;
            n = n.ch[x];
        }
        return n;
    }
}
Example 1 — simple

Insert "a" then "app".

root └─ a end = true (word "a") └─ p end = false └─ p end = true (word "app")
Two inserts from an empty trie
InsertHopsNew nodesMark end
"a"root → acreate aa.end = true
"app"root → a (exists) → p → pcreate two p nodeslast p.end = true; a.end stays true

After both inserts, search("a") is true and search("ap") is false: the middle p is only a prefix.

Example 2 — every step

Insert "app", "apple", "apt". Then search "ap", "app", "apex" — every node hop.

root └─ a └─ p ├─ p end = true "app" │ └─ l │ └─ e end = true "apple" └─ t end = true "apt"
Inserts — every hop
InserticharFromChild exists?Land
app0arootno → createnode a
app1pano → createnode p1
app2pp1no → createnode p2, end=true
apple0arootyesnode a
apple1payesnode p1
apple2pp1yesnode p2
apple3lp2no → createnode l
apple4elno → createnode e, end=true
apt0arootyesnode a
apt1payesnode p1
apt2tp1no → createnode t, end=true
Searches — every hop
QueryHopsStuck?end on landing nodesearchstartsWith
aproot → a → p1nofalse (p1 is not a word)falsetrue
approot → a → p1 → p2notruetruetrue
apexroot → a → p1, next letter ep1 has no child e (children are p and t)—falsefalse

Answers: search("ap") = false, search("app") = true, search("apex") = false. The first is the prefix trap; the third is a missing hop.

Insert/search O(L) Space O(total letters)
Why this DS A HashSet search is O(L) too, but it cannot answer “all words with this prefix” without scanning every word. The trie pays memory for shared prefixes so that walk is one path. Array[26] beats a map when the alphabet is tiny; a map wins for unicode.
Common mistakes
  • Returning true from search when the walk succeeded but end is false ("ap" after inserting "app").
  • Marking every node along the way as an end — then every prefix becomes a word.
  • Reusing one static node array across words without creating missing children.
  • Indexing c - 'a' on uppercase or non-letters without a guard.
ProblemPatternLinks
No named-IE trie card in this notebook same hop as insert/search above Java in ch.05 (chapter, no trie fragment) · cheat card · clue
See also — cards several algos share Distance K needs both a parent map (LCA card) and BFS (traversal card). Tree path A/B/C and diameter sit next to max-path: same post-order, different payload. Merge k lists is a heap of heads, not a size-k stream — still this chapter’s heap. None of those hashes is a Job 10454435 live question.