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
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.
| Call | What happens | out |
|---|---|---|
| walk(2) | go left | [] |
| walk(1) | left is null, visit 1, right is null, return | [1] |
| walk(2) resumes | visit 2, go right | [1, 2] |
| walk(3) | left null, visit 3, right null, return | [1, 2, 3] |
| walk(2) returns | done | [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.
| Stack | Event | out after |
|---|---|---|
| 1 | enter 1, call left | [] |
| 1 · 2 | enter 2, call left | [] |
| 1 · 2 · 4 | enter 4, left null | [] |
| 1 · 2 · 4 | visit 4, right null, return | [4] |
| 1 · 2 | visit 2, call right | [4, 2] |
| 1 · 2 · 5 | enter 5, left null | [4, 2] |
| 1 · 2 · 5 | visit 5, right null, return | [4, 2, 5] |
| 1 · 2 | right done, return | [4, 2, 5] |
| 1 | visit 1, call right | [4, 2, 5, 1] |
| 1 · 3 | enter 3, call left | [4, 2, 5, 1] |
| 1 · 3 · 6 | enter 6, left null, visit 6, right null, return | [4, 2, 5, 1, 6] |
| 1 · 3 | visit 3, call right | [4, 2, 5, 1, 6, 3] |
| 1 · 3 · 7 | enter 7, left null, visit 7, right null, return | [4, 2, 5, 1, 6, 3, 7] |
| 1 · 3 | right done, return | [4, 2, 5, 1, 6, 3, 7] |
| 1 | right done, return | [4, 2, 5, 1, 6, 3, 7] |
Inorder answer: 4, 2, 5, 1, 6, 3, 7.
| Layer sz | Queue (front → back) | Poll | Offer | Level list |
|---|---|---|---|---|
| — | [1] | — | root | — |
| 1 | [1] | 1 | 2, 3 | [1] |
| 2 | [2, 3] | 2 | 4, 5 | [2] |
| 2 | [3, 4, 5] | 3 | 6, 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]].
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.”
- 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.
| Problem | Pattern | Links |
|---|---|---|
| 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.
| Node | Integer.compare(4, val) | Go |
|---|---|---|
| 5 | 4 < 5 → negative | left |
| 3 | 4 > 3 → positive | right |
| 4 | 0 | found node 4 |
Answer: the node whose value is 4.
Example 2 — every step
Failed search that goes left, then right, then right. Key = 8.
| Node | Compare 8 vs val | Next pointer |
|---|---|---|
| 10 | 8 < 10 | left → 4 |
| 4 | 8 > 4 | right → 7 |
| 7 | 8 > 7 | right → 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.
- Inserting without
n.left = insert(...)/n.right = insert(...)— the new node is created and thrown away. - Using
key - n.valfor compare;Integer.MIN_VALUEminus a positive overflows. - Assuming a “BST” that is only a heap-shaped binary tree. Heap order is parent vs children, not left vs right.
| Problem | Pattern | Links |
|---|---|---|
| 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.
| Call | L (clamped) | R (clamped) | bend / best | Return |
|---|---|---|---|---|
| gain(2) | 0 | 0 | 2 → best = 2 | 2 |
| gain(3) | 0 | 0 | 3 → best = 3 | 3 |
| gain(1) | 2 | 3 | 2+1+3 = 6 → best = 6 | 1+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.
| Call | L | R | bend | best after | Return to parent |
|---|---|---|---|---|---|
| gain(9) left/right null | 0 | 0 | 9 | 9 | 9 |
| gain(15) leaves | 0 | 0 | 15 | 15 | 15 |
| gain(7) leaves | 0 | 0 | 7 | 15 (7 does not win) | 7 |
| gain(20) | max(0,15)=15 | max(0,7)=7 | 15+20+7 = 42 | 42 | 20+15 = 35 |
| gain(-10) | max(0,9)=9 | max(0,35)=35 | 9+(-10)+35 = 34 | 42 (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.
- 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
bestif the method is called twice on one instance.
| Problem | Pattern | Links |
|---|---|---|
| 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.
| Call | Base? | L | R | Return |
|---|---|---|---|---|
| lca(5) | n == p | — | — | 5 |
| lca(1) | n == q | — | — | 1 |
| lca(3) | no | 5 | 1 | both 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.
| Call | L return | R return | This returns |
|---|---|---|---|
| lca(null) under 0 | — | — | null |
| lca(0) | — | — | 0 (n == p), children not explored after the base case |
| lca(4) | lca(null)=null | lca(null)=null | null (4 is not p or q) |
| lca(2) | 0 | null | 0 (only left hit) |
| lca(9) | — | — | 9 (n == q) |
| lca(8) | null (no left) | 9 | 9 |
| lca(6) | 0 | 9 | both 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.
- 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 == phandles 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.
| Problem | Pattern | Links |
|---|---|---|
| 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.
| add | offer | size > 2? | poll | heap | peek (2nd largest) |
|---|---|---|---|---|---|
| 4 | 4 | no (1) | — | [4] | 4 |
| 1 | 1 | no (2) | — | [1, 4] | 1 |
| 7 | 7 | yes (3) | 1 | [4, 7] | 4 |
| 3 | 3 | yes (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.
| add | heap before | offer → heap | size | poll | heap after | peek |
|---|---|---|---|---|---|---|
| 3 | [] | [3] | 1 | — | [3] | 3 |
| 2 | [3] | [2, 3] | 2 | — | [2, 3] | 2 |
| 1 | [2, 3] | [1, 3, 2] | 3 | 1 | [2, 3] | 2 |
| 5 | [2, 3] | [2, 3, 5] | 3 | 2 | [3, 5] | 3 |
| 6 | [3, 5] | [3, 5, 6] | 3 | 3 | [5, 6] | 5 |
| 4 | [5, 6] | [4, 6, 5] | 3 | 4 | [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.
- 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.comparewhen the payload is a node / pair. - Using a heap of size n when k is tiny — that is just sorting.
| Problem | Pattern | Links |
|---|---|---|
| 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".
| Insert | Hops | New nodes | Mark end |
|---|---|---|---|
| "a" | root → a | create a | a.end = true |
| "app" | root → a (exists) → p → p | create two p nodes | last 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.
| Insert | i | char | From | Child exists? | Land |
|---|---|---|---|---|---|
| app | 0 | a | root | no → create | node a |
| app | 1 | p | a | no → create | node p1 |
| app | 2 | p | p1 | no → create | node p2, end=true |
| apple | 0 | a | root | yes | node a |
| apple | 1 | p | a | yes | node p1 |
| apple | 2 | p | p1 | yes | node p2 |
| apple | 3 | l | p2 | no → create | node l |
| apple | 4 | e | l | no → create | node e, end=true |
| apt | 0 | a | root | yes | node a |
| apt | 1 | p | a | yes | node p1 |
| apt | 2 | t | p1 | no → create | node t, end=true |
| Query | Hops | Stuck? | end on landing node | search | startsWith |
|---|---|---|---|---|---|
| ap | root → a → p1 | no | false (p1 is not a word) | false | true |
| app | root → a → p1 → p2 | no | true | true | true |
| apex | root → a → p1, next letter e | p1 has no child e (children are p and t) | — | false | false |
Answers: search("ap") = false, search("app") = true, search("apex") = false. The first is the prefix trap; the third is a missing hop.
- Returning true from search when the walk succeeded but
endis 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.
| Problem | Pattern | Links |
|---|---|---|
| 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 |