Master this topic with zero to advance depth.
Invert Binary Tree
Given the root of a binary tree, invert the tree, and return its root.
Examples
Intuition
To invert a binary tree, we swap the left and right children of every node. This can be done recursively: swap children of the current node, then recurse into both subtrees.
Detailed Dry Run
root = [4,2,7] -> Swap(2,7) -> [4,7,2]. Recurse into subtrees.
Intuition
Process the tree level by level. Use a queue to store nodes. For each node, swap its children and add them to the queue if they exist.
Detailed Dry Run
Queue: [4]. Pop 4, swap(2,7), Queue: [7,2]. Pop 7, swap(6,9)...
Intuition
Similar to BFS, but use a stack. The order of traversal doesn't change the fact that every node's children need to be swapped.
(1) Swap children of Root (2) Result after subtrees
4 4
/ \ / \
2 7 7 2
/ \ / \ / \ / \
1 3 6 9 9 6 3 1Detailed Dry Run
Stack: [4]. Pop 4, swap(2,7), Push(2,7). Pop 7, swap(6,9)... mirrored tree.
Maximum Depth of Binary Tree
Given the root of a binary tree, return its maximum depth.
Examples
Intuition
Max depth is . This naturally fits recursion.
Detailed Dry Run
root(3) -> 1 + max(depth(9), depth(20)). depth(9)=1, depth(20)=2. Result = 3.
Intuition
Traverse the tree level by level. Every completed level increases the depth.
Level 1: [3] -> Depth 1
Level 2: [9, 20] -> Depth 2
Level 3: [15, 7] -> Depth 3Detailed Dry Run
Queue: [3]. size=1. Pop 3, push 9,20. depth=1. Queue: [9,20]. size=2. Pop both, push 15,7. depth=2. Final depth=3.
Intuition
Manually manage a stack to mimic recursion. Store pairs of (node, depth) to keep track of max depth.
Detailed Dry Run
Stack: [(3,1)]. Pop (3,1), Max=1. Push (20,2), (9,2). Pop (9,2), Max=2. Pop (20,2), Push(7,3), (15,3). Pop (15,3), Max=3.
Same Tree
Given roots of two trees p and q, check if they are the same.
Examples
Intuition
Two trees are identical if their roots match and their respective subtrees are identical.
(1) p.val == q.val?
(2) isSameTree(p.left, q.left)
(3) isSameTree(p.right, q.right)Detailed Dry Run
p(1), q(1) -> match. Recurse(2,2) -> match. Recurse(3,3) -> match. Result = true.
Intuition
Use a queue to process pairs of nodes from both trees. If any pair differs, trees are not identical.
Detailed Dry Run
Queue: [(p, q)]. Pop, compare. Push children pairs. Any mismatch -> false.
Symmetric Tree
Check if a tree is a mirror of itself.
Examples
Intuition
A tree is symmetric if its left and right subtrees are mirrors of each other. Two trees t1 and t2 are mirrors if:
t1.left is a mirror of t2.right.t1.right is a mirror of t2.left.Detailed Dry Run
Mirror(L, R) -> L.val == R.val && Mirror(L.left, R.right) && Mirror(L.right, R.left).
Intuition
Use a queue to process nodes in pairs. Each pair contains nodes that should be mirrors of each other. For a root, we push (root.left, root.right).
(1) Queue: [1.L, 1.R] -> [2, 2]
(2) Pop pair (2, 2), Push pairs: [(2.L, 2.R), (2.R, 2.L)] -> [3, 3, 4, 4]Detailed Dry Run
Queue: [2, 2]. Pop 2, 2. Push 3(L), 3(R) and 4(R), 4(L). Queue: [3, 3, 4, 4]. All match -> true.
Intuition
Process the tree in parallel using two stacks. One stack follows Left -> Right and the other follows Right -> Left. Their values and structure must match at every step.
Detailed Dry Run
Stack1: [L], Stack2: [R]. Pop both, compare. Push L1.left, R1.right and L1.right, R1.left to respective stacks.
Subtree of Another Tree
Check if a tree subRoot is a subtree of root.
Examples
Intuition
A tree s is a subtree of t if:
s and t are identical.s is a subtree of t.left.s is a subtree of t.right.Detailed Dry Run
root(3) != sub(4). check root(4) == sub(4) -> YES (IsSameTree).
Intuition
Serialize both trees into strings using preorder traversal. Add special markers for null nodes (e.g., '#') and separate values to avoid ambiguity (e.g., ','). If the serialized subRoot is a substring of serialized root, it's a subtree.
Tree: 3(4,5) -> Preorder: ",3,4,#,#,5,#,#"
Subtree: 4 -> Preorder: ",4,#,#"
Match found in ",3,4,#,#,5,#,#"!Detailed Dry Run
Serialize root: ^3,4,1,#,#,2,#,#,5,#,#. Serialize sub: ^4,1,#,#,2,#,#. Sub exists in Root -> True.
Diameter of Binary Tree
Find the length of the longest path between any two nodes in a tree.
Examples
Intuition
The diameter of a tree at node is the maximum of:
Detailed Dry Run
Node(2): Left height=1, Right height=1. Diameter = 2. Max = 2. Return height=2.
Intuition
Instead of using a global variable, each recursive call returns a pair: (height, diameter_so_far). This is a cleaner functional approach.
Node(2) returns {Height: 2, Diameter: 2}
Node(3) returns {Height: 1, Diameter: 0}
Root(1) calc: max(2.H+3.H, 2.D, 3.D) -> max(3, 2, 0) = 3.Detailed Dry Run
DFS returns {h, d}. For null: {0, 0}. For leaf: {1, 0}. For 2: l={1,0}, r={1,0} -> {2, 2}. Final root returns {max_h, max_d}.
Intuition
We can solve this iteratively by performing a Post-order traversal using a stack. We keep a Map to store the heights of nodes as we finish processing them. For each node, diameter = height(left) + height(right).
Map<TreeNode, Integer>.height = 1 + max(h_left, h_right).diameter = h_left + h_right.Detailed Dry Run
Stack processes leaves first, populating Map(leaf) = 1. Then Parent(2) sees l=1, r=1, Map(2)=2, MaxD=2. Root(1) sees Map(2)=2, Map(3)=1, MaxD=max(2, 3)=3.
Balanced Binary Tree
Check if a tree is height-balanced.
Examples
Intuition
For each node, compute the height of its left and right subtrees. If the difference is , it's not balanced. Then recurse for both children.
Detailed Dry Run
root(3): H(L)=1, H(R)=2. Diff=1. OK. Recurse(9) and Recurse(20).
Intuition
To optimize the approach, we can cache the height of each node in a HashMap. This ensures that we only calculate the height of each node once, bringing the complexity down to .
node -> height.height(node), return map value if present.Detailed Dry Run
Map: {9:1, 15:1, 7:1, 20:2, 3:3}. Heights are fetched from Map in O(1) during balance check.
Intuition
Compute heights bottom-up. If any subtree is unbalanced, return -1 to signal failure to parents immediately.
Detailed Dry Run
Node(20): L=1, R=1 -> height=2. root(3): L=1, R=2 -> height=3. Balanced.
Construct Binary Tree from Preorder and Inorder Traversal
Rebuild tree.
Intuition
To reconstruct a binary tree from preorder and inorder traversals, we use the fact that the first element in preorder is always the root. We then find this root in the inorder traversal. Everything to the left of the root in inorder belongs to the left subtree, and everything to the right belongs to the right subtree.
root = preorder[0].root index in inorder by iterating (Linear Search).inorder into leftInorder and rightInorder.Detailed Dry Run
| Preorder | Inorder | Root | Left Subtree | Right Subtree |
|---|---|---|---|---|
| [3,9,20] | [9,3,20] | 3 | [9] | [20] |
Intuition
We can build the tree iteratively using a stack. The first element of preorder is the root. For subsequent elements, if a node is a left child, it follows its parent in preorder. If it's a right child, it follows some ancestor's left subtree completion.
stack.top().val != inorder[inIndex], it's a left child.stack.top().val != inorder[inIndex] to find the parent whose right child this value is.Detailed Dry Run
Pre:[3,9,20], In:[9,3,20]. Push 3. 3!=9 -> 9 is 3.left. Push 9. 9==9 -> Pop 9, Pop 3. Next is 20. 20 is 3.right.
Intuition
Linear search for the root in the inorder array is the primary bottleneck ( per call). We can optimize this by pre-processing the inorder array into a Hash Map that stores value -> index. This reduces the lookup time to .
Pre: [3, 9, 20, 15, 7]
In: [9, 3, 15, 20, 7]
Map: {9:0, 3:1, 15:2, 20:3, 7:4}
Step 1: Root = 3 (Index 1 in Inorder)
Left Inorder: [0..0], Right Inorder: [2..4]Detailed Dry Run
| Call | Range (Start, End) | Root Val | Map Index |
|---|---|---|---|
| Main | (0, 4) | 3 | 1 |
| Left | (0, 0) | 9 | 0 |
| Right | (2, 4) | 20 | 3 |
⚠️ Common Pitfalls & Tips
Ensure preIndex is accessed globally or passed correctly across recursive calls. Using array slicing (like preorder[1:mid+1]) is easier but often in languages like Python due to copy overhead.
Validate Binary Search Tree
Is valid BST?
Intuition
A Binary Search Tree has a unique property: its inorder traversal (Left, Root, Right) produces a strictly increasing sequence of values. We can perform an inorder traversal, store the results in an array, and then check if the array is sorted and contains no duplicates.
node.val in a list.list[i] < list[i+1].Detailed Dry Run
| Node | Left Subtree | Right Subtree | Array |
|---|---|---|---|
| 2 | [1] | [3] | [1, 2, 3] |
| Result | Valid BST! |
Intuition
Using a stack, we can simulate the recursion of an inorder traversal. We push all left children of the current node onto the stack, then pop one, check it, and move to its right child.
Keep a pointer prev to the previously visited node. At each step, if current.val <= prev.val, it's not a valid BST.
Detailed Dry Run
Stack: [2, 1]. Pop 1, prev=1. Pop 2, prev=2 (2>1 OK). Push 3, Pop 3, prev=3 (3>2 OK).
Intuition
Instead of storing the whole tree in an array, we can validate each node as we visit it by passing down an allowable range (min, max). For any node, all values in its left subtree must be less than the node's value, and all values in its right subtree must be greater.
Top-Down DFS: Pass constraints from parent to children.
10 (range: -∞, +∞)
/ \
5 15
/ \ / \
L R L R
(5 range: -∞, 10) (15 range: 10, +∞)Detailed Dry Run
| Node | Range (Min, Max) | Valid? |
|---|---|---|
| 10 | (-∞, +∞) | YES |
| 5 | (-∞, 10) | YES |
| 15 | (10, +∞) | YES |
⚠️ Common Pitfalls & Tips
Be careful with Integer.MIN_VALUE. Use null or Long for the initial bounds to handle nodes that contain the minimum possible integer value.
Binary Tree Level Order Traversal
BFS traversal.
Intuition
We can use recursion to traverse the tree. By passing the current depth (level) as an argument, we can add each node's value to a list corresponding to its level. If the list for a level doesn't exist yet, we create it.
res.helper(node, level).res.size() == level, add a new inner list.res.get(level).add(node.val).level + 1.Detailed Dry Run
| Node | Level | Result (State) |
|---|---|---|
| 3 | 0 | [[3]] |
| 9 | 1 | [[3], [9]] |
| 20 | 1 | [[3], [9, 20]] |
| 15 | 2 | [[3], [9, 20], [15]] |
Intuition
While BFS is the natural choice for level-order, we can simulate it using DFS with depth tracking. We maintain a stack of (node, depth). If the result list size is equal to the current depth, we add a new empty list. Then, we add the node value to the list corresponding to its depth.
depth for each node.res[depth].add(node.val).Detailed Dry Run
| Stack | Depth | res state |
|---|---|---|
| [(3,0)] | 0 | [[3]] |
| [(20,1), (9,1)] | 1 | [[3], [9]] |
| [(20,1)] | 1 | [[3], [9, 20]] |
Intuition
The standard way to perform level-order traversal is Breadth-First Search (BFS) using a Queue. We process the tree level by level. For each level, we record how many nodes are currently in the queue; this count is the number of nodes in that specific level.
Queue: [3] -> Level nodes: [3]
Queue: [9, 20] -> Level nodes: [9, 20]
Queue: [15, 7] -> Level nodes: [15, 7]Detailed Dry Run
| Queue | Level Size | Processing | Result Area |
|---|---|---|---|
| [3] | 1 | Pop 3, Push 9, 20 | [3] |
| [9, 20] | 2 | Pop 9, Pop 20, Push 15, 7 | [9, 20] |
| [15, 7] | 2 | Pop 15, Pop 7 | [15, 7] |
⚠️ Common Pitfalls & Tips
Empty tree checking is vital (if root == null). Forget this and the while loop might run on a null list or throw errors.
Lowest Common Ancestor of a Binary Tree
Find LCA.
Intuition
The Lowest Common Ancestor (LCA) is the node where p and q first reside in different subtrees (one in left, one in right), or the node itself is one of p or q. A brute force approach checks for every node if it contains p and q in its subtrees.
contains(node, target) helper.root, if root is p or q, it might be LCA.contains(root.left, p & q) is true, move to left child.contains(root.right, p & q) is true, move to right child.root is LCA.Detailed Dry Run
| Node | Left contains p, q? | Right contains p, q? | Decision |
|---|---|---|---|
| 3 | no | no | Split! 3 is LCA |
Intuition
We can traverse the tree and store each node's parent in a hash map. Once we've found both nodes p and q, we can backtrack from p to the root, storing all ancestors in a set. Then, we backtrack from q the first ancestor that exists in that set is the LCA.
parentMap until both p and q are found.p in a set.q until an ancestor exists in p's ancestor set.Detailed Dry Run
p=5, q=1. parents={5:3, 1:3, 3:null}. p-ancestors={5, 3}. q's parent 3 is in set -> LCA=3.
Intuition
Instead of checking containment repeatedly, we can do a single bottom-up traversal. We ask each child: "Do you see p or q?".
p or q was found in that subtree.Bottom-Up DFS: Aggregate information from children to decide for the parent.
Detailed Dry Run
| Node | Left Result | Right Result | Pass Up |
|---|---|---|---|
| 5 | p | null | p |
| 1 | null | q | q |
| 3 | 5(p) | 1(q) | 3 (LCA) |
⚠️ Common Pitfalls & Tips
This approach assumes both p and q exist in the tree. If they might not exist, a double-check pass is required.
Binary Tree Zigzag Level Order Traversal
Zigzag BFS.
Intuition
Process the tree level by level using a queue (standard BFS). After gathering all nodes for a specific level, check if the level index is odd. If it is, reverse the list of values before adding it to the final result.
levelIndex starting at 0.levelIndex % 2 != 0, reverse level list.levelIndex.Detailed Dry Run
| Level | Nodes | Reverse? | State |
|---|---|---|---|
| 0 | [3] | No | [[3]] |
| 1 | [9, 20] | Yes | [[3], [20, 9]] |
| 2 | [15, 7] | No | [[3], [20, 9], [15, 7]] |
Intuition
Instead of reversing level results after-the-fact, we can use two stacks to maintain the order. One stack processes Left-to-Right, and the other Right-to-Left. As we pop from one stack, we push its children into the other stack in an order that keeps the next level correctly oriented.
Detailed Dry Run
S1: [3]. Pop 3, S2: [9, 20]. Pop 20, S1: [7, 15]. Pop 9... result levels built directly in order.
Intuition
Directly inserting at either the head or tail of a Deque based on the current level's direction avoids the need for an explicit Reverse step. This is more efficient as we determine the correct spot for each node exactly once.
LevelIndex is even: Add to the back (tail).LevelIndex is odd: Add to the front (head).Detailed Dry Run
| Level | Node | Direction | Deque State |
|---|---|---|---|
| 1 | 9 | Left to Right | [9] |
| 1 | 20 | Left to Right | [9, 20] |
| 2 | 15 | Right to Left | [15] |
| 2 | 7 | Right to Left | [7, 15] |
⚠️ Common Pitfalls & Tips
While unshift in JS or appendleft in Python is for Deques, using them on standard arrays can be per operation. Be sure to use appropriate data structures.
Path Sum II
All root-to-leaf paths summing to target.
Intuition
To find all root-to-leaf paths that sum to a target, we can use Depth First Search (DFS). In this basic version, each time we recurse, we create a new copy of the current path. This is simpler to implement but less space-efficient than backtracking.
node.val from the remaining target.remainingTarget == 0, add the current path to the result list.Detailed Dry Run
| Tree | Target | Path | Remaining | Action |
|---|---|---|---|---|
| 5 | 22 | [5] | 17 | Recurse |
| 4 | 17 | [5, 4] | 13 | Recurse |
| 11 | 13 | [5, 4, 11] | 2 | Recurse (to 2) |
| 2 | 2 | [5, 4, 11, 2] | 0 | SUCCESS! |
Intuition
We can use a Queue of Triplets (node, currentPath, currentSum) to simulate BFS. For each node, we push its children along with the updated path and sum. This avoids recursion entirely while still exploring the tree level by level.
[node, path_list, rem_sum].rem_sum == node.val, save path.path + [node.val] and rem_sum - node.val.Detailed Dry Run
Queue: [(5, [], 22)]. Pop 5, Push (4, [5], 17) and (8, [5], 17). Pop 4, Push (11, [5, 4], 13)...
Intuition
Instead of copying the array at every step, we use a single global list and perform backtracking. We add the current node to the list, recurse, and then remove the current node (the last element) before returning from the function call.
DFS + Backtracking: Keep state clean for previous caller by undoing changes.
Detailed Dry Run
| Action | Path | Sum (Remaining) |
|---|---|---|
| Visit 5 | [5] | 17 |
| Visit 4 | [5, 4] | 13 |
| Visit 11 | [5, 4, 11] | 2 |
| Pop 11 | [5, 4] | 13 |
⚠️ Common Pitfalls & Tips
You still need to copy the list when adding it to the result (res.add(new ArrayList<>(path))), otherwise all paths in the result will end up empty at the end of the process!
Kth Smallest Element in a BST
Find kth val.
Intuition
As with any BST problem, the inorder traversal gives us the elements in strictly increasing order. We can perform a full inorder traversal, store the results in an array, and then simply return the element at index k-1.
list[k-1].Detailed Dry Run
| Tree | Inorder Array | k | Result |
|---|---|---|---|
| [3,1,4,null,2] | [1, 2, 3, 4] | 1 | 1 |
Intuition
If we can efficiently count the number of nodes in a subtree, we can determine whether the -th smallest element lies in the left subtree, is the root itself, or lies in the right subtree.
L = countNodes(root.left).k == L + 1: root.val is the result.k <= L: Search in the left subtree.k > L + 1: Search in the right subtree for the (k - L - 1)-th smallest element.Detailed Dry Run
k=1, L=2 (nodes in left). k<=L -> Search Left. L=1 (at next level). k<=L -> Search Left. L=0. k=1, L=0 -> k=L+1 -> Result found.
Intuition
We don't need to visit all nodes if is small. By using an iterative inorder traversal with a stack, we can visit nodes one by one in increasing order. As soon as we reach the -th node, we return its value and stop the traversal.
Detailed Dry Run
| Stack | Current Node | k-count | Action |
|---|---|---|---|
| [3, 1] | null | 0 | Pop 1, Push 2 |
| [3] | 2 | 1 | Pop 1 was 1st smallest. |
| [3] | null | 1 | Pop 2, k reaches 1 (if k=2) |
⚠️ Common Pitfalls & Tips
Be careful with the condition. Since is 1-indexed (1st smallest), we decrement and check k == 0 immediately after popping.
Populating Next Right Pointers in Each Node
Connect levels.
Intuition
Standard level-order traversal (BFS) using a queue allows us to process nodes level by level. At each level, we iterate through the nodes and set the next pointer of the current node to the next node in the queue (if it's not the last node of the level).
i (of size S):j from 0 to S-1:
node.next = queue.peek() (if j < S-1).Detailed Dry Run
| Level | Node | Queue After Pop | Next Pointer |
|---|---|---|---|
| 1 | 2 | [3] | 3 |
| 1 | 3 | [] | null |
| 2 | 4 | [5, 6, 7] | 5 |
Intuition
We can use the fact that the tree is a perfect binary tree to recursively connect nodes. For any node, we connect its left child to its right child. Then, if the node has a next element, we connect its right child to the next element's left child.
if (!root || !root.left) return.root.left.next = root.right.if (root.next) root.right.next = root.next.left.Detailed Dry Run
root=1: 2.next=3. root=2: 4.next=5, 5.next=6 (if 2.next exists).
Intuition
Since the tree is a perfect binary tree, we can use the already connected next pointers from the current level to connect the children of the next level. This avoids the use of a queue, making it space.
leftmost).cur = cur.next.cur.left.next = cur.right.cur.right.next = cur.next.left (if cur.next exists).Detailed Dry Run
| Level Head | Current | cur.left.next | cur.right.next | Action |
|---|---|---|---|---|
| 1 | 1 | 2->3 | null | Done with L1 |
| 2 | 2 | 4->5 | 5->6 | Move to cur=3 |
| 2 | 3 | 6->7 | null | Done with L2 |
⚠️ Common Pitfalls & Tips
This space trick only works for Perfect Binary Trees. For general trees, you need a dummy node and a pointer to track the next level's current node.
Serialize and Deserialize Binary Tree
Tree to string and back.
Intuition
Standard level-order traversal (BFS) using a queue allows us to convert the tree into a list of strings. Empty children are represented as '#'.
Detailed Dry Run
| Tree | Serialized String |
|---|---|
| [1, 2, 3] | "1,2,3,#,#,#,#" |
| [1, null, 2] | "1,#,2,#,#" |
Intuition
Postorder traversal (Left, Right, Root) is also a valid way to serialize. Deserialization starts from the end of the list and builds the root first, then the right child, then the left child.
dfs(left), dfs(right), append(val).val, then root.right = dfs(), root.left = dfs().Detailed Dry Run
Serialize [1, 2, 3] -> [#, #, 2, #, #, 3, 1]. Pop 1 (root), then 3 (right), then 2 (left).
Intuition
Preorder DFS (Root, Left, Right) is more compact and easier to code recursively. We use a List or a String Join for serialization. During deserialization, we convert the string back into a Queue or use an index, then recursively rebuild the structure.
DFS Preorder Serialization: Maintain structure by marking leaves explicitly.
Detailed Dry Run
| Node | Action | Serialized |
|---|---|---|
| 1 | Visit | [1] |
| 2 | Visit | [1, 2] |
| null | Visit | [1, 2, #] |
| null | Visit | [1, 2, #, #] |
⚠️ Common Pitfalls & Tips
Be aware of recursion limits for very large trees. Serialization strings can become very large, so StringBuilder (Java) or join (Python) is much faster than string concatenation.
Binary Tree Maximum Path Sum
Max path sum (duplicate entry).
Intuition
Every path has a 'peak' node (the highest node in the tree structure that belongs to the path). A brute force approach iterates through every node in the tree, treats it as the peak, and calculates the maximum possible path extending down its left and right subtrees.
max_down(node.left) and max_down(node.right).node.val + max_down(node.left) + max_down(node.right).Detailed Dry Run
At node 20: Left path 15, Right path 7. Path = 15+20+7 = 42. Repeat for all nodes.
Intuition
We can solve this problem by having our recursive function return multiple pieces of information: specifically, the maximum path sum found within the subtree so far, and the maximum gain that can be used by an ancestor. This avoids using a global or member variable to track the maximum path sum.
{max_path_overall, max_gain_to_parent}.max_gain_to_parent = node.val + max(0, left_gain, right_gain).max_path_here = node.val + max(0, left_gain) + max(0, right_gain).max_path_overall = max(max_path_here, left_max_overall, right_max_overall).Detailed Dry Run
At node 20: Left returns {15, 15}, Right returns {7, 7}. Max path overall = max(15, 7, 20+15+7) = 42. Gain to return = 20+15 = 35.
Intuition
We perform a Post-order DFS. For each node, we want to know two things:
gain = node.val + max(0, leftGain, rightGain)
We discard negative gains because a path would be shorter if it simply didn't include that subtree.
-10
/ \
9 20
/ \
15 7
Gain(15) = 15, Gain(7) = 7
At 20: Max path = 15+7+20 = 42. Gain to return to -10 = 20+15 = 35.
At -10: Max path = 9+35-10 = 34.
Result: 42.Detailed Dry Run
| Node | Left Gain | Right Gain | Node Sum | Global Max |
|---|---|---|---|---|
| 15 | 0 | 0 | 15 | 15 |
| 7 | 0 | 0 | 7 | 15 |
| 20 | 15 | 7 | 42 | 42 |
| 9 | 0 | 0 | 9 | 42 |
| -10 | 9 | 35 | 34 | 42 |
⚠️ Common Pitfalls & Tips
Negative values can be tricky. If all nodes are negative, the max path might just be the single node with the highest value (least negative). That's why we initialize max to Integer.MIN_VALUE and use Math.max(..., 0) to decide whether to include subtrees.
Recover Binary Search Tree
Swapped nodes fix.
Intuition
In a sorted array, the elements are always in increasing order. If two elements are swapped, we will find either one or two points where arr[i] > arr[i+1].
Detailed Dry Run
| Tree Inorder | Swapped Pairs | First Node | Second Node |
|---|---|---|---|
| [1, 3, 2, 4] | (3, 2) | 3 | 2 |
| [3, 2, 1] | (3, 2), (2, 1) | 3 | 1 |
Intuition
Instead of checking sortedness in an array, we can do it on the fly using a stack during an iterative inorder traversal. We maintain a prev pointer and identify where prev.val > current.val.
prev.val > current.val, identifying the first and last out-of-order nodes.first and second.Detailed Dry Run
Stack: [1, 3, 2]. Pop 3, prev=3. Pop 2, current=2. prev > current! first=3, second=2. Result: swap(3, 2).
Intuition
We can find the swapped nodes during a single inorder traversal without storing the whole tree. We keep tracks of the prev node seen. If prev.val > current.val, we have found an anomaly.
There are two cases:
prev and second anomaly's current).Detailed Dry Run
| Prev | Current | Anomaly? | First | Second |
|---|---|---|---|---|
| 3 | 2 | Yes (3>2) | 3 | 2 |
| 2 | 1 | Yes (2>1) | (keep 3) | 1 |
⚠️ Common Pitfalls & Tips
You must update second in every anomaly found. In Case 2 (non-adjacent swaps), second will be updated twice, correctly identifying the further node.
Count Univalue Subtrees
Count same val subtrees.
Intuition
A uni-value subtree is a subtree where all nodes have the same value. A brute force approach checks for every node n if all nodes in its subtree have the same value as n.val.
isUnival(node, val) helper.count function:
isUnival(root, root.val), increment count.Detailed Dry Run
| Node | Subtree Values | All Same? | Action |
|---|---|---|---|
| 1 | [1, 1, 1] | YES | count++ |
| 5 | [5, 5] | YES | count++ |
Intuition
Instead of checking every node as root, we can pass the parent's value down. If a node and its children all match that value, we have a candidate. This approach is slightly more efficient than checking every node but still redundant compared to bottom-up.
isUnival(node, target) helper.Detailed Dry Run
Node 5. isUnival(5, 5) -> returns true if all in 5's subtree are 5.
Intuition
Instead of re-checking each subtree, we can use a bottom-up approach. A node is the root of a uni-value subtree if children are uni-value and match the parent's value.
DFS Result Aggregation: Pass child status up to parents.
Detailed Dry Run
| Node | Left Uni? | Right Uni? | Matches Children? | Count |
|---|---|---|---|---|
| Leaf | true | true | yes | count++ |
| Root | true | true | yes | count++ |
⚠️ Common Pitfalls & Tips
You must call DFS for both children before returning, even if one child is not a uni-value subtree. This is because the other child might still contain uni-value subtrees that need to be counted.
Serialize and Deserialize N-ary Tree
N-ary tree.
Intuition
In an N-ary tree, we can use a marker (like null) to distinguish between the children of different parents. During serialization, we use a queue and for every node, we add its children followed by a null to indicate the end of that node's children list.
null after children of each node.Detailed Dry Run
Node 1 children: [3, 2, 4]. Serialized: [1, null, 3, 2, 4, null, 5, 6, null...]
Intuition
We can represent the tree structure using parentheses: 1(3(5,6),2,4). This is a very intuitive human-readable format.
node.val + ( + serialize(child) for each child + ).Detailed Dry Run
Tree: 1 -> [3, 2, 4]. 3 -> [5, 6]. Serial: 1(3(5,6),2,4).
Intuition
To serialize an N-ary tree, we can use DFS (Preorder). For each node, we record its value and the number of children it has. During deserialization, we read the value, create a node, and then recursively deserialize exactly count children for that node.
val and children.size().val, read size, then loop size times to build child subtrees.Detailed Dry Run
| Node | Children | Serialized Array |
|---|---|---|
| 1 | 3 | [1, 3] |
| 3 | 2 | [1, 3, 3, 2] |
| 5 | 0 | [1, 3, 3, 2, 5, 0] |
| 6 | 0 | [1, 3, 3, 2, 5, 0, 6, 0] |
Lowest Common Ancestor III
With parent pointers.
Intuition
Since each node has a parent pointer, we can trace the path from node p back to the root, storing all ancestors in a Hash Set. Then, trace back from node q. The first ancestor of q that is already in the set is the LCA.
p to root, adding every node to Set.q to root.Set.Detailed Dry Run
| Node | Set (Ancestors of P) | In Set? |
|---|---|---|
| P=5 | {5} | - |
| 3 (parent of 5) | {5, 3} | - |
| Q=1 | {5, 3} | NO |
| 3 (parent of 1) | {5, 3} | YES! (Result=3) |
Intuition
We can calculate the depth of both nodes p and q. Then, move the deeper node up its parent chain until both nodes are at the same depth. From there, move both up together until they meet.
depthP and depthQ.p == q.Detailed Dry Run
P at depth 3, Q at depth 2. Move P up once. Both at depth 2. Move together -> meet at LCA.
Intuition
This problem is equivalent to finding the intersection of two linked lists. We use two pointers, a and b.
a starts at p, b starts at q.null), it jumps to the other starting node (q or p).a = (a == null) ? q : a.parent
b = (b == null) ? p : b.parent
Detailed Dry Run
| Step | Ptr A | Ptr B | Notes |
|---|---|---|---|
| 1 | 5 | 1 | Start |
| 2 | 3 | 3 | MEET! (LCA=3) |
⚠️ Common Pitfalls & Tips
Be extremely careful with the null jumping logic. If you jump at the wrong node, you might enter an infinite loop. The pointers must switch to the other starting node to align their path lengths.
Path Sum IV
Tree as array sum.
Intuition
We can build the tree level by level using a queue. For each level, we store the full path sum to each node. When we find a node that has no children in the input set, it's a leaf, and we add its path sum to the total.
(depth, position, currentSum).Detailed Dry Run
Queue starts with [1,1,3]. Next level: [2,1,8], [2,2,4]. Since these have no children, add 8+4=12 to total.
Intuition
We can use a recursive approach that builds the tree logic internally. For any ID ij, its children are (i+1)(2j-1) and (i+1)(2j). We can track the path sum as we recurse down.
nums in a Map.11 with currentSum = 0.Detailed Dry Run
DFS(11, 0) -> DFS(21, 3) + DFS(22, 3). 21 is leaf -> total += 3+node.val.
Intuition
In this problem, a tree is given as a list of integers [Depth, Position, Value]. Depth 1 means root, depth 2 is its children. A position P in depth D has children at position 2P-1 and 2P in depth D+1.
key = (depth * 10 + position), value = val.Detailed Dry Run
| Input | Map | Depth/Pos | Path Sum | Action |
|---|---|---|---|---|
| 113 | {11: 3} | 1, 1 | 3 | Root |
| 215 | {11: 3, 21: 5} | 2, 1 | 8 | Left Child |
| 221 | {11: 3, 21: 5, 22: 1} | 2, 2 | 4 | Right Child |
Binary Tree Cameras
Min cameras.
Intuition
A brute force approach would try every possible subset of nodes to place cameras and check if all nodes are covered. Since there are subsets, this is too slow but conceptually simple.
Detailed Dry Run
Try all 8 combinations for 3 nodes. Min cameras = 1 (at root).
Intuition
For each node, we can track 3 states:
dp(node, state) returns min cameras for subtree rooted at node given state. This avoids the greedy assumptions by exploring all valid coverages recursiveley.Detailed Dry Run
DP(root, hasCamera) returns min of children. DP(root, notCovered) forces parent camera.
Intuition
We want to place cameras such that we cover all nodes with the minimum number. A camera at a node covers itself, its parent, and its children.
Greedy Strategy: It's always better to place a camera at a parent of a leaf node rather than the leaf node itself. A camera at the parent covers more nodes.
Detailed Dry Run
| Node | Left State | Right State | Node State | Action |
|---|---|---|---|---|
| Leaf | 2 | 2 | 0 | Return 0 (leaf uncovered) |
| Parent | 0 | 0 | 1 | Camera++ (covers leaf) |
| GrandP | 1 | 1 | 2 | Covered by child |
Find Duplicate Subtrees
List duplicates.
Intuition
For every node in the tree, we can compare its subtree with the subtrees of all other nodes. This involves a nested traversal: for each node, we start a secondary traversal that checks for equality with every other node.
dfs(node) visits every node.isSame(node, otherNode) checks if they are identical.Detailed Dry Run
Compare Node(2) with every other node in the tree. If equal and not seen before, add to result.
Intuition
How do we know if two subtrees are identical? We can serialize each subtree (convert to string) and store the frequency of these strings in a HashMap. If a string appears for the second time, we've found a duplicate.
val + "," + left + "," + right.map.get(string) == 2, add current node to results.Detailed Dry Run
| Node | Left Serial | Right Serial | Node Serial | Count | Duplicate? |
|---|---|---|---|---|---|
| Leaf 4 | # | # | "4,#,#" | 1 | NO |
| Leaf 4 | # | # | "4,#,#" | 2 | YES! |
| Node 2 | "4,#,#" | # | "2,4,#,#,#" | 1 | NO |
Intuition
The bottleneck in Level II is caused by string concatenation. Instead of strings, we can assign a unique integer ID to each subtree structure. Two subtrees are identical if they have the same (node.val, left_child_id, right_child_id) triplet.
tripletToID to map triplets of integers to a unique ID.idCount to track how many times each ID has appeared.Detailed Dry Run
| Node | Left ID | Right ID | Triplet | New ID | Count | Duplicate? |
|---|---|---|---|---|---|---|
| 4 | 0 | 0 | (4,0,0) | 1 | 1 | NO |
| 4 | 0 | 0 | (4,0,0) | 1 | 2 | YES! |
| 2 | 1 | 0 | (2,1,0) | 2 | 1 | NO |
Vertical Order Traversal
Vertical columns.
Intuition
We can perform a DFS and for each node, we pass down its column index. We store nodes in a Map of col -> List<Node>. After traversal, we sort the keys (cols) and then sort each list of nodes by their depth and value.
(node, col, depth).Map<Integer, List<int[]>> (col -> [depth, val]).Detailed Dry Run
Col -1: [2]. Col 0: [1, 3]. Col 1: [4]. Sorting ensures correct ordering.
Intuition
BFS naturally processes nodes level by level. By adding column information, we can group nodes. However, since multiple nodes can have the same (col, depth), we still need sorting for nodes in the same cell.
(node, col, depth).col -> List of (depth, val).Detailed Dry Run
Similar to Level I but uses Queue.
Intuition
We want to group nodes by their vertical column. Root is col=0. Left child is col-1, Right is col+1.
Since we need to return results from left-to-right, we can track the min and max column indices during BFS. This avoids sorting at the end.
(Node, col).Map<Integer, List<Integer>> (col -> list of vals).minCol and maxCol.minCol to maxCol to collect results.Detailed Dry Run
| Node | Col | Min | Max | Map |
|---|---|---|---|---|
| 3 | 0 | 0 | 0 | {0: [3]} |
| 9 | -1 | -1 | 0 | {0: [3], -1: [9]} |
| 20 | 1 | -1 | 1 | {0: [3], -1: [9], 1: [20]} |
Construct Binary Tree from Preorder and Postorder
Rebuild tree.
Intuition
In both preorder and postorder, the first element (or last in postorder) is the root. The element after the root in preorder (preorder[1]) is the root of the left subtree. We find this element in the postorder array; all elements before it in postorder belong to the left subtree.
root = preorder[0].preorder[1].preorder[1] in postorder (linear search).postorder are in the left subtree.Detailed Dry Run
| Preorder | Postorder | Left Root | Post Index | Left Size |
|---|---|---|---|---|
| [1,2,4,5,3,6,7] | [4,5,2,6,7,3,1] | 2 | 2 | 3 |
Intuition
Instead of full array slicing or repeated index searching, we maintain global pointers/indices and use a Hash Map for lookups of the left child's position in the postorder array.
Pre: [1, 2, 4, 5, 3, 6, 7]
Idx: 0, 1, 2, 3, 4, 5, 6
If we are at 1, next is 2.
Find 2 in Post: Index 2.
Left Subtree count = 2 - 0 + 1 = 3.Detailed Dry Run
| Pre Index | Node Val | Left Val | Post Map Idx | Action |
|---|---|---|---|---|
| 0 | 1 | 2 | 2 | Build 1, proceed to Left |
| 1 | 2 | 4 | 0 | Build 2, proceed to Left |
| 2 | 4 | - | - | Leaf 4, return |
⚠️ Common Pitfalls & Tips
Note that this construction (Pre+Post) does not uniquely define a tree if any node has only one child. The standard implementation treats the single child as the left child.
House Robber III
Tree robbery.
Intuition
Each node can either be robbed or not robbed. If we rob a node, we cannot rob its direct children. If we don't rob it, we can either rob its children or not.
rob(root) = max(root.val + rob(left.left) + rob(left.right) + rob(right.left) + rob(right.right), rob(left) + rob(right)).
Detailed Dry Run
At root: Option 1 (Rob 1 + grandchildren). Option 2 (Rob children 2 and 3). Max is 3.
Intuition
We can store the result of rob(node) in a HashMap to avoid recomputing for the same node multiple times.
Detailed Dry Run
Store [Node Pointer] -> Max rob value.
Intuition
Each node returns a pair: [max if robbed, max if NOT robbed].
root, we must NOT rob its children: root.val + left[1] + right[1].root, we take the max of robbing or not robbing each child: max(left) + max(right).Detailed Dry Run
Leaf returns [val, 0]. Parent returns [parentVal + 0, leafVal + 0].
Help us improve! Report bugs or suggest new features on our Telegram group.