Binary Tree Traversal
Visit every node of a tree exactly once, in an order you choose on purpose. The order you pick (pre, in, post, or level) decides what information is ready when you reach each node.
1. What is this pattern for?
A binary tree is a set of nodes where each node has a value and up to two children, left and right. Almost every tree problem is "walk the tree and do something at each node". The only real decision is when you do the work relative to the children:
| Order | Visit sequence | Use it when the node needs… |
|---|---|---|
| Preorder | node → left → right | …to pass something down (depth, path so far, bounds). Also copying / serializing. |
| Inorder | left → node → right | …sorted order in a BST (binary search tree). |
| Postorder | left → right → node | …answers from its children first (height, sums, "best path below me"). |
| Level order | row by row, using a queue | …anything per level: averages, right-side view, zig-zag, min depth. |
The first three are depth-first: they go deep before going wide. You can write them with recursion (the computer's call stack remembers where you were) or with your own explicit stack. Level order is breadth-first and uses a queue.
2. How it works (bisected)
- Base case: empty tree. If
node is None, there's nothing to do. Every recursive tree function starts here. - Each node is touched three times. Recursion arrives at a node (1st touch), comes back after finishing the left subtree (2nd touch), and comes back after the right subtree (3rd touch). Preorder records on touch 1, inorder on touch 2, postorder on touch 3. That's the whole difference.
- Iterative = simulate the call stack. Push nodes you still need to return to. For inorder: slide left pushing every node, pop one, record it, then move to its right child.
- Level order = queue + frozen size. Put the root in a queue. While it's not empty, read
size = len(queue); pop exactlysizenodes (that's one level) and push their children for the next level. - Pick the order from the question. Information flows down? Preorder. Up? Postorder. Sorted BST? Inorder. Per row? Level order.
3. Seeing a problem: how to spot tree traversal
Trigger words
- "binary tree", "root", "
TreeNode", "BST", "subtree", "ancestor", "leaf" - "each level", "level by level", "right side view", "zig-zag", "minimum depth" → level order
- "kth smallest", "sorted", "validate BST", "successor" → inorder
- "height", "diameter", "max path sum", "is balanced", "subtree sum" → postorder
- "serialize", "clone", "construct from…", "path from root" → preorder
Shape of the input
- A
root: TreeNode, or a level-order list like[3,9,20,null,null,15,7]. - The answer for a node depends on its subtrees (combine) or its ancestors (pass down).
- Brute-force smell: you call a helper like
height(node)from inside another recursion, re-walking subtrees over and over. That'sO(n²). One postorder pass that returns the height while computing the answer isO(n).
4. When to use it, and when not to
| Use tree traversal when… | Prefer something else when… |
|---|---|
The input is a rooted tree with left/right pointers | Nodes can have many neighbours and cycles: that's a general graph, use DFS or BFS with a visited set |
| You need per-level answers (level order) | You need shortest distance in an unweighted graph or grid: use BFS (same queue idea, plus visited) |
| BST questions about order (inorder) or searching (walk one branch) | You only need to find one value in a BST: just walk down one path, O(h), similar in spirit to Modified Binary Search |
| Subtree answers combine into parent answers (postorder) | Overlapping sub-problems on sequences/grids: use Dynamic Programming (tree DP is postorder + memo) |
| You must explore all root-to-leaf paths | You must build/undo choices to enumerate combinations: use Backtracking |
Cost: every traversal is O(n) time (each node visited once). Space is O(h) for DFS (recursion/stack depth; h = log n if balanced, n if it's a stick) and O(w) for level order, where w is the widest level (up to n/2).
5. The Python templates (memorize these)
Each template below is written out in full, one small step per line. Read the code, then the plain-English walk-through underneath it. All of them use LeetCode's TreeNode (fields val, left, right).
A) Recursive DFS: one skeleton, three orders
# A) Recursive DFS skeleton: move ONE line to switch between pre / in / post
def dfs(node):
# Base case: an empty subtree has nothing to visit, so stop here.
if node is None:
return
# visit(node) <- PREORDER (work before children: pass info down)
dfs(node.left)
# visit(node) <- INORDER (work between: sorted order in a BST)
dfs(node.right)
# visit(node) <- POSTORDER (work after: combine children's answers)
What the code is doing, line by line:
if node is None: returnis the base case. Every recursive tree function starts with it; without it you would crash onNone.left.dfs(node.left)thendfs(node.right)visit the two subtrees, left before right.- The three
# visit(node)comments mark the three moments a node is "touched": before the children (preorder), between them (inorder), after them (postorder). Put your real work on exactly one of those lines.
# A2) The three recursive orders, each one collecting values into a list.
# The recursion lives in a separate top-level helper ("..._walk").
# The helper receives the list "out" and adds values to it.
def preorder_walk(node, out):
if node is None: # empty subtree: nothing to add
return
out.append(node.val) # PRE: record the node BEFORE its children
preorder_walk(node.left, out)
preorder_walk(node.right, out)
def preorder_rec(root):
out = [] # will hold the values in visiting order
preorder_walk(root, out) # the helper fills the list
return out
def inorder_walk(node, out):
if node is None:
return
inorder_walk(node.left, out)
out.append(node.val) # IN: record the node BETWEEN its children
inorder_walk(node.right, out)
def inorder_rec(root):
out = []
inorder_walk(root, out)
return out
def postorder_walk(node, out):
if node is None:
return
postorder_walk(node.left, out)
postorder_walk(node.right, out)
out.append(node.val) # POST: record the node AFTER its children
def postorder_rec(root):
out = []
postorder_walk(root, out)
return out
What the code is doing, line by line:
- Each order has two functions: a small public one (
preorder_rec) that creates the empty listout, and a recursive helper (preorder_walk) that fills it. - The helper takes
outas a parameter, so every recursive call appends to the same list. No nested functions are needed. - The three helpers are identical except for where the line
out.append(node.val)sits: first (pre), middle (in) or last (post).
Dry run on the tree [4,2,6,1,3,5,7] (4 at the root, 2 and 6 below it, then 1, 3, 5, 7):
| Function | Result |
|---|---|
preorder_rec | [4, 2, 1, 3, 6, 5, 7] |
inorder_rec | [1, 2, 3, 4, 5, 6, 7] (sorted, because it's a BST) |
postorder_rec | [1, 3, 2, 5, 7, 6, 4] |
B) Postorder that returns a value
# B) Postorder that RETURNS a value up (height, sums, best path...)
def height(node):
# An empty subtree has height 0.
if node is None:
return 0
# First ask both children for their heights (children before parent = postorder).
left_height = height(node.left)
right_height = height(node.right)
# This node adds one level on top of its taller child.
taller_child = max(left_height, right_height)
return 1 + taller_child # the parent gets back one number
What the code is doing, line by line:
- An empty subtree has height 0. That stops the recursion at the leaves' missing children.
- Both children are asked for their height first. Because the node waits for its children, this is postorder.
max(left_height, right_height)picks the taller child, and1 +counts the node itself.- The function returns the number to its caller, which is how information flows up the tree.
Dry run on [4,2,6,1,3,5,7]:
| Node | left_height | right_height | returns |
|---|---|---|---|
| 1 (leaf) | 0 | 0 | 1 |
| 3 (leaf) | 0 | 0 | 1 |
| 2 | 1 | 1 | 2 |
| 6 (same as 2) | 1 | 1 | 2 |
| 4 (root) | 2 | 2 | 3 |
C) Iterative inorder
# C) Iterative inorder (the one interviewers ask for)
def inorder_iter(root):
out = [] # the values, in inorder
stack = [] # ancestors we still have to come back to
current = root # the node we are about to explore
# Keep going while there is a node to explore OR an ancestor waiting on the stack.
while current is not None or len(stack) > 0:
# 1) Slide down-left, pushing every node we pass on the way.
while current is not None:
stack.append(current)
current = current.left
# 2) The top of the stack is the leftmost node we have not visited yet.
current = stack.pop()
out.append(current.val)
# 3) Now explore that node's right subtree.
current = current.right
return out
What the code is doing, line by line:
outcollects the answer,stackremembers ancestors we must come back to,currentis where we are.- The outer
whileruns as long as there is either a node to explore or an ancestor waiting. - Step 1, the inner
while: walk left as far as possible, pushing each node. This is exactly what the recursive version does when it callsdfs(node.left)again and again. - Step 2: pop. The popped node has no unvisited left side any more, so it is next in inorder. Record it.
- Step 3: move to its right child and repeat. If there is no right child,
currentis None and the next loop pops an ancestor.
Dry run on [4,2,6,1,3,5,7]:
| Step | Stack after sliding left | Popped (recorded) | out so far |
|---|---|---|---|
| 1 | [4, 2, 1] | 1 | [1] |
| 2 | [4, 2] | 2 | [1, 2] |
| 3 | [4, 3] | 3 | [1, 2, 3] |
| 4 | [4] | 4 | [1, 2, 3, 4] |
| 5 | [6, 5] | 5 | [1, 2, 3, 4, 5] |
| 6 | [6] | 6 | [1, 2, 3, 4, 5, 6] |
| 7 | [7] | 7 | [1, 2, 3, 4, 5, 6, 7] |
D) Iterative preorder and postorder
# D) Iterative preorder: stack, push RIGHT before LEFT
def preorder_iter(root):
if root is None: # empty tree: nothing to visit
return []
out = []
stack = [root] # start with only the root on the stack
while len(stack) > 0:
node = stack.pop() # take the most recently pushed node
out.append(node.val) # PRE: record it straight away
# Push RIGHT first, so that LEFT ends up on top and is popped first.
if node.right is not None:
stack.append(node.right)
if node.left is not None:
stack.append(node.left)
return out
# Iterative postorder: collect "root, right, left", then read it backwards.
def postorder_iter(root):
if root is None:
return []
mirrored = [] # will hold root, right, left order
stack = [root]
while len(stack) > 0:
node = stack.pop()
mirrored.append(node.val)
# Push LEFT first, so that RIGHT is popped first.
if node.left is not None:
stack.append(node.left)
if node.right is not None:
stack.append(node.right)
# Read "mirrored" from the last item to the first: that gives left, right, root.
out = []
i = len(mirrored) - 1
while i >= 0:
out.append(mirrored[i])
i = i - 1
return out
What the code is doing, line by line:
preorder_iter: the stack starts with just the root. Each loop pops one node and records it immediately (that's "me first").- A stack is last-in-first-out, so the right child is pushed before the left child. That way the left child is on top and gets handled first.
postorder_iterruns the same loop but pushes left before right, which produces "root, right, left" inmirrored.- The final
while i >= 0loop copiesmirroredfrom the last item to the first. Reversed, "root, right, left" becomes "left, right, root", which is postorder.
Dry run of preorder_iter on [4,2,6,1,3,5,7]:
| Popped | out so far | Stack after pushing children (top is last) |
|---|---|---|
| 4 | [4] | [6, 2] |
| 2 | [4, 2] | [6, 3, 1] |
| 1 | [4, 2, 1] | [6, 3] |
| 3 | [4, 2, 1, 3] | [6] |
| 6 | [4, 2, 1, 3, 6] | [7, 5] |
| 5 | [4, 2, 1, 3, 6, 5] | [7] |
| 7 | [4, 2, 1, 3, 6, 5, 7] | [] |
E) Level order (BFS on a tree)
# E) Level order (BFS on a tree)
from collections import deque
def level_order(root):
if root is None: # empty tree: no levels at all
return []
result = [] # one inner list per level
# deque is a "double-ended queue":
# queue.append(x) adds x at the BACK of the line
# queue.popleft() removes and returns the item at the FRONT
queue = deque()
queue.append(root)
while len(queue) > 0:
# Freeze the size: exactly this many nodes belong to the current level.
level_size = len(queue)
level = []
for _ in range(level_size): # "_" means: we don't need the counter
node = queue.popleft() # oldest node first
level.append(node.val)
# Children go to the BACK: they belong to the NEXT level.
if node.left is not None:
queue.append(node.left)
if node.right is not None:
queue.append(node.right)
result.append(level)
return result
What the code is doing, line by line:
- An empty tree has no levels, so return
[]right away. - The queue starts with only the root.
appendadds at the back andpoplefttakes from the front, so nodes come out in the order they went in. level_size = len(queue)is read before the inner loop. At that moment the queue holds exactly one full level.- The inner loop pops exactly
level_sizenodes, records their values, and pushes their children to the back. Those children wait for the next round. - After the inner loop,
levelis complete and is added toresult.
Dry run on [4,2,6,1,3,5,7]:
| Round | Queue at start | level_size | level | Queue after |
|---|---|---|---|---|
| 1 | [4] | 1 | [4] | [2, 6] |
| 2 | [2, 6] | 2 | [2, 6] | [1, 3, 5, 7] |
| 3 | [1, 3, 5, 7] | 4 | [1, 3, 5, 7] | [] |
6. How to remember it forever
How this hook works: what “The name tells you where the root goes” means
What the slogan means, phrase by phrase
- “The name”
- pre-, in- and post-order.
- “tells you where the root goes”
- Each name says when you visit the current node ("the root" of this subtree) relative to its two children: pre = before both, in = between, post = after both. Left always comes before right.
Why this picture fits the pattern: All three depth-first orders are the same recursive function. Only the position of one line, the visit, moves. Once the name tells you where that line goes, you never need to memorise three separate algorithms.
The story → the code
| In the story | In the code |
|---|---|
| Pre: root first | visit, then dfs(node.left), dfs(node.right) |
| In: root in the middle | dfs(node.left), visit, dfs(node.right) |
| Post: root last | dfs(node.left), dfs(node.right), visit |
| Level order: one row at a time | a deque queue, not recursion |
Watch it on numbers
A tree with 1 at the root, children 2 and 3, and 2 has children 4 and 5.
- Pre (root first): 1, then all of 2's subtree (2, 4, 5), then 3 →
1 2 4 5 3. - In (root between): 2's subtree (4, 2, 5), then 1, then 3 →
4 2 5 1 3. - Post (root last): 4, 5, 2, then 3, then 1 →
4 5 2 3 1. - Level:
[1],[2, 3],[4, 5].
The trap it saves you from
Picking the wrong order for the job. If a node needs its children's answers (height, diameter, subtree sums), the work must go after the calls: postorder. "Down = pre, up = post."
It still works when…
In a BST, inorder visits values in sorted order, which is the key to Validate BST and Kth Smallest. Serialising a tree uses preorder, because the root comes first so you can rebuild from the front.
Picture
Trace a pencil around the outside of the tree, starting left of the root. You pass each node on its left side (pre), under it (in), and on its right side (post). Record the node at the matching moment.
Chant
"Pre: me first. In: me between. Post: me last. Level: one row at a time."
Direction rule
"Down = pre, up = post." Passing values to children? Preorder with parameters. Children reporting to the parent? Postorder with return values.
7. LeetCode problems, solved step by step
Helper used below: class TreeNode: val, left, right (LeetCode's standard definition).
Medium 230. Kth Smallest Element in a BST Google Meta Amazon Cisco Microsoft
Problem: Given the root of a binary search tree and an integer k, return the k-th smallest value (1-indexed).
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [3,1,4,null,2], k = 11root = [5,3,6,2,4,null,null,1], k = 33Constraints: The number of nodes in the tree is n. · 1 ≤ k ≤ n ≤ 10⁴ · 0 ≤ Node.val ≤ 10⁴
Hint 1
An in-order traversal of a BST visits values in increasing order.
Hint 2
Do an in-order traversal and stop at the k-th node you visit (an explicit stack lets you stop early).
From brute force to the pattern: Collecting every value and sorting is O(n log n). The BST's in-order is already sorted, so walking it and stopping after k nodes is O(h + k).
Spot it: "BST" + "kth smallest" = sorted order is needed. Inorder traversal of a BST visits values in ascending order.
Key insight: Use the iterative inorder so you can stop early the moment you've visited k nodes. Cost is O(h + k), not O(n).
Approach:
- Start with
current = root, an empty stack, andvisited_count = 0. - Slide left: push
currentand move tocurrent.leftuntilcurrentis None. - Pop the top node. It's the next-smallest value. Add 1 to
visited_count. - If
visited_count == k, return this value (stop early). - Otherwise set
current = current.rightand repeat from step 2.
class Solution:
def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
stack = [] # ancestors waiting to be visited
current = root # where we are in the tree
visited_count = 0 # how many values we have visited so far (in sorted order)
while current is not None or len(stack) > 0:
# Go as far left as possible, remembering the path on the stack.
while current is not None:
stack.append(current)
current = current.left
# The top of the stack is the next smallest value.
current = stack.pop()
visited_count = visited_count + 1
# Is this the k-th smallest? Then stop early; no need to see the rest.
if visited_count == k:
return current.val
# Otherwise continue with this node's right subtree.
current = current.right
return -1 # only reached if k is bigger than the number of nodes
What the code is doing, line by line:
stack,currentandvisited_countare set up. The count tells us how many values we have seen in sorted order.- The inner
whileslides left fromcurrent, pushing every node. The smallest unvisited value ends up on top of the stack. stack.pop()gives the next smallest value, sovisited_countgoes up by 1.- If the count has reached
k, that value is the answer and we return immediately, without visiting the rest of the tree. - Otherwise
current = current.right: the next bigger values live in the right subtree (or in an ancestor on the stack).
Dry run on root = [5,3,6,2,4,null,null,1], k = 3:
| Step | Stack after sliding left | Popped | visited_count | Action |
|---|---|---|---|---|
| 1 | [5, 3, 2, 1] | 1 | 1 | 1 ≠ 3, go right (None) |
| 2 | [5, 3, 2] | 2 | 2 | 2 ≠ 3, go right (None) |
| 3 | [5, 3] | 3 | 3 | count == k, return 3 |
Edge cases to test:
k = 1→ the leftmost node.k = n→ the rightmost node.- A completely skewed tree.
Complexity: O(h + k) time, O(h) space.
Medium 98. Validate Binary Search Tree Google Meta Amazon Microsoft
Problem: Return True if the tree is a valid BST: every node's left subtree holds only smaller values, its right subtree only larger values, recursively.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [2,1,3]trueroot = [5,1,4,null,null,3,6]falseConstraints: The number of nodes in the tree is in the range [1, 10⁴]. · -2³¹ ≤ Node.val ≤ 2³¹ - 1
Hint 1
Comparing each node only with its children is not enough. Why?
Hint 2
Every node must lie within a range inherited from its ancestors: pass (low, high) down, or check that the in-order traversal is strictly increasing.
From brute force to the pattern: Checking, for every node, that its whole left subtree is smaller and its whole right subtree larger is O(n²). Passing bounds down (or one in-order pass) checks it in O(n).
Spot it: "valid BST" means "is the inorder sequence strictly increasing?"
Key insight: Checking only node.left.val < node.val is the classic trap. In [5,4,6,null,null,3,7] the 3 is a right-subtree descendant of 5, which breaks the rule, but every parent-child pair looks fine. Comparing each inorder value with the previous inorder value catches it.
Approach:
- Run the iterative inorder traversal (slide left, pop, go right).
- Keep
previous_value= the last value you popped. - Each time you pop a node, if
previous_valueexists andcurrent.val <= previous_value, the order isn't strictly increasing: return False. - Update
previous_valueand continue. If the traversal finishes, return True.
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
stack = [] # ancestors waiting to be visited
current = root
previous_value = None # the value we visited just before (None = nothing yet)
while current is not None or len(stack) > 0:
# Slide left, pushing every node.
while current is not None:
stack.append(current)
current = current.left
# Visit the next node in inorder.
current = stack.pop()
# Inorder of a BST must be STRICTLY increasing.
if previous_value is not None:
if current.val <= previous_value:
return False # out of order (or a duplicate): not a valid BST
previous_value = current.val
# Move on to the right subtree.
current = current.right
return True # every value was bigger than the one before it
What the code is doing, line by line:
- Same iterative inorder as before, plus
previous_value, the last value we visited (None at the start). - The inner
whileslides left, pushing nodes.stack.pop()then gives the next value in inorder. - If there is a previous value and the current one is not bigger, the inorder sequence is not strictly increasing, so the tree is not a BST: return False.
- Otherwise remember this value in
previous_valueand go to the right subtree. - If the loop finishes without a problem, every value was bigger than the one before it: return True.
Dry run on root = [5,1,4,null,null,3,6] (expected False):
| Step | Stack after sliding left | Popped | previous_value | Check |
|---|---|---|---|---|
| 1 | [5, 1] | 1 | None | nothing to compare, OK |
| 2 | [5] | 5 | 1 | 5 > 1, OK |
| 3 | [4, 3] | 3 | 5 | 3 <= 5, return False |
Alternative (preorder, passing info down): check(node, lo, hi) requires lo < node.val < hi, then recurses with (lo, node.val) on the left and (node.val, hi) on the right.
Edge cases to test:
- A deep violation, e.g.
[5, 4, 6, null, null, 3, 7](3 is inside the right subtree of 5). - Duplicate values → invalid (strictly increasing is required).
- Values at the integer limits (use infinities as the initial bounds).
Complexity: O(n) time, O(h) space.
Medium 105. Construct Binary Tree from Preorder and Inorder Traversal Google Amazon Microsoft
Problem: You get the preorder and inorder lists of a tree with unique values. Rebuild the tree.
Solve it on LeetCode ↗ · Video walkthrough ↗
preorder = [3,9,20,15,7], inorder = [9,3,15,20,7][3,9,20,null,null,15,7]preorder = [-1], inorder = [-1][-1]Constraints: 1 ≤ preorder.length ≤ 3000 · inorder.length == preorder.length · -3000 ≤ preorder[i], inorder[i] ≤ 3000 · preorder and inorder consist of unique values. · Each value of inorder also appears in preorder. · preorder is guaranteed to be the preorder traversal of the tree. · inorder is guaranteed to be the inorder traversal of the tree.
Hint 1
The first value of the preorder is the root. Where is it in the inorder list?
Hint 2
Everything left of the root in inorder is the left subtree; recurse, using a value → index map to find the root in O(1).
From brute force to the pattern: Searching the inorder list for each root is O(n) per node, O(n²) overall. A hash map from value to inorder index makes every split O(1): O(n).
Spot it: "construct from traversals" means you have to know exactly what each order guarantees.
Key insight: Preorder's first value is always the root. Find that root in inorder: everything to its left is the left subtree, everything to its right is the right subtree. Keep a pointer into preorder and build left before right, because preorder is root → left → right. A hash map gives O(1) lookups of positions in inorder.
Approach:
- Build a map
value → index in inorderfor O(1) lookups. - Keep a pointer
self.next_rootinto preorder. The value there is the root of the subtree you're about to build. - The helper method
build(low, high)builds the subtree whose values areinorder[low..high]. Iflow > high, return None. - Take
preorder[self.next_root]as the root, advanceself.next_root, and find its inorder positionmiddle. - Build the left subtree from
(low, middle-1)FIRST, then the right subtree from(middle+1, high). That's the same order preorder uses.
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
# Remember where each value sits in "inorder", so we can find it in O(1).
self.index_in_inorder = {}
for i in range(len(inorder)):
value = inorder[i]
self.index_in_inorder[value] = i
# Keep preorder on self so the helper method can read it.
self.preorder = preorder
# Position in preorder of the next root we will create.
self.next_root = 0
# Build the whole tree: it uses every position of inorder, 0 .. n-1.
return self.build(0, len(inorder) - 1)
def build(self, low: int, high: int) -> Optional[TreeNode]:
# Builds the subtree made of the values inorder[low..high] (helper method).
if low > high:
return None # empty range: no subtree here
# The next value in preorder is the root of this subtree.
root_value = self.preorder[self.next_root]
self.next_root = self.next_root + 1
node = TreeNode(root_value)
# Everything left of the root in inorder is its left subtree, the rest is its right.
middle = self.index_in_inorder[root_value]
node.left = self.build(low, middle - 1) # LEFT first: same order as preorder
node.right = self.build(middle + 1, high)
return node
What the code is doing, line by line:
- The
forloop buildsself.index_in_inorder: value → its position ininorder. Looking up a root's position is then O(1). self.preorderandself.next_rootare stored onselfso the helper methodbuildcan read and advance them across recursive calls.build(low, high)builds the subtree whose values areinorder[low..high]. If the range is empty (low > high), there is no subtree: return None.- The value at
preorder[self.next_root]is the root of this subtree (preorder always lists the root first). We create the node and move the pointer forward. middleis where that root sits in inorder. Positionslow..middle-1are its left subtree,middle+1..highits right subtree.- The left subtree is built before the right one, because preorder lists root, then all of the left subtree, then the right.
Dry run on preorder = [3,9,20,15,7], inorder = [9,3,15,20,7] (each row is one call that creates a node; calls with an empty range just return None):
| Call | Root taken from preorder | middle | Left range | Right range |
|---|---|---|---|---|
build(0, 4) | 3 | 1 | 0..0 | 2..4 |
build(0, 0) | 9 | 0 | 0..-1 (empty) | 1..0 (empty) |
build(2, 4) | 20 | 3 | 2..2 | 4..4 |
build(2, 2) | 15 | 2 | empty | empty |
build(4, 4) | 7 | 4 | empty | empty |
Edge cases to test:
- A single node.
- A left-skewed or right-skewed tree.
- Values are unique, which is what makes the map work.
Complexity: O(n) time, O(n) space for the map (plus O(h) recursion).
Hard 124. Binary Tree Maximum Path Sum Google Meta Amazon Microsoft
Problem: A path is any chain of connected nodes (it doesn't have to pass through the root, and it can't branch). Return the largest possible sum of node values on a path. Values may be negative.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [1,2,3]6root = [-10,9,20,null,null,15,7]42Constraints: The number of nodes in the tree is in the range [1, 3 * 10⁴]. · -1000 ≤ Node.val ≤ 1000
Hint 1
A path can bend at one node: down its left side and down its right side.
Hint 2
Post-order: each node returns its best one-sided gain (ignore negative gains) and updates a global best with node.val + left + right.
From brute force to the pattern: Trying every pair of endpoints and summing the path between them is O(n²) or worse. A single post-order pass computes the best path bending at each node: O(n).
Spot it: "max path" in a tree, and the best answer at a node depends on the best answers below it. That's postorder.
Key insight: Each node does two different jobs. (1) It updates the global best with a path that bends at it: val + left + right. (2) It returns to its parent a path that goes down only one side: val + max(left, right). A parent can only extend one branch. Clamp negative branch gains to 0 (just don't take that branch).
Approach:
- Write a helper method
gain(node)= the best sum of a path that starts atnodeand goes down one side. - Postorder: compute
left_gainandright_gainfrom the children, and replace a negative gain with 0. Negative branches are dropped. - Update
self.bestwithnode.val + left_gain + right_gain, the path that bends at this node. - Return
node.valplus the bigger ofleft_gainandright_gain. A parent can extend only one branch. - The answer is
self.bestafter callingself.gain(root).
class Solution:
def maxPathSum(self, root: Optional[TreeNode]) -> int:
# Best path sum seen anywhere so far. Start at "minus infinity"
# so that even a single negative node can beat it.
self.best = float("-inf")
self.gain(root) # walks the whole tree and updates self.best
return self.best
def gain(self, node: Optional[TreeNode]) -> int:
# Helper method: best sum of a path that STARTS at node and goes DOWN one side.
if node is None:
return 0
# Postorder: get the children's answers first.
left_gain = self.gain(node.left)
if left_gain < 0:
left_gain = 0 # a negative branch only hurts: don't take it
right_gain = self.gain(node.right)
if right_gain < 0:
right_gain = 0
# Job 1: a path that BENDS here uses both sides. Is it the best so far?
path_through_node = node.val + left_gain + right_gain
if path_through_node > self.best:
self.best = path_through_node
# Job 2: the parent can extend only ONE side, so return the better one.
if left_gain > right_gain:
return node.val + left_gain
else:
return node.val + right_gain
What the code is doing, line by line:
maxPathSumsetsself.bestto minus infinity, calls the helpergainon the root, and returns whatever best was found.gain(node)returns the best sum of a path that starts atnodeand goes down one side. An empty subtree gives 0.- It first computes the children's gains (postorder). A negative gain is replaced by 0, meaning "don't go down that branch".
path_through_nodeis a path that bends at this node and uses both sides. If it beatsself.best, it becomes the new best.- Finally it returns the node's value plus the better of the two sides, because a parent can only continue the path down one side.
Dry run on root = [-10,9,20,null,null,15,7] (rows are in postorder, the order gain finishes):
| Node | left_gain | right_gain | path_through_node | self.best | returns |
|---|---|---|---|---|---|
| 9 | 0 | 0 | 9 | 9 | 9 |
| 15 | 0 | 0 | 15 | 15 | 15 |
| 7 | 0 | 0 | 7 | 15 | 7 |
| 20 | 15 | 7 | 42 | 42 | 35 |
| -10 | 9 | 35 | 34 | 42 | 25 |
The answer is 42: the path 15 → 20 → 7.
Edge cases to test:
- All negative values → the single largest value.
- A single node.
- The best path doesn't pass through the root.
Complexity: O(n) time, O(h) space.
Hard 297. Serialize and Deserialize Binary Tree Google Meta Amazon Microsoft
Problem: Design serialize(root) -> str and deserialize(str) -> root so that a tree survives the round trip.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [1,2,3,null,null,4,5][1,2,3,null,null,4,5]root = [][]Constraints: The number of nodes in the tree is in the range [0, 10⁴]. · -1000 ≤ Node.val ≤ 1000
Hint 1
Pick a traversal that records missing children explicitly, e.g. pre-order with a marker for None.
Hint 2
Deserialise by reading the tokens back in the same order with an index or iterator.
From brute force to the pattern: This is a design task rather than an optimisation. The main pitfall is leaving out the None markers, which makes the tree ambiguous. A pre-order or level-order encoding with explicit nulls is O(n) both ways.
Spot it: "encode/decode a tree": preorder with explicit null markers is self-describing.
Key insight: If you write # for every missing child, a preorder string alone determines the tree. That's different from 105, which needed two traversals. Deserialize by reading tokens in the same preorder: make the node, then build its left, then its right.
Approach:
- Serialize: preorder walk (helper method
serialize_walk). Write the value for a node and#for every missing child, then join with commas. - Deserialize: split on commas and keep
self.position, the index of the next token to read. deserialize_build()reads one token.#means None. Otherwise create the node, then calldeserialize_build()for its left, then its right.- Because both directions use the same preorder, the tokens are consumed in exactly the order they were written.
class Codec:
def serialize(self, root: Optional[TreeNode]) -> str:
tokens = [] # one string per node, "#" for a missing child
self.serialize_walk(root, tokens)
# [1,2,3,null,null,4,5] -> "1,2,#,#,3,4,#,#,5,#,#"
return ",".join(tokens)
def serialize_walk(self, node: Optional[TreeNode], tokens: List[str]) -> None:
# Helper method: PREORDER walk (me, then left, then right).
if node is None:
tokens.append("#") # null marker keeps the shape of the tree
return
tokens.append(str(node.val))
self.serialize_walk(node.left, tokens)
self.serialize_walk(node.right, tokens)
def deserialize(self, data: str) -> Optional[TreeNode]:
self.tokens = data.split(",") # "1,2,#" -> ["1", "2", "#"]
self.position = 0 # index of the next token to read
return self.deserialize_build()
def deserialize_build(self) -> Optional[TreeNode]:
# Helper method: read tokens in the SAME preorder that serialize wrote them.
token = self.tokens[self.position]
self.position = self.position + 1
if token == "#":
return None # a missing child
node = TreeNode(int(token))
node.left = self.deserialize_build() # next tokens describe the left subtree
node.right = self.deserialize_build() # then the right subtree
return node
What the code is doing, line by line:
serializecreates an empty listtokens, lets the helper methodserialize_walkfill it, and joins the tokens with commas.serialize_walkis a preorder walk: a missing child adds"#"; a real node adds its value, then walks left, then right.deserializesplits the string back into a list and setsself.position = 0, the index of the next token to read.deserialize_buildreads one token and moves the position on."#"means None. Otherwise it creates the node and builds its left child, then its right child, from the following tokens.- Because both directions use the same preorder (me, left, right), tokens are read back in exactly the order they were written.
Dry run of serialize on [1,2,3,null,null,4,5]:
| Step | Visiting | tokens joined so far |
|---|---|---|
| 1 | node 1 | 1 |
| 2 | node 2 | 1,2 |
| 3 | 2's left (None) | 1,2,# |
| 4 | 2's right (None) | 1,2,#,# |
| 5 | node 3 | 1,2,#,#,3 |
| 6 | node 4 | 1,2,#,#,3,4 |
| 7 | 4's left (None) | 1,2,#,#,3,4,# |
| 8 | 4's right (None) | 1,2,#,#,3,4,#,# |
| 9 | node 5 | 1,2,#,#,3,4,#,#,5 |
| 10 | 5's left (None) | 1,2,#,#,3,4,#,#,5,# |
| 11 | 5's right (None) | 1,2,#,#,3,4,#,#,5,#,# |
deserialize reads those 11 tokens in the same order: 1 becomes the root, 2 its left child, the two # close 2's children, 3 becomes the root's right child, and so on.
Edge cases to test:
- An empty tree.
- Negative and multi-digit values (use a separator between tokens).
- A skewed tree (deep recursion; level order avoids it).
Complexity: O(n) time and space for both directions.
8. Common mistakes
node is None first, or you'll get AttributeError: 'NoneType' object has no attribute 'left'.prev, or pass (lo, hi) bounds down.right first so left pops first.len(queue) in level orderIf you loop while queue without capturing the size first, children of this level mix into the current level.O(n²))Calling height() inside every node's check (as in "is balanced" or "diameter") re-walks subtrees. Return the height from the same postorder pass.sys.setrecursionlimit when the tree is guaranteed balanced.9. Self-check quiz
You need the diameter of a tree, which requires each node to know the heights of both of its subtrees. Which order?
Inorder traversal of the BST [4,2,6,1,3,5,7] gives…
"Return the values you'd see standing on the right side of the tree, top to bottom."
Extra space used by a recursive DFS on a tree of height h?
h frames. That's log n when balanced and n when it's a stick.10. Practice list (in order)
| # | Problem | Level | Data structure | Twist |
|---|---|---|---|---|
| 94 | Binary Tree Inorder Traversal | Easy | Warm-up: write it iteratively, without looking | |
| 100 | Same Tree | Easy | Walk two trees in lock-step | |
| 101 | Symmetric Tree | Easy | Same Tree, but compare outer with outer and inner with inner | |
| 104 | Maximum Depth of Binary Tree | Easy | Depth = 1 + the deeper subtree | |
| 108 | Convert Sorted Array to Binary Search Tree | Easy | The middle element is the root; recurse on each half | |
| 110 | Balanced Binary Tree | Easy | Return the height, or -1 as an "unbalanced" signal | |
| 226 | Invert Binary Tree | Easy | Post-order: invert both subtrees, then swap them | |
| 530 | Minimum Absolute Difference in BST | Easy | In-order gives sorted values; compare neighbours only | |
| 543 | Diameter of Binary Tree | Easy | Return the height up, record left + right on the side | |
| 572 | Subtree of Another Tree | Easy | Traverse root; at each node run Same Tree against subRoot | |
| 102 | Binary Tree Level Order Traversal | Medium | The level-order template itself | |
| 103 | Binary Tree Zigzag Level Order Traversal | Medium | Reverse every other level | |
| 199 | Binary Tree Right Side View | Medium | Last node of each level | |
| 98 | Validate Binary Search Tree | Medium | Inorder must be strictly increasing | |
| 230 | Kth Smallest Element in a BST | Medium | Iterative inorder, stop early | |
| 236 | Lowest Common Ancestor of a Binary Tree | Medium | Postorder: "did either side find p or q?" | |
| 105 | Construct Binary Tree from Preorder and Inorder | Medium | Preorder gives roots, inorder gives splits | |
| 173 | Binary Search Tree Iterator | Medium | Iterative inorder, paused between calls | |
| 106 | Construct Binary Tree from Inorder and Postorder Traversal | Medium | Postorder's last value is the root; build the right subtree first | |
| 114 | Flatten Binary Tree to Linked List | Medium | Splice each left subtree in between the node and its right subtree | |
| 235 | Lowest Common Ancestor of a Binary Search Tree | Medium | Use BST order: walk down until p and q split | |
| 427 | Construct Quad Tree | Medium | Divide the square into four until each part is uniform | |
| 95 | Unique Binary Search Trees II | Medium | Pick each value as the root; combine every left subtree of smaller values with every right subtree of larger ones | |
| 96 | Unique Binary Search Trees | Medium | count[n] = Σ count[left size] · count[right size] over every root (Catalan numbers) | |
| 99 | Recover Binary Search Tree | Medium | In-order should be increasing; the two swapped nodes are where it decreases | |
| 107 | Binary Tree Level Order Traversal II | Medium | Normal BFS level by level, then reverse the list of levels | |
| 116 | Populating Next Right Pointers in Each Node | Medium | Use the level above, already linked by next, to link the level below in O(1) space | |
| 222 | Count Complete Tree Nodes | Medium | If the leftmost and rightmost depths match, the subtree is perfect: 2^h − 1 nodes | |
| 366 | Find Leaves of Binary Tree | Medium | Group nodes by height (distance from the deepest leaf below), computed post-order | |
| 450 | Delete Node in a BST | Medium | Two children: replace the value with the in-order successor, then delete the successor from the right subtree | |
| 662 | Maximum Width of Binary Tree | Medium | Number nodes like a heap (children 2i, 2i+1); width = last index − first index + 1 per level | |
| 701 | Insert into a Binary Search Tree | Medium | Walk down like a search; attach the new node where the search falls off the tree | |
| 979 | Distribute Coins in Binary Tree | Medium | Each edge carries |excess of the subtree below it| coins; post-order sums them | |
| 1110 | Delete Nodes And Return Forest | Medium | Post-order: a kept node becomes a new root when its parent was deleted | |
| 1161 | Maximum Level Sum of a Binary Tree | Medium | BFS level by level; keep the first level with the largest sum | |
| 2096 | Step-By-Step Directions From a Binary Tree Node to Another | Medium | Root-to-node paths for both; drop the common prefix (the LCA); 'U' for the rest of start's path | |
| 2196 | Create Binary Tree From Descriptions | Medium | Create nodes on demand in a map; the root is the only value that is never a child | |
| 113 | Path Sum II | Medium | DFS with a path list: append on the way down, record at a leaf with the right sum, pop on the way back | |
| 314 | Binary Tree Vertical Order Traversal | Medium | BFS carrying a column number; append to columns[col] in BFS order (top to bottom, left to right) | |
| 545 | Boundary of Binary Tree | Medium | Root + left boundary (no leaves) + all leaves left to right + right boundary reversed (no leaves) | |
| 549 | Binary Tree Longest Consecutive Sequence II | Medium | Post-order returns (longest increasing run down, longest decreasing run down); a path through a node joins inc + dec − 1 | |
| 1382 | Balance a Binary Search Tree | Medium | In-order gives the sorted values; rebuild by always choosing the middle as the root | |
| 1650 | Lowest Common Ancestor of a Binary Tree III | Medium | Parent pointers make two linked lists ending at the root: walk both, switching heads, until they meet | |
| 2265 | Count Nodes Equal to Average of Subtree | Medium | Post-order returns (sum, count) of each subtree; compare node.val with sum // count | |
| 337 | House Robber III | Medium | Post-order returns (best if this node is robbed, best if it isn't) | |
| 988 | Smallest String Starting From Leaf | Medium | DFS building the root-to-node path; at a leaf, compare the reversed path | |
| 1038 | Binary Search Tree to Greater Sum Tree | Medium | Reverse in-order (right, node, left) with a running sum | |
| 1315 | Sum of Nodes with Even-Valued Grandparent | Medium | DFS passing (parent, grandparent) down | |
| 2385 | Amount of Time for Binary Tree to Be Infected | Medium | Add parent links, then BFS from the start node; the number of layers is the answer | |
| 2583 | Kth Largest Sum in a Binary Tree | Medium | BFS level sums, then the k-th largest (a size-k min-heap or a sort) | |
| 3558 | Number of Ways to Assign Edge Weights I | Medium | Find the maximum depth d by BFS; half of the 2^d weightings are odd: 2^(d−1) | |
| 1443 | Minimum Time to Collect All Apples in a Tree | Medium | Post-order: a child's subtree costs (its time + 2) if it contains an apple | |
| 889 | Construct Binary Tree from Preorder and Postorder Traversal | Medium | preorder[1] is the left child's root; its position in postorder gives the left subtree's size | |
| 124 | Binary Tree Maximum Path Sum | Hard | Postorder: update global, return one side | |
| 297 | Serialize and Deserialize Binary Tree | Hard | Preorder with null markers | |
| 834 | Sum of Distances in Tree | Hard | Rerooting: two DFS passes. Moving the root to a child changes the sum by n − 2·size(child) | |
| 987 | Vertical Order Traversal of a Binary Tree | Hard | Record (col, row, val) for every node, sort, and group by column | |
| 1483 | Kth Ancestor of a Tree Node | Hard | Binary lifting: up[j][v] = the 2^j-th ancestor; jump by the set bits of k | |
| 3559 | Number of Ways to Assign Edge Weights II | Hard | A path of L edges has 2^(L−1) odd-cost weightings; L comes from depths and the LCA (binary lifting) | |
| 968 | Binary Tree Cameras | Hard | Greedy post-order with 3 states: needs cover, has camera, covered; place cameras at parents of uncovered leaves | |
| 1028 | Recover a Tree From Preorder Traversal | Hard | Parse (depth, value) pairs; a stack holds the current root-to-node path, popped to depth before attaching | |
| 1373 | Maximum Sum BST in Binary Tree | Hard | Post-order returns (is BST, min, max, sum) so each subtree is checked once | |
| 1932 | Merge BSTs to Create Single BST | Hard | The root is the tree whose value is no other tree's leaf; repeatedly attach trees at matching leaves, then validate | |
| 428 | Serialize and Deserialize N-ary Tree | Hard | Preorder with a child count after each value; rebuild by reading value, count, then that many subtrees | |
| 2791 | Count Paths That Can Form a Palindrome in a Tree | Hard | mask[v] = parity of letters from the root; a path u–v works if mask[u] ^ mask[v] has at most one bit |
Worked solutions for the practice list
Problems already solved in section 7 link back up there. Everything else is solved here.
Easy 94. Binary Tree Inorder Traversal Start here Google Meta Amazon
Problem: Return the values of a binary tree in inorder (left, node, right). Do it without recursion.
root = [1,null,2,3][1,3,2]root = [1,2,3,4,5,null,8,null,null,6,7,9][4,2,6,5,7,1,3,9,8]Constraints: The number of nodes in the tree is in the range [0, 100]. · -100 ≤ Node.val ≤ 100
Hint 1
In-order = left subtree, node, right subtree.
Hint 2
Iteratively: push nodes while going left, pop one, visit it, then go to its right child.
From brute force to the pattern: The recursive version is three lines but uses the call stack. The explicit-stack version gives the same O(n) traversal and is the basis for BST iterators and early stopping.
Spot it: "inorder" is literally in the name. The iterative version is the building block for 98, 173 and 230.
Approach:
- Keep a stack of ancestors you still need to come back to, and a pointer
current. - Slide down-left from
current, pushing every node. - Pop: that's the leftmost node not yet visited. Record its value.
- Move to its right child and repeat until both
currentis None and the stack is empty.
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
out = [] # the answer, in inorder
stack = [] # ancestors we still have to come back to
current = root
# Still a node to visit, or an ancestor to return to?
while current is not None or len(stack) > 0:
# Slide left, remembering every ancestor.
while current is not None:
stack.append(current)
current = current.left
# Leftmost node not visited yet.
current = stack.pop()
out.append(current.val) # INORDER: record between left and right
# Now handle its right subtree.
current = current.right
return out
What the code is doing, line by line:
outis the answer,stackholds ancestors still to be visited,currentis our position.- The inner
whileslides left fromcurrent, pushing each node. stack.pop()returns the leftmost node not yet visited. Its value is recorded (left side is already done, right side comes next: that's inorder).current = current.rightmoves into the right subtree. The outer loop stops when there is no current node and the stack is empty.
Dry run on root = [1,null,2,3] (1 at the root, 2 its right child, 3 the left child of 2):
| Step | Stack after sliding left | Popped | out so far |
|---|---|---|---|
| 1 | [1] | 1 | [1] |
| 2 | [2, 3] | 3 | [1, 3] |
| 3 | [2] | 2 | [1, 3, 2] |
Edge cases to test:
- An empty tree →
[]. - Only left children.
- Only right children.
Complexity: O(n) time, O(h) space.
Medium 102. Binary Tree Level Order Traversal Google Meta Amazon Microsoft
Problem: Return the node values level by level, from top to bottom and left to right within each level, as a list of lists.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [3,9,20,null,null,15,7][[3],[9,20],[15,7]]root = [1][[1]]Constraints: The number of nodes in the tree is in the range [0, 2000]. · -1000 ≤ Node.val ≤ 1000
Hint 1
Visit nodes level by level: that's BFS with a queue.
Hint 2
At the start of each round, len(queue) is exactly the number of nodes on the current level.
From brute force to the pattern: Computing each node's depth with DFS and grouping by depth also works in O(n). BFS produces the levels directly, in order, with one queue.
Spot it: "level by level" means BFS on the tree with a queue.
Approach:
- If the tree is empty, return
[]. Otherwise put the root in a deque. - While the queue isn't empty, freeze
level_size = len(queue). - Pop exactly
level_sizenodes, collect their values intolevel, and push their children. - Append
levelto the result.
class Solution:
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
if root is None:
return []
result = []
# deque: queue.append(x) adds at the BACK, queue.popleft() takes from the FRONT.
queue = deque()
queue.append(root)
while len(queue) > 0:
level_size = len(queue) # freeze: exactly the nodes of THIS level
level = []
for _ in range(level_size):
node = queue.popleft()
level.append(node.val)
# Children go to the back of the queue: they are the next level.
if node.left is not None:
queue.append(node.left)
if node.right is not None:
queue.append(node.right)
result.append(level)
return result
What the code is doing, line by line:
- An empty tree returns
[]. Otherwise the root goes into the queue. - Each round of the outer loop handles one level.
level_sizeis read first, so later children don't sneak into this level. - The inner loop pops exactly
level_sizenodes from the front, records their values, and appends their children to the back. - The finished
levellist is appended toresult.
Dry run on root = [3,9,20,null,null,15,7]:
| Round | Queue at start | level_size | level | Queue after |
|---|---|---|---|---|
| 1 | [3] | 1 | [3] | [9, 20] |
| 2 | [9, 20] | 2 | [9, 20] | [15, 7] |
| 3 | [15, 7] | 2 | [15, 7] | [] |
Result: [[3], [9, 20], [15, 7]].
Edge cases to test:
- An empty tree →
[]. - A skewed tree (one node per level).
- A complete tree.
Complexity: O(n) time, O(w) space (w = widest level).
Medium 103. Binary Tree Zigzag Level Order Traversal Google Meta Amazon Microsoft
Problem: Like level order, but alternate direction: level 0 left→right, level 1 right→left, and so on.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [3,9,20,null,null,15,7][[3],[20,9],[15,7]]root = [1][[1]]Constraints: The number of nodes in the tree is in the range [0, 2000]. · -100 ≤ Node.val ≤ 100
Hint 1
Do a normal level-order traversal.
Hint 2
Reverse every other level (or insert at the front of a deque on those levels).
From brute force to the pattern: Level-order BFS plus reversing alternate levels is O(n). Building each level with a deque avoids the extra reverse step.
Spot it: Still "per level", so it's level order. Only the way each level is written changes.
Approach:
- Run the normal level-order BFS. The queue order stays left→right.
- For each level, collect values into a list
level, always left to right. - When going left→right add
levelas it is. When going right→left copy it backwards (with awhileloop) and add the copy. - Flip the direction flag after every level.
class Solution:
def zigzagLevelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
if root is None:
return []
result = []
# deque: queue.append(x) adds at the BACK, queue.popleft() takes from the FRONT.
queue = deque()
queue.append(root)
left_to_right = True # direction for the current level
while len(queue) > 0:
level_size = len(queue) # freeze the size of this level
level = [] # values of this level, always left to right
for _ in range(level_size):
node = queue.popleft()
level.append(node.val)
# Children ALWAYS go in left-to-right; only the output is flipped.
if node.left is not None:
queue.append(node.left)
if node.right is not None:
queue.append(node.right)
if left_to_right:
result.append(level)
else:
# Copy the level backwards into a new list.
reversed_level = []
i = len(level) - 1
while i >= 0:
reversed_level.append(level[i])
i = i - 1
result.append(reversed_level)
left_to_right = not left_to_right # flip the direction for the next level
return result
What the code is doing, line by line:
- This is the level-order code from 102, plus a flag
left_to_rightthat starts as True. - Inside each level the queue is handled exactly as in 102, so
levelis always collected left to right. - If the flag is True,
levelis added as it is. If it is False, awhileloop copies it from the last item to the first intoreversed_level, and that is added instead. left_to_right = not left_to_rightflips the direction after every level.
Dry run on root = [3,9,20,null,null,15,7]:
| Round | Queue at start | level (left to right) | left_to_right | Added to result |
|---|---|---|---|---|
| 1 | [3] | [3] | True | [3] |
| 2 | [9, 20] | [9, 20] | False | [20, 9] |
| 3 | [15, 7] | [15, 7] | True | [15, 7] |
Result: [[3], [20, 9], [15, 7]].
Edge cases to test:
- An empty tree.
- A single node.
- Levels with missing children.
Complexity: O(n) time, O(w) space.
Medium 199. Binary Tree Right Side View Google Meta Amazon Microsoft
Problem: Imagine standing to the right of the tree. Return the values you can see, from top to bottom. That's the rightmost node of each level.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [1,2,3,null,5,null,4][1,3,4]root = [1,2,3,4,null,null,null,5][1,3,4,5]Constraints: The number of nodes in the tree is in the range [0, 100]. · -100 ≤ Node.val ≤ 100
Hint 1
From the right, you see the last node of every level.
Hint 2
BFS level by level and record the last node of each level (or DFS visiting right before left, recording the first node at each new depth).
From brute force to the pattern: Collecting every level fully and then taking its last element is still O(n). Either BFS or right-first DFS gives the view in one pass.
Spot it: "one value per row, top to bottom" is a per-level question, so level order.
Approach:
- Run level-order BFS with a frozen size.
- While popping the
level_sizenodes of a level, remember the last one popped. - After the level ends, append that last value: it's the rightmost node.
class Solution:
def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
if root is None:
return []
view = [] # one value per level: the rightmost one
# deque: queue.append(x) adds at the BACK, queue.popleft() takes from the FRONT.
queue = deque()
queue.append(root)
while len(queue) > 0:
level_size = len(queue) # freeze the size of this level
for i in range(level_size):
node = queue.popleft()
# The last node popped in this level is the rightmost one.
if i == level_size - 1:
view.append(node.val)
if node.left is not None:
queue.append(node.left)
if node.right is not None:
queue.append(node.right)
return view
What the code is doing, line by line:
- An empty tree has no view. Otherwise start the queue with the root.
- Each round freezes
level_sizeand pops exactly that many nodes, so one round = one level. - The counter
itells us how many nodes of this level we have popped. Wheni == level_size - 1it's the last one, which is the rightmost node, so its value goes intoview. - Children are pushed left then right, keeping the queue in left-to-right order for the next level.
Dry run on root = [1,2,3,null,5,null,4]:
| Round | Queue at start | level_size | Last node popped | view so far |
|---|---|---|---|---|
| 1 | [1] | 1 | 1 | [1] |
| 2 | [2, 3] | 2 | 3 | [1, 3] |
| 3 | [5, 4] | 2 | 4 | [1, 3, 4] |
Edge cases to test:
- A left child visible because the right side is shorter, e.g.
[1, 2]→[1, 2]. - An empty tree.
- Only right children.
Complexity: O(n) time, O(w) space.
Medium 236. Lowest Common Ancestor of a Binary Tree Google Meta Amazon Cisco
Problem: Given two nodes p and q in a binary tree (not a BST), return their lowest common ancestor: the deepest node that has both of them in its subtree. A node counts as its own descendant.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 13root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 45Constraints: The number of nodes in the tree is in the range [2, 10⁵]. · -10⁹ ≤ Node.val ≤ 10⁹ · All Node.val are unique. · p != q · p and q will exist in the tree.
Hint 1
Search both subtrees for p and q.
Hint 2
If one is found on each side, the current node is the LCA; if both are on one side, the answer comes up from that side.
From brute force to the pattern: Storing the root-to-node paths of p and q and comparing them is O(n) time and space. One post-order recursion returning the found node (or None) is O(n) with only the recursion stack.
Spot it: Each node needs to know "did my left / right subtree contain p or q?". That information flows up, so postorder.
Approach:
- Base case: if the node is None, return None; if it is
porq, return it as is. - Recurse into the left and right subtrees.
- If both sides returned something non-null,
pandqare split across this node, so this node is the LCA. - Otherwise return whichever side is non-null. It carries
p,q, or the LCA found deeper down.
class Solution:
# LeetCode calls the two targets p and q; here they are named node_p and node_q.
def lowestCommonAncestor(self, root: 'TreeNode', node_p: 'TreeNode', node_q: 'TreeNode') -> 'TreeNode':
# Ran off the tree: nothing found down here.
if root is None:
return None
# Found one of the targets: report it to the parent.
if root is node_p or root is node_q:
return root
# Postorder: ask both subtrees first.
found_left = self.lowestCommonAncestor(root.left, node_p, node_q)
found_right = self.lowestCommonAncestor(root.right, node_p, node_q)
# One target on each side: this node is where they split, so it is the LCA.
if found_left is not None and found_right is not None:
return root
# Otherwise pass up whatever was found (it may be None).
if found_left is not None:
return found_left
return found_right
What the code is doing, line by line:
- LeetCode names the two target nodes
pandq. The code calls themnode_pandnode_qso the names say what they are; LeetCode passes them by position, so this still works there. - If
rootis None we fell off the tree: nothing found, return None. - If
rootisnode_pornode_q, return it. We don't need to look below it: if the other target is underneath, this node is the LCA anyway. - Otherwise search both subtrees (postorder) and keep what each one reports in
found_leftandfound_right. - If both are non-None, one target is on each side, so this node is the lowest common ancestor.
- Otherwise pass up whichever side found something (or None if neither did).
Dry run on root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1:
| Call on node | What happens | Returns |
|---|---|---|
| 5 | it is p, stop here (6, 2, 7, 4 are never visited) | node 5 |
| 1 | it is q, stop here | node 1 |
| 3 | found_left = 5, found_right = 1, both non-None | node 3 |
Edge cases to test:
pis an ancestor ofq→p.pandqon opposite sides of the root → the root.- Deep, unbalanced trees.
Complexity: O(n) time, O(h) space.
Medium 173. Binary Search Tree Iterator Meta Microsoft
Problem: Design an iterator over a BST: next() returns the next smallest value, and hasNext() says whether any remain. Aim for average O(1) time and O(h) memory.
Solve it on LeetCode ↗ · Video walkthrough ↗
["BSTIterator", "next", "next", "hasNext", "next", "hasNext", "next", "hasNext", "next", "hasNext"]
[[[7, 3, 15, null, null, 9, 20]], [], [], [], [], [], [], [], [], []][null, 3, 7, true, 9, true, 15, true, 20, false]Constraints: The number of nodes in the tree is in the range [1, 10⁵]. · 0 ≤ Node.val ≤ 10⁶ · At most 10⁵ calls will be made to hasNext, and next.
Hint 1
The iterator must return BST values in increasing order: in-order traversal.
Hint 2
Keep a stack of the leftmost path; next() pops a node, then pushes the leftmost path of its right child.
From brute force to the pattern: Flattening the whole tree into a list in the constructor uses O(n) memory. The controlled stack keeps only O(h) nodes, with amortised O(1) per next().
Spot it: "next smallest" in a BST is inorder. To "pause" between calls, keep the iterative inorder stack as object state.
Approach:
- In the constructor, push the root and all of its left descendants (the "slide left" step).
next(): pop the top node. That's the answer.- Before returning, slide left from that node's right child, pushing nodes. That prepares the next call.
hasNext()is just "is the stack non-empty?".
class BSTIterator:
def __init__(self, root: Optional[TreeNode]):
self.stack = [] # the paused iterative-inorder stack
self.push_left(root) # smallest value ends up on top
def push_left(self, node: Optional[TreeNode]) -> None:
# The "slide left" step of iterative inorder: push node and all its left descendants.
while node is not None:
self.stack.append(node)
node = node.left
def next(self) -> int:
node = self.stack.pop() # next smallest value
self.push_left(node.right) # its successor lives in the right subtree
return node.val
def hasNext(self) -> bool:
# Values remain exactly when the stack is not empty.
return len(self.stack) > 0
What the code is doing, line by line:
- The constructor creates an empty stack and calls
push_left(root), so the path down to the smallest value is on the stack with the smallest on top. push_left(node)is the "slide left" step of iterative inorder: push the node, move to its left child, repeat until None.next()pops the top node (the next smallest value). Before returning, it slides left from that node's right child, so the following value is ready on top.hasNext()returns True while the stack still holds nodes.- Each node is pushed once and popped once, so over all calls
next()costs O(1) on average.
Dry run on root = [7,3,15,null,null,9,20]:
| Call | Returns | Stack afterwards (top is last) |
|---|---|---|
BSTIterator(root) | - | [7, 3] |
next() | 3 | [7] |
next() | 7 | [15, 9] |
next() | 9 | [15] |
next() | 15 | [20] |
next() | 20 | [] |
hasNext() | False | [] |
Edge cases to test:
- A single node.
- A skewed tree.
hasNext()called after the last value → False.
Complexity: next() is amortized O(1) because each node is pushed and popped once. O(h) memory.
Easy 100. Same Tree Google Meta Amazon Microsoft
Problem: Return True if the two binary trees p and q have the same shape and the same values.
Solve it on LeetCode ↗ · Video walkthrough ↗
p = [1,2,3], q = [1,2,3]truep = [1,2], q = [1,null,2]falseConstraints: The number of nodes in both trees is in the range [0, 100]. · -10⁴ ≤ Node.val ≤ 10⁴
Hint 1
Two trees are the same when the roots match and both pairs of subtrees are the same.
Hint 2
Handle the base cases first: both None → True; exactly one None → False.
From brute force to the pattern: Serialising both trees with null markers and comparing the strings works in O(n) but uses extra memory. A lock-step recursion stops at the first difference.
Spot it: Compare two trees node by node → one recursion that takes a node from each tree.
Approach:
- Both nodes missing → equal so far:
True. - Exactly one missing, or the values differ →
False. - Otherwise both left subtrees must be the same and both right subtrees must be the same.
class Solution:
def isSameTree(self, p: Optional[TreeNode], q: Optional[TreeNode]) -> bool:
if p is None and q is None:
return True # both empty: identical
if p is None or q is None:
return False # only one is empty: different shape
if p.val != q.val:
return False # same position, different value
# identical only if BOTH pairs of subtrees are identical
return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right)
What the code is doing, line by line:
if p is None and q is None: return True: two empty trees match.if p is None or q is None: return False: we already know they aren't bothNone, so exactly one is. The shapes differ.if p.val != q.val: return False: same position, different value.return ... and ...: both subtrees must match.andstops early: if the left side fails, the right side is never checked.
Dry run on the example:
p = [1, 2], q = [1, null, 2] (LeetCode Example 2). Returns False.
| compare (p node, q node) | result | why |
|---|---|---|
| (1, 1) | check children | both exist, values equal |
| (2, None) (the left children) | False | only one exists |
| (1, 1) | False | and short-circuits: the right children are never compared |
Edge cases to test:
- Both empty → True.
- Same values, different shape, e.g.
[1, 2]vs[1, null, 2]. - Same shape, different values.
Complexity: O(min(m, n)) time (stops at the first difference), O(h) recursion space.
Easy 101. Symmetric Tree Google Meta Amazon Microsoft
Problem: Return True if the tree is a mirror image of itself around its centre.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [1,2,2,3,4,4,3]trueroot = [1,2,2,null,3,null,3]falseConstraints: The number of nodes in the tree is in the range [1, 1000]. · -100 ≤ Node.val ≤ 100
Hint 1
A tree is symmetric when its left subtree mirrors its right subtree.
Hint 2
Compare a.left with b.right and a.right with b.left recursively.
From brute force to the pattern: Building a mirrored copy and comparing it with the original is O(n) time and space. Comparing the two halves in mirrored lock-step uses only the recursion stack.
Spot it: A tree is symmetric when its left subtree mirrors its right subtree. "Mirror" is Same Tree with crossed children: a.left pairs with b.right, and a.right with b.left.
Approach:
- Write a helper
mirror(a, b)that checks whether two subtrees mirror each other. - Both missing →
True; one missing or values differ →False. - Otherwise the outer pair
(a.left, b.right)and the inner pair(a.right, b.left)must both mirror. - The answer is
mirror(root.left, root.right).
class Solution:
def isSymmetric(self, root: Optional[TreeNode]) -> bool:
def mirror(a, b):
if a is None and b is None:
return True
if a is None or b is None:
return False
if a.val != b.val:
return False
# outer children pair up, and inner children pair up
return mirror(a.left, b.right) and mirror(a.right, b.left)
return mirror(root.left, root.right)
What the code is doing, line by line:
def mirror(a, b):true when the subtree atais the mirror image of the subtree atb.- The first three checks are the same as Same Tree: matching emptiness and equal values.
mirror(a.left, b.right): the outer edges of the two halves must match.mirror(a.right, b.left): and so must the inner ones.return mirror(root.left, root.right): the root itself sits on the mirror line, so only its two subtrees are compared.
Dry run on the example:
root = [1, 2, 2, 3, 4, 4, 3] (LeetCode Example 1). Returns True. Call the two 2s 2L and 2R.
| mirror(a, b) | values | next checks | result |
|---|---|---|---|
| (2L, 2R) | 2 = 2 | outer (3, 3), inner (4, 4) | waits |
| (3, 3) | 3 = 3 | both pairs of children are (None, None) | True |
| (4, 4) | 4 = 4 | both pairs of children are (None, None) | True |
| (2L, 2R) | True and True | True |
Edge cases to test:
- A single node → True.
- Same values but asymmetric shape, e.g.
[1, 2, 2, null, 3, null, 3]→ False. - An empty subtree on one side only.
Complexity: O(n) time, O(h) recursion space.
Easy 104. Maximum Depth of Binary Tree Start here Google Meta Amazon Microsoft
Problem: Return the number of nodes on the longest path from the root down to a leaf.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [3,9,20,null,null,15,7]3root = [1,null,2]2Constraints: The number of nodes in the tree is in the range [0, 10⁴]. · -100 ≤ Node.val ≤ 100
Hint 1
The depth of a tree = 1 + the depth of its deeper subtree.
Hint 2
Base case: an empty tree has depth 0.
From brute force to the pattern: This is already a single O(n) traversal; BFS counting levels works equally well. The recursion is the most direct form.
Spot it: A value that depends on both subtrees → post-order recursion: ask each child for its depth, then combine.
Approach:
- An empty tree has depth 0.
- Get the depth of the left and right subtrees recursively.
- This node adds one level: return
1 + max(left, right).
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if root is None:
return 0
left_depth = self.maxDepth(root.left)
right_depth = self.maxDepth(root.right)
# this node is one more level on top of its deeper subtree
return 1 + max(left_depth, right_depth)
What the code is doing, line by line:
if root is None: return 0: no node, no depth. This is what makes a leaf's depth come out as 1.left_depth,right_depth: the recursion answers the same question for each subtree.return 1 + max(...): the longest path goes through the deeper side, plus this node.
Dry run on the example:
root = [3, 9, 20, null, null, 15, 7] (LeetCode Example 1). Returns 3.
| call finishes | left depth | right depth | returns |
|---|---|---|---|
| 9 | 0 | 0 | 1 |
| 15 | 0 | 0 | 1 |
| 7 | 0 | 0 | 1 |
| 20 | 1 | 1 | 2 |
| 3 | 1 | 2 | 3 |
Edge cases to test:
- An empty tree → 0.
- A skewed tree (depth
n). - A single node → 1.
Complexity: O(n) time, O(h) recursion space.
Easy 108. Convert Sorted Array to Binary Search Tree Google Meta Amazon Microsoft
Problem: nums is sorted ascending. Build a height-balanced BST from it (subtree heights differ by at most 1 at every node).
Solve it on LeetCode ↗ · Video walkthrough ↗
nums = [-10,-3,0,5,9][0,-3,9,-10,null,5]nums = [1,3][3,1]Constraints: 1 ≤ nums.length ≤ 10⁴ · -10⁴ ≤ nums[i] ≤ 10⁴ · nums is sorted in a strictly increasing order.
Hint 1
To keep the BST balanced, which element should be the root?
Hint 2
Take the middle element as the root and recurse on the left half and the right half.
From brute force to the pattern: Inserting the values one by one into a BST in sorted order gives a skewed tree. Picking the middle element at each level gives a balanced tree in O(n).
Spot it: BST + sorted input: the in-order of the tree must equal nums. Picking the middle element as the root puts half of the values on each side, which keeps the tree balanced. Then do the same for each half.
Approach:
build(lo, hi)builds a tree fromnums[lo..hi]; an empty range givesNone.mid = (lo + hi) // 2becomes the root.- The left subtree is
build(lo, mid − 1), the right isbuild(mid + 1, hi). - Return
build(0, n − 1).
class Solution:
def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
def build(lo, hi):
if lo > hi:
return None # empty range: no node
mid = (lo + hi) // 2
node = TreeNode(nums[mid])
# smaller values on the left, larger on the right
node.left = build(lo, mid - 1)
node.right = build(mid + 1, hi)
return node
return build(0, len(nums) - 1)
What the code is doing, line by line:
def build(lo, hi):works on index ranges instead of slicing, so no list copies are made.if lo > hi: return None: an empty range is a missing child.mid = (lo + hi) // 2,TreeNode(nums[mid]): the middle value; the two halves differ in size by at most 1, which is what keeps the tree balanced.node.left = build(lo, mid - 1): everything smaller;node.right = build(mid + 1, hi): everything larger. That's exactly the BST rule.
Dry run on the example:
nums = [-10, -3, 0, 5, 9] (LeetCode Example 1). Our code returns [0, -10, 5, null, -3, null, 9], one of the accepted answers.
| build(lo, hi) | mid | node | left call | right call |
|---|---|---|---|---|
| (0, 4) | 2 | 0 | (0, 1) | (3, 4) |
| (0, 1) | 0 | −10 | (0, −1) → None | (1, 1) |
| (1, 1) | 1 | −3 | None | None |
| (3, 4) | 3 | 5 | (3, 2) → None | (4, 4) |
| (4, 4) | 4 | 9 | None | None |
Edge cases to test:
- One or two elements.
- An even length: either middle element works.
- Negative values.
Complexity: O(n) time (each element becomes one node), O(log n) recursion depth.
Easy 110. Balanced Binary Tree Google Meta Amazon Microsoft
Problem: A tree is height-balanced if, at every node, the heights of its two subtrees differ by at most 1. Check it.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [3,9,20,null,null,15,7]trueroot = [1,2,2,3,3,null,null,4,4]falseConstraints: The number of nodes in the tree is in the range [0, 5000]. · -10⁴ ≤ Node.val ≤ 10⁴
Hint 1
You need every node's subtree heights.
Hint 2
Return the height from each recursive call, and use -1 as a signal meaning "unbalanced below" so you can stop early.
From brute force to the pattern: Calling a separate height function at every node is O(n²) on skewed trees. Returning the height and the balance status together makes it O(n).
Spot it: Each node needs both subtree heights → post-order. Computing heights separately at every node costs O(n²); instead return the height and use -1 to mean "already unbalanced below".
Approach:
- A helper
height(node)returns the subtree's height, or-1if it is unbalanced anywhere. - Empty tree → height 0.
- Get both child heights; if either is
-1, pass the-1up immediately. - If they differ by more than 1, return
-1. Otherwise return1 + max(left, right). - The tree is balanced if
height(root) != -1.
class Solution:
def isBalanced(self, root: Optional[TreeNode]) -> bool:
def height(node):
# returns the height, or -1 if this subtree is unbalanced somewhere
if node is None:
return 0
left = height(node.left)
if left == -1:
return -1
right = height(node.right)
if right == -1:
return -1
if abs(left - right) > 1:
return -1
return 1 + max(left, right)
return height(root) != -1
What the code is doing, line by line:
def height(node):one function, two jobs: the height, and an "unbalanced" flag (-1, which can never be a real height).if node is None: return 0: empty subtrees have height 0.if left == -1: return -1: an unbalanced subtree makes the whole tree unbalanced. Stop early (we don't even visit the right side).if abs(left - right) > 1: return -1: this node itself breaks the rule.return 1 + max(left, right): the normal height.return height(root) != -1: balanced exactly when no-1reached the top.
Dry run on the example:
root = [1, 2, 2, 3, 3, null, null, 4, 4] (LeetCode Example 2). Returns False. Name the left 2 2a (children 3a, 3b; 3a has two children 4) and the right 2 2b (a leaf).
| call finishes | left h | right h | returns |
|---|---|---|---|
| 4 (left of 3a) | 0 | 0 | 1 |
| 4 (right of 3a) | 0 | 0 | 1 |
| 3a | 1 | 1 | 2 |
| 3b | 0 | 0 | 1 |
| 2a | 2 | 1 | 3 (difference 1, fine) |
| 2b | 0 | 0 | 1 |
| 1 | 3 | 1 | −1 (difference 2 > 1) |
height(root) is −1, so isBalanced returns False. The root is the only node that breaks the rule.
Edge cases to test:
- An empty tree → True.
- Balanced at the root but not deeper down → False.
- A long chain → False.
Complexity: O(n) time (each node visited once), O(h) recursion space.
Easy 226. Invert Binary Tree Start here Google Meta Microsoft
Problem: Mirror the binary tree (swap every node's left and right children) and return its root.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [4,2,7,1,3,6,9][4,7,2,9,6,3,1]root = [2,1,3][2,3,1]Constraints: The number of nodes in the tree is in the range [0, 100]. · -100 ≤ Node.val ≤ 100
Hint 1
Inverting a tree = swapping the left and right children of every node.
Hint 2
Recursively invert both subtrees, then swap them.
From brute force to the pattern: This is already O(n): every node must be visited once. Recursion or a BFS with a queue both work.
Spot it: "Do something to every node" on a tree → a recursive traversal. Each node's job is local: swap its two children.
Approach:
- An empty tree is already inverted: return
None. - Recursively invert the left subtree and the right subtree.
- Swap them: the inverted right becomes the new left and vice versa.
- Return the node.
class Solution:
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if root is None:
return None
# invert both subtrees first (post-order) ...
left = self.invertTree(root.left)
right = self.invertTree(root.right)
# ... then swap them
root.left = right
root.right = left
return root
What the code is doing, line by line:
if root is None: return None: the base case. Every leaf's missing children end here.left = self.invertTree(root.left)andright = ...: trust the recursion to mirror each subtree completely.root.left = right,root.right = left: swap the two (already mirrored) subtrees. Saving them in variables first means no temporary is needed.return root: the parent call attaches this mirrored subtree on its opposite side.
Dry run on the example:
root = [4, 2, 7, 1, 3, 6, 9] (LeetCode Example 1). Returns [4, 7, 2, 9, 6, 3, 1]. Rows are in the order the calls finish.
| call finishes | children after the swap (left, right) |
|---|---|
| 1 | (None, None) |
| 3 | (None, None) |
| 2 | (3, 1) |
| 6 | (None, None) |
| 9 | (None, None) |
| 7 | (9, 6) |
| 4 | (7, 2) |
Edge cases to test:
- An empty tree.
- A single node.
- A skewed tree.
Complexity: O(n) time (every node once), O(h) space for the recursion stack (h = height).
Easy 530. Minimum Absolute Difference in BST Google
Problem: Return the minimum absolute difference between the values of any two different nodes in a BST.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [4,2,6,1,3]1root = [1,0,48,null,null,12,49]1Constraints: The number of nodes in the tree is in the range [2, 10⁴]. · 0 ≤ Node.val ≤ 10⁵
Hint 1
In a sorted list, the closest pair is always adjacent.
Hint 2
An in-order traversal of a BST is sorted; compare each value with the previous one.
From brute force to the pattern: Comparing every pair of nodes is O(n²). An in-order walk compares only neighbours in sorted order: O(n).
Spot it: An in-order traversal of a BST visits values in sorted order, and in a sorted list the closest pair is always adjacent. So walk in order and compare each value with the previous one.
Approach:
- Do an in-order traversal (left, node, right), remembering the previously visited value
prev. - At each node, if there is a
prev, updatebestwithnode.val − prev. - Set
prev = node.valand continue to the right subtree.
class Solution:
def getMinimumDifference(self, root: Optional[TreeNode]) -> int:
prev = None # value of the previous node in sorted order
best = float("inf")
def inorder(node):
nonlocal prev, best
if node is None:
return
inorder(node.left)
if prev is not None:
# sorted order: the closest value to node.val is its neighbour
best = min(best, node.val - prev)
prev = node.val
inorder(node.right)
inorder(root)
return best
What the code is doing, line by line:
prev = None: no value visited yet.best = float("inf"): any real difference beats infinity.inorder(node.left)→ handlenode→inorder(node.right): that order lists BST values from smallest to largest.if prev is not None: best = min(best, node.val - prev): values are increasing, so the difference is never negative andabsisn't needed.prev = node.val: this node becomes the neighbour of the next one.
Dry run on the example:
root = [4, 2, 6, 1, 3] (LeetCode Example 1). In-order: 1, 2, 3, 4, 6. Returns 1.
| visit | prev | difference | best |
|---|---|---|---|
| 1 | — | — | ∞ |
| 2 | 1 | 1 | 1 |
| 3 | 2 | 1 | 1 |
| 4 | 3 | 1 | 1 |
| 6 | 4 | 2 | 1 |
Edge cases to test:
- Two nodes.
- The closest pair spans the root (e.g. the root and its in-order successor).
- A skewed tree.
Complexity: O(n) time, O(h) recursion space.
Easy 543. Diameter of Binary Tree Google Meta Amazon Microsoft
Problem: The diameter is the number of edges on the longest path between any two nodes. The path does not have to pass through the root. Return it.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [1,2,3,4,5]3root = [1,2]1Constraints: The number of nodes in the tree is in the range [1, 10⁴]. · -100 ≤ Node.val ≤ 100
Hint 1
The longest path bends at some node: down the left side, then down the right side.
Hint 2
Post-order: return each subtree's height and update a global best with left + right at every node.
From brute force to the pattern: Computing heights separately for each node as the bend point is O(n²). Returning heights while tracking the best left + right is O(n).
Spot it: The longest path bends at some node: down its left side and down its right side. At every node, left height + right height is a candidate. Post-order recursion gives both heights; a variable outside the recursion keeps the best candidate.
Approach:
best = 0holds the longest path seen.- A helper
height(node)returns how many nodes are on the longest downward path fromnode(0 forNone). - At each node: get the left and right heights. The path that bends here has
left + rightedges; updatebest. - Return
1 + max(left, right)to the parent.
class Solution:
def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
best = 0
def height(node):
nonlocal best
if node is None:
return 0
left = height(node.left)
right = height(node.right)
# the longest path that bends at this node uses left + right edges
best = max(best, left + right)
# the parent can only extend ONE side of this node
return 1 + max(left, right)
height(root)
return best
What the code is doing, line by line:
best = 0: the answer, updated as the recursion visits every node.nonlocal best: lets the inner function changebestfrom the outer function (without it, Python would create a new localbest).left = height(node.left),right = height(node.right): nodes on the longest path down each side.best = max(best, left + right): going downleftnodes on one side andrightnodes on the other is a path of exactlyleft + rightedges through this node.return 1 + max(left, right): a parent can continue the path through only one of our sides, so it gets the taller one.
Dry run on the example:
root = [1, 2, 3, 4, 5] (LeetCode Example 1). Returns 3 (path 4 → 2 → 1 → 3 or 5 → 2 → 1 → 3).
| call finishes | left | right | path through it | best | returns |
|---|---|---|---|---|---|
| 4 | 0 | 0 | 0 | 0 | 1 |
| 5 | 0 | 0 | 0 | 0 | 1 |
| 2 | 1 | 1 | 2 | 2 | 2 |
| 3 | 0 | 0 | 0 | 2 | 1 |
| 1 | 2 | 1 | 3 | 3 | 3 |
Edge cases to test:
- A single node → 0.
- The longest path doesn't go through the root.
- A skewed tree.
Complexity: O(n) time, O(h) recursion space.
Easy 572. Subtree of Another Tree Google Amazon
Problem: Return True if some node of root, together with all its descendants, is identical to the tree subRoot.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [3,4,5,1,2], subRoot = [4,1,2]trueroot = [3,4,5,1,2,null,null,null,null,0], subRoot = [4,1,2]falseConstraints: The number of nodes in the root tree is in the range [1, 2000]. · The number of nodes in the subRoot tree is in the range [1, 1000]. · -10⁴ ≤ root.val ≤ 10⁴ · -10⁴ ≤ subRoot.val ≤ 10⁴
Hint 1
Every node of root is a possible starting point.
Hint 2
At each node, run Same Tree against subRoot; otherwise recurse into the children.
From brute force to the pattern: Same Tree from every node is O(m · n) in the worst case, which is acceptable here. Serialising both trees and doing a substring search (with careful separators) or tree hashing gets close to O(m + n).
Spot it: Two layers: a traversal that visits every candidate node of root, and Same Tree (LeetCode 100) to compare a candidate with subRoot.
Approach:
- Write
same(a, b)exactly as in Same Tree. - If
rootisNone, there is nothing to match:False. - If
same(root, subRoot), we're done:True. - Otherwise the match must be inside the left or right subtree: recurse.
class Solution:
def isSubtree(self, root: Optional[TreeNode], subRoot: Optional[TreeNode]) -> bool:
def same(a, b):
if a is None and b is None:
return True
if a is None or b is None or a.val != b.val:
return False
return same(a.left, b.left) and same(a.right, b.right)
if root is None:
return False
if same(root, subRoot):
return True
# not at this node: try every node below it
return self.isSubtree(root.left, subRoot) or self.isSubtree(root.right, subRoot)
What the code is doing, line by line:
def same(a, b):LeetCode 100 in a few lines: equal shape and equal values.if root is None: return False: ran out of candidates on this path.if same(root, subRoot): return True: the subtree starting here matches exactly.return ... or ...: look for a match anywhere in the left subtree, otherwise anywhere in the right subtree.orstops at the first success.
Dry run on the example:
root = [3, 4, 5, 1, 2], subRoot = [4, 1, 2] (LeetCode Example 1). Returns True.
| candidate in root | same(candidate, subRoot)? | why |
|---|---|---|
| 3 | no | 3 ≠ 4 |
| 4 | yes | 4 = 4, then (1, 1) and (2, 2) match, all leaves end together |
The left subtree of 3 matched, so or never needs to try the 5.
Edge cases to test:
subRootequal to the whole tree.- Equal values but an extra node below, e.g. a leaf vs a node with a child.
- Repeated values (several candidate starting points).
Complexity: O(m · n) time in the worst case (Same Tree from each of the m nodes), O(h) recursion space.
Medium 106. Construct Binary Tree from Inorder and Postorder Traversal Google
Problem: Given the in-order and post-order traversals of a binary tree with unique values, rebuild the tree.
Solve it on LeetCode ↗ · Video walkthrough ↗
inorder = [9,3,15,20,7], postorder = [9,15,7,20,3][3,9,20,null,null,15,7]inorder = [-1], postorder = [-1][-1]Constraints: 1 ≤ inorder.length ≤ 3000 · postorder.length == inorder.length · -3000 ≤ inorder[i], postorder[i] ≤ 3000 · inorder and postorder consist of unique values. · Each value of postorder also appears in inorder. · inorder is guaranteed to be the inorder traversal of the tree. · postorder is guaranteed to be the postorder traversal of the tree.
Hint 1
The last value of the postorder is the root.
Hint 2
Read the postorder backwards (root, right, left), building the right subtree first; find each root in the inorder list via a hash map.
From brute force to the pattern: Searching the inorder list for each root costs O(n) per node, O(n²) total. A value → index map and a pointer moving backwards through the postorder make it O(n).
Spot it: Mirror of LeetCode 105. Post-order is left, right, root, so the root is at the end. Its position in the in-order list splits the values into the left subtree and the right subtree.
Key insight: Reading post-order backwards gives root, right, left. So take values from the end of postorder and build the right subtree before the left one. A dictionary value → in-order index makes each split O(1).
Approach:
- Map every value to its index in
inorder. - Keep a pointer
post_iat the last element ofpostorder. build(lo, hi)builds the subtree whose values areinorder[lo..hi]: takepostorder[post_i]as the root and movepost_ileft.- Find the root's in-order index
mid. Build the right subtree frommid + 1..hifirst, then the left fromlo..mid − 1. - An empty range returns
None.
class Solution:
def buildTree(self, inorder: List[int], postorder: List[int]) -> Optional[TreeNode]:
# value -> its position in inorder, to split left/right in O(1)
index_of = {}
for i in range(len(inorder)):
index_of[inorder[i]] = i
# we consume postorder from the END: root, then right subtree, then left
post_i = len(postorder) - 1
def build(lo, hi):
nonlocal post_i
if lo > hi:
return None
root_val = postorder[post_i]
post_i = post_i - 1
node = TreeNode(root_val)
mid = index_of[root_val]
# right first, because postorder read backwards is root, right, left
node.right = build(mid + 1, hi)
node.left = build(lo, mid - 1)
return node
return build(0, len(inorder) - 1)
What the code is doing, line by line:
index_of: where each value sits ininorder. Values are unique, so this is well defined.post_i = len(postorder) - 1: the next root to use, starting from the end.if lo > hi: return None: no values for this subtree.root_val = postorder[post_i],post_i = post_i - 1: take the next root.mid = index_of[root_val]: in-order values left ofmidform the left subtree and values right of it form the right subtree.node.right = build(mid + 1, hi)beforenode.left: the element just before a root in post-order belongs to its right subtree. Building left first would hand the left subtree the wrong roots.
Dry run on the example:
inorder = [9, 3, 15, 20, 7], postorder = [9, 15, 7, 20, 3] (LeetCode Example 1). Returns [3, 9, 20, null, null, 15, 7].
| build(lo, hi) | takes postorder[post_i] | in-order index | right range | left range |
|---|---|---|---|---|
| (0, 4) | 3 | 1 | (2, 4) | (0, 0) |
| (2, 4) | 20 | 3 | (4, 4) | (2, 2) |
| (4, 4) | 7 | 4 | empty | empty |
| (2, 2) | 15 | 2 | empty | empty |
| (0, 0) | 9 | 0 | empty | empty |
Edge cases to test:
- A single node.
- Skewed trees.
- Forgetting to build the right subtree first gives the wrong tree.
Complexity: O(n) time, O(n) space for the map and recursion.
Medium 114. Flatten Binary Tree to Linked List Google Meta Amazon Microsoft
Problem: Flatten the tree in place into a "linked list" that uses the right pointers, in pre-order (node, left, right). Every left pointer must become None.
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [1,2,5,3,4,null,6][1,null,2,null,3,null,4,null,5,null,6]root = [][]Constraints: The number of nodes in the tree is in the range [0, 2000]. · -100 ≤ Node.val ≤ 100
Hint 1
Pre-order puts the whole left subtree between a node and its right subtree.
Hint 2
For each node with a left child: attach its right subtree to the rightmost node of the left subtree, then move the left subtree to the right.
From brute force to the pattern: Collecting the pre-order into a list and re-linking uses O(n) space. Splicing each left subtree in place uses O(1) extra space and still touches every edge a constant number of times: O(n).
Spot it: Pre-order puts the whole left subtree between a node and its right subtree. So for each node with a left child: hang the right subtree off the rightmost node of the left subtree, then move the left subtree over to the right. O(1) extra space, no recursion.
Approach:
- Walk
curdown the right pointers from the root. - If
curhas a left subtree: find that subtree's rightmost nodetail(the last node of the left subtree in pre-order). - Attach
cur.righttotail.right, then makecur.right = cur.leftandcur.left = None. - Move
cur = cur.rightand repeat untilcurisNone.
class Solution:
def flatten(self, root: Optional[TreeNode]) -> None:
cur = root
while cur is not None:
if cur.left is not None:
# the last node of the left subtree in pre-order is its rightmost node
tail = cur.left
while tail.right is not None:
tail = tail.right
# the right subtree comes after the whole left subtree
tail.right = cur.right
# move the left subtree into the right slot
cur.right = cur.left
cur.left = None
cur = cur.right
What the code is doing, line by line:
cur = root: we work down the chain that is being built.if cur.left is not None:only nodes with a left subtree need re-wiring.tail = cur.left, thenwhile tail.right is not None: tail = tail.right: the rightmost node of the left subtree is visited last in pre-order, so the right subtree must follow it.tail.right = cur.right: hang the old right subtree after that last node.cur.right = cur.left,cur.left = None: the left subtree now comes right aftercur, and no left pointers remain.cur = cur.right: continue. Nodes deeper down still get the same treatment whencurreaches them.
Dry run on the example:
root = [1, 2, 5, 3, 4, null, 6] (LeetCode Example 1). Result [1, null, 2, null, 3, null, 4, null, 5, null, 6].
| cur | left subtree? | tail (rightmost of left) | re-wiring | chain so far (right pointers) |
|---|---|---|---|---|
| 1 | yes (2) | 4 | 4.right = 5, 1.right = 2, 1.left = None | 1 → 2 → 4 → 5 → 6 (2 still has left child 3) |
| 2 | yes (3) | 3 | 3.right = 4, 2.right = 3, 2.left = None | 1 → 2 → 3 → 4 → 5 → 6 |
| 3 | no | — | — | unchanged |
| 4 | no | — | — | unchanged |
| 5 | no | — | — | unchanged |
| 6 | no | — | — | done |
Edge cases to test:
- An empty tree.
- Only left children.
- Already flat (only right children).
Complexity: O(n) time (each tail walk covers edges no other walk covers), O(1) extra space.
Medium 235. Lowest Common Ancestor of a Binary Search Tree Google Amazon Microsoft
Problem: In a BST, return the lowest node that has both p and q as descendants (a node counts as a descendant of itself).
Solve it on LeetCode ↗ · Video walkthrough ↗
root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 86root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 42Constraints: The number of nodes in the tree is in the range [2, 10⁵]. · -10⁹ ≤ Node.val ≤ 10⁹ · All Node.val are unique. · p != q · p and q will exist in the BST.
Hint 1
Use the BST ordering: you know which side each value lies on.
Hint 2
While both values are smaller, go left; while both are larger, go right; otherwise the current node is the split point.
From brute force to the pattern: The general LCA algorithm explores the whole tree (O(n)). In a BST the ordering tells you which way to go, so you follow one path: O(h).
Spot it: BST order tells you which side each value is on. While both values are smaller than the current node, the LCA is on the left; while both are larger, on the right. The first node where they split (or that equals one of them) is the LCA.
Approach:
- Start at the root.
- If both
p.valandq.valare smaller thannode.val, move left. - If both are larger, move right.
- Otherwise they are on different sides (or one of them is this node): return it.
class Solution:
def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
node = root
while node is not None:
if p.val < node.val and q.val < node.val:
node = node.left # both are in the left subtree
elif p.val > node.val and q.val > node.val:
node = node.right # both are in the right subtree
else:
# they split here (or one of them IS this node)
return node
return None
What the code is doing, line by line:
node = root: an iterative walk. No recursion is needed because we only ever go down one path.if p.val < node.val and q.val < node.val:in a BST everything smaller lives on the left, so the LCA must be there too.elif p.val > node.val and q.val > node.val:both on the right.else: return node: one value is ≤ and the other ≥ this node, so this is the deepest node above both.return None: unreachable whenpandqare in the tree, as the problem guarantees.
Dry run on the example:
root = [6, 2, 8, 0, 4, 7, 9, null, null, 3, 5], p = 2, q = 4 (LeetCode Example 2). Returns 2.
| node | p = 2 vs node | q = 4 vs node | move |
|---|---|---|---|
| 6 | smaller | smaller | go left |
| 2 | equal | larger | split → return 2 |
Edge cases to test:
pis an ancestor ofq.pandqare the root's two children.- A skewed BST.
Complexity: O(h) time (one root-to-node path), O(1) space.
Medium 427. Construct Quad Tree Google
Problem: grid is an n × n matrix of 0s and 1s (n a power of 2). Build its quad tree: a uniform square becomes a leaf with that value; otherwise the node has four children for the top-left, top-right, bottom-left and bottom-right quarters.
Solve it on LeetCode ↗ · Video walkthrough ↗
grid = [[0,1],[1,0]][[0,1],[1,0],[1,1],[1,1],[1,0]]grid = [[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,1,1,1,1],[1,1,1,1,1,1,1,1],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0],[1,1,1,1,0,0,0,0]][[0,1],[1,1],[0,1],[1,1],[1,0],null,null,null,null,[1,0],[1,0],[1,1],[1,1]]Constraints: n == grid.length == grid[i].length · n == 2x where 0 ≤ x ≤ 6
Hint 1
If every value in a square is the same, the square is a leaf.
Hint 2
Otherwise split it into four quarters and build each recursively.
From brute force to the pattern: Checking uniformity by scanning each square is O(n² log n) overall. A 2-D prefix sum checks a square in O(1), giving O(n²).
Spot it: A tree defined by recursive splitting → recursion that builds one node per square. Each node has four children instead of two.
Approach:
build(row, col, size)handles the square whose top-left corner is(row, col).- Check whether every cell in it equals the first cell.
- If so, return a leaf:
Node(val = that value, isLeaf = True). - Otherwise split into four squares of
size // 2and return an internal node with those four children.
class Solution:
def construct(self, grid: List[List[int]]) -> "Node":
def build(row, col, size):
# is every cell in this square the same as its first cell?
first = grid[row][col]
uniform = True
for r in range(row, row + size):
for c in range(col, col + size):
if grid[r][c] != first:
uniform = False
break
if not uniform:
break
if uniform:
return Node(first == 1, True)
half = size // 2
# not uniform: split into four quarters
return Node(True, False,
build(row, col, half), # top-left
build(row, col + half, half), # top-right
build(row + half, col, half), # bottom-left
build(row + half, col + half, half)) # bottom-right
return build(0, 0, len(grid))
What the code is doing, line by line:
def build(row, col, size):a square is described by its corner and side length, so no sub-grids are copied.- The two nested loops compare every cell with
firstandbreakout at the first mismatch. if uniform: return Node(first == 1, True): a leaf. LeetCode stores the value as a boolean, sofirst == 1turns 1/0 intoTrue/False.half = size // 2: the side of each quarter.Node(True, False, ...): an internal node (isLeaf = False; itsvalcan be anything) with four recursively built children, in the order top-left, top-right, bottom-left, bottom-right.
Dry run on the example:
grid = [[0, 1], [1, 0]] (LeetCode Example 1). Returns [[0, 1], [1, 0], [1, 1], [1, 1], [1, 0]] (each pair is [isLeaf, val]).
| build(row, col, size) | cells | uniform? | result |
|---|---|---|---|
| (0, 0, 2) | 0, 1, 1, 0 | no | internal node, split into four |
| (0, 0, 1) | 0 | yes | leaf val = False |
| (0, 1, 1) | 1 | yes | leaf val = True |
| (1, 0, 1) | 1 | yes | leaf val = True |
| (1, 1, 1) | 0 | yes | leaf val = False |
Edge cases to test:
- A 1 × 1 grid.
- A uniform grid → a single leaf.
- A checkerboard → maximum splitting.
Complexity: O(n² log n) time (each level of splitting re-scans all cells once), O(log n) recursion depth.
Medium 95. Unique Binary Search Trees II Google Meta Microsoft
Problem: Return every structurally unique BST that stores exactly the values 1..n.
n = 3[[1,null,2,null,3],[1,null,3,2],[2,1,3],[3,1,null,null,2],[3,2,null,1]]n = 1[[1]]Constraints: 1 ≤ n ≤ 8
Hint 1
If r is the root, which values are in its left subtree, and which in its right?
Hint 2
Recursively build all trees for lo..r − 1 and r + 1..hi, then combine every pair.
From brute force to the pattern: Inserting every permutation of 1..n gives n! trees with many duplicates. Building by root choice produces each tree once.
Spot it: In a BST, choosing the root r splits the values: lo..r − 1 go left, r + 1..hi go right. The trees for a range are built recursively from the trees of its two sub-ranges: a tree-building recursion (divide and conquer).
Why not another pattern? Backtracking over insert orders produces the same tree many times (different insert orders give equal trees).
Approach:
build(lo, hi)returns all BSTs using valueslo..hi; an empty range gives[None](one empty tree).- For each root
rinlo..hi: getlefts = build(lo, r − 1),rights = build(r + 1, hi). - For every pair
(left, right), make a new rootrwith those children. - Memoise on
(lo, hi): subtrees can be shared between results since they're never modified.
from functools import lru_cache
class Solution:
def generateTrees(self, n: int) -> List[Optional[TreeNode]]:
@lru_cache(maxsize=None)
def build(lo, hi):
if lo > hi:
return [None] # one way to build an empty tree
trees = []
for root in range(lo, hi + 1): # try every value as the root
for left in build(lo, root - 1): # all BSTs of the smaller values
for right in build(root + 1, hi): # all BSTs of the larger values
trees.append(TreeNode(root, left, right))
return trees
return build(1, n)
What the code is doing, line by line:
[None](not[]) for an empty range is what makes a leaf possible: one way to have no left child.- Every combination of a left subtree and a right subtree under root
ris a distinct BST. - Sharing subtrees between answers is safe because nothing is modified after it's built.
- The number of trees is the Catalan number
Cₙ(42 forn = 5).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
n = 1→ one single-node tree.- The empty range must return
[None], not[]. - Shared subtrees between results (fine because they're never mutated).
Complexity: O(Cₙ · n) time and space (Cₙ ≈ 4ⁿ / n^1.5): proportional to the output.
Medium 96. Unique Binary Search Trees Google Microsoft
Problem: Return how many structurally unique BSTs store exactly the values 1..n.
n = 35n = 11Constraints: 1 ≤ n ≤ 19
Hint 1
Pick the root. How many values are on each side?
Hint 2
G(n) = Σ G(r − 1) · G(n − r) over all roots r.
From brute force to the pattern: Building every tree costs as much as the answer itself (exponential). The DP is O(n²).
Spot it: Only the count is needed, and it depends only on the number of values, not which ones. With root r, the left side has r − 1 values and the right side n − r: multiply their counts and add over all roots.
Why not another pattern? Generating every tree (LeetCode 95) is exponential; counting needs only a small DP table.
Approach:
count[0] = count[1] = 1.- For each size
mfrom 2 ton:count[m] = Σ count[r − 1] · count[m − r]forr = 1..m. - Return
count[n].
class Solution:
def numTrees(self, n: int) -> int:
count = [0] * (n + 1)
count[0] = 1 # the empty tree
count[1] = 1
for m in range(2, n + 1): # number of values in the tree
for root in range(1, m + 1):
count[m] += count[root - 1] * count[m - root] # left shapes x right shapes
return count[n]
What the code is doing, line by line:
- The shape count for a set of values depends only on how many values there are.
- Root
rootleavesroot − 1values on the left andm − rooton the right; every pair of shapes is a new tree. - These are the Catalan numbers: 1, 1, 2, 5, 14, 42, …
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
n = 1→ 1.- The empty subtree counts as one shape.
n = 19fits in a 32-bit integer.
Complexity: O(n²) time, O(n) space.
Medium 99. Recover Binary Search Tree Google Amazon
Problem: Exactly two nodes of a BST had their values swapped. Fix the tree without changing its structure.
root = [1,3,null,null,2][3,1,null,null,2]root = [3,1,4,null,null,2][2,1,4,null,null,3]Constraints: The number of nodes in the tree is in the range [2, 1000]. · -2³¹ ≤ Node.val ≤ 2³¹ - 1
Hint 1
What does an in-order traversal of a valid BST look like?
Hint 2
Find where the in-order sequence decreases; the first and last such places identify the two nodes.
From brute force to the pattern: Collect values, sort them, write them back in order: O(n log n) and O(n) space. Tracking drops during one walk is O(n).
Spot it: A BST's in-order traversal is sorted. Swapping two values creates one or two places where prev > current. The first wrong node is the prev of the first drop; the second is the current of the last drop.
Why not another pattern? Copying values out, sorting and writing them back works but uses O(n) space and ignores that only two are wrong.
Approach:
- Walk the tree in order (iteratively, with a stack), remembering the previous node.
- At each drop
prev.val > node.val: if it's the first, setfirst = prev; always setsecond = node. - Swap
first.valandsecond.val.
class Solution:
def recoverTree(self, root: Optional[TreeNode]) -> None:
stack = []
node = root
prev = first = second = None
while stack or node:
while node:
stack.append(node) # go as far left as possible
node = node.left
node = stack.pop() # next node in sorted order
if prev and prev.val > node.val: # order is broken here
if first is None:
first = prev # the larger value that moved forward
second = node # the smaller value that moved back
prev = node
node = node.right
first.val, second.val = second.val, first.val # put them back
What the code is doing, line by line:
- The iterative in-order walk visits nodes in what should be increasing order.
- Adjacent swapped nodes produce one drop (
first = prev,second = nodeat that drop). - Non-adjacent swaps produce two drops:
firstcomes from the first,secondfrom the second. - Only the values are swapped; the structure stays as it was.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- The swapped nodes are adjacent in in-order (one drop).
- The root is one of them.
- Two-node tree.
Complexity: O(n) time, O(h) space for the stack (Morris traversal brings it to O(1)).
Medium 107. Binary Tree Level Order Traversal II Google
Problem: Return the node values level by level from the bottom level up (left to right within each level).
root = [3,9,20,null,null,15,7][[15,7],[9,20],[3]]root = [1][[1]]Constraints: The number of nodes in the tree is in the range [0, 2000]. · -1000 ≤ Node.val ≤ 1000
Hint 1
Can you do a normal level-order traversal first?
Hint 2
Then reverse the result.
From brute force to the pattern: Prepending each level to the answer shifts the whole list each time; one reverse at the end is O(levels).
Spot it: "Level by level" is BFS with a queue, processing one level per loop. Bottom-up just reverses the finished list.
Why not another pattern? Inserting each level at the front of a list is O(levels²); append and reverse once instead.
Approach:
- Queue starts with the root.
- While the queue isn't empty: pop exactly
len(queue)nodes (one level), collect their values, push their children. - Reverse the levels at the end.
from collections import deque
class Solution:
def levelOrderBottom(self, root: Optional[TreeNode]) -> List[List[int]]:
if root is None:
return []
levels = []
queue = deque([root])
while queue:
level = []
for _ in range(len(queue)): # exactly the nodes of this level
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
levels.append(level)
return levels[::-1] # bottom level first
What the code is doing, line by line:
len(queue)at the start of each round is the size of the current level, so children wait for the next round.- Levels are produced top-down; reversing once gives bottom-up.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Empty tree →
[]. - A tree that is a single path.
- Only the root.
Complexity: O(n) time, O(width) space for the queue.
Medium 116. Populating Next Right Pointers in Each Node Google Amazon
Problem: In a perfect binary tree, set each node's next to the node on its right in the same level (or None).
root = [1,2,3,4,5,6,7][1,#,2,3,#,4,5,6,7,#]root = [][]Constraints: The number of nodes in the tree is in the range [0, 2¹² - 1]. · -1000 ≤ Node.val ≤ 1000
Hint 1
A BFS works. Can you avoid the queue?
Hint 2
Once a level's next pointers are set, you can walk that level like a linked list to link the next one.
From brute force to the pattern: BFS with a queue is O(n) space; walking each linked level is O(1).
Spot it: A BFS queue does it in O(n) space. In a perfect tree there's a constant-space trick: once a level is linked, walk it via next and link its children: left.next = right, and right.next = node.next.left.
Why not another pattern? BFS with a queue is correct but uses O(n) extra space; the follow-up asks for constant space.
Approach:
leftmost= the first node of the current level, starting at the root.- Walk the level with
node = node.next: setnode.left.next = node.right, and ifnode.next,node.right.next = node.next.left. - Move down:
leftmost = leftmost.left, until the leaves.
class Solution:
def connect(self, root: "Optional[Node]") -> "Optional[Node]":
leftmost = root
while leftmost and leftmost.left: # there is a level below to link
node = leftmost
while node: # walk this level using the next pointers already set
node.left.next = node.right # siblings under the same parent
if node.next:
node.right.next = node.next.left # across to the neighbouring parent
node = node.next
leftmost = leftmost.left # go down one level
return root
What the code is doing, line by line:
- The root level is trivially linked (one node,
next = None). - Each linked level lets us reach every pair of neighbouring children without a queue.
- Children of the same parent link directly; children of neighbouring parents link through
node.next. - The tree is perfect, so
leftexists exactly whenrightdoes.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Empty tree.
- Only the root.
- The rightmost node of each level keeps
next = None.
Complexity: O(n) time, O(1) extra space.
Medium 222. Count Complete Tree Nodes Google Amazon
Problem: Count the nodes of a complete binary tree in less than O(n) time.
root = [1,2,3,4,5,6]6root = []0Constraints: The number of nodes in the tree is in the range [0, 5 * 10⁴]. · 0 ≤ Node.val ≤ 5 * 10⁴ · The tree is guaranteed to be complete.
Hint 1
A perfect binary tree of height h has 2^h − 1 nodes. How can you tell a subtree is perfect?
Hint 2
Compare the leftmost depth with the rightmost depth.
From brute force to the pattern: Counting every node is O(n); using perfect subtrees skips most of them.
Spot it: In a complete tree, at least one of the two subtrees is perfect, and a perfect tree's size is 2^h − 1 without visiting it. Compare the depth down the far-left edge with the depth down the far-right edge: equal means perfect.
Why not another pattern? Any traversal counts in O(n); the complete shape allows O(log² n).
Approach:
- Measure the left-edge height and right-edge height of the current node.
- If equal, return
2^h − 1. - Otherwise return
1 + count(left) + count(right); one of the two calls stops right away.
class Solution:
def countNodes(self, root: Optional[TreeNode]) -> int:
if root is None:
return 0
left_h = 0
node = root
while node:
left_h = left_h + 1 # depth along the leftmost path
node = node.left
right_h = 0
node = root
while node:
right_h = right_h + 1 # depth along the rightmost path
node = node.right
if left_h == right_h:
return (1 << left_h) - 1 # perfect tree: 2^h - 1 nodes
return 1 + self.countNodes(root.left) + self.countNodes(root.right)
What the code is doing, line by line:
- Equal edge heights mean every level is full, so the size follows from the height.
- When they differ, recursing into both children is fine: one child is always perfect and returns immediately.
- There are
O(log n)levels of recursion, each spendingO(log n)on the edge walks.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Empty tree → 0.
- A perfect tree (answered at the root).
- The last level has a single node.
Complexity: O(log² n) time, O(log n) recursion space.
Medium 366. Find Leaves of Binary Tree Google
Problem: Repeatedly collect and remove all leaves until the tree is empty; return the groups in removal order.
Solve it on LeetCode ↗ (Premium)
root = [1,2,3,4,5][[4,5,3],[2],[1]]root = [1][[1]]Constraints: The number of nodes in the tree is in the range [1, 100] · -100 ≤ Node.val ≤ 100
Hint 1
In which round is a node removed? Think about the longest path down to a leaf.
Hint 2
Compute each node's height post-order and group nodes by height.
From brute force to the pattern: Deleting leaves round by round is O(n · h); grouping by height is one O(n) walk.
Spot it: A node is removed in round h, where h is its height (leaves have height 0). Height needs the children first: a post-order DFS that returns 1 + max(left, right) and appends the node to rounds[h].
Why not another pattern? Actually deleting leaves round by round re-walks the tree each round: O(n · h).
Approach:
height(node):None→ −1; otherwiseh = 1 + max(height(left), height(right)).- Append
node.valtorounds[h](create the list whenhis new). - Return
rounds.
class Solution:
def findLeaves(self, root: Optional[TreeNode]) -> List[List[int]]:
rounds = []
def height(node):
if node is None:
return -1
h = 1 + max(height(node.left), height(node.right)) # children first (post-order)
if h == len(rounds):
rounds.append([]) # first node removed in round h
rounds[h].append(node.val)
return h
height(root)
return rounds
What the code is doing, line by line:
- A node becomes a leaf once all nodes below it are gone, which takes
heightrounds. - Post-order visits nodes left to right within each height, matching the expected order.
- Heights are reached in increasing order the first time, so
h == len(rounds)is enough to grow the list.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A single node →
[[val]]. - A path-shaped tree (one node per round).
- Leaves at different depths.
Complexity: O(n) time, O(h) recursion space.
Medium 450. Delete Node in a BST Google Amazon
Problem: Delete the node with value key from a BST (if present) and return the root; the result must still be a BST.
root = [5,3,6,2,4,null,7], key = 3[5,4,6,2,null,null,7]root = [5,3,6,2,4,null,7], key = 0[5,3,6,2,4,null,7]Constraints: The number of nodes in the tree is in the range [0, 10⁴]. · -10⁵ ≤ Node.val ≤ 10⁵ · Each node has a unique value. · root is a valid binary search tree. · -10⁵ ≤ key ≤ 10⁵
Hint 1
Find the node first. What are the cases, by number of children?
Hint 2
With two children, which value can replace it and keep the BST property?
From brute force to the pattern: Collecting all values except key and rebuilding is O(n); deleting along one path is O(h).
Spot it: Find the node by BST search. Zero or one child: replace it with that child. Two children: copy in its in-order successor (smallest value in the right subtree) and delete that successor, which has at most one child.
Why not another pattern? Rebuilding the BST from its sorted values is O(n); deleting in place is O(h).
Approach:
- If
key < root.val, delete from the left subtree; if greater, from the right. - Found it: no left child → return the right; no right child → return the left.
- Both: find the minimum of the right subtree, copy its value here, delete that value from the right subtree.
class Solution:
def deleteNode(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]:
if root is None:
return None # key not found
if key < root.val:
root.left = self.deleteNode(root.left, key) # it's in the left subtree
elif key > root.val:
root.right = self.deleteNode(root.right, key)
else:
if root.left is None:
return root.right # zero or one child: lift the child up
if root.right is None:
return root.left
succ = root.right
while succ.left:
succ = succ.left # smallest value in the right subtree
root.val = succ.val # the successor takes this node's place
root.right = self.deleteNode(root.right, succ.val)
return root
What the code is doing, line by line:
- Each recursive call returns the new root of its subtree, so parents re-attach whatever comes back.
- A node with at most one child is replaced by that child (or
None). - The in-order successor is larger than everything on the left and smaller than the rest of the right, so it can take the node's place.
- The successor has no left child, so deleting it is the simple case.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
keynot in the tree.- Deleting the root.
- Empty tree.
Complexity: O(h) time and recursion space (h = height).
Medium 662. Maximum Width of Binary Tree Google Amazon
Problem: The width of a level is the distance between its leftmost and rightmost nodes, counting the gaps. Return the maximum width.
root = [1,3,2,5,3,null,9]4root = [1,3,2,5,null,null,9,6,null,7]7Constraints: The number of nodes in the tree is in the range [1, 3000]. · -100 ≤ Node.val ≤ 100
Hint 1
Number nodes the way a heap array does.
Hint 2
Then the width of a level is the last number minus the first, plus one.
From brute force to the pattern: Materialising null placeholders in a complete tree can need 2^depth entries; heap indices need one number per node.
Spot it: Give each node its position in a complete tree: root 0, children 2i and 2i + 1. Then a level's width is just last − first + 1. Visit level by level with BFS.
Why not another pattern? Filling the gaps with placeholder nodes explodes exponentially on deep, sparse trees.
Approach:
- Queue of
(node, index), starting with(root, 0). - For each level: width = index of last − index of first + 1.
- Push children with indices
2iand2i + 1, rebased to the level's first index to keep numbers small.
from collections import deque
class Solution:
def widthOfBinaryTree(self, root: Optional[TreeNode]) -> int:
best = 0
queue = deque([(root, 0)]) # (node, position in a complete tree)
while queue:
first = queue[0][1]
last = queue[-1][1]
best = max(best, last - first + 1) # gaps between them count
for _ in range(len(queue)):
node, i = queue.popleft()
i = i - first # rebase so indices stay small
if node.left:
queue.append((node.left, 2 * i))
if node.right:
queue.append((node.right, 2 * i + 1))
return best
What the code is doing, line by line:
- Heap numbering places every possible position of a level on a number line, so the gap count is a subtraction.
- The queue holds exactly one level at the start of each round, ordered left to right.
- Subtracting
firstbefore doubling keeps indices bounded by the level width instead of growing as2^depth.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A single node → 1.
- A deep zig-zag tree (indices would overflow without rebasing).
- The widest level isn't the last one.
Complexity: O(n) time, O(width) space.
Medium 701. Insert into a Binary Search Tree Google Amazon
Problem: Insert val (not already present) into a BST and return the root. Any valid BST is accepted.
root = [4,2,7,1,3], val = 5[4,2,7,1,3,5]root = [40,20,60,10,30,50,70], val = 25[40,20,60,10,30,50,70,null,null,25]Constraints: The number of nodes in the tree will be in the range [0, 10⁴]. · -10⁸ ≤ Node.val ≤ 10⁸ · All the values Node.val are unique. · -10⁸ ≤ val ≤ 10⁸ · It's guaranteed that val does not exist in the original BST.
Hint 1
Search for val as if it were in the tree.
Hint 2
Where the search falls off the tree, attach the new node.
From brute force to the pattern: Collecting values and rebuilding is O(n); walking one path is O(h).
Spot it: BST search tells you exactly where val would be: follow left/right comparisons until a missing child, and put it there.
Why not another pattern? Rebuilding or rebalancing isn't required; inserting at a leaf keeps the BST property in O(h).
Approach:
- Empty tree: return a new node.
- Walk from the root: go left if
val < node.val, else right. - When that child is
None, attach the new node there.
class Solution:
def insertIntoBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
if root is None:
return TreeNode(val)
node = root
while True:
if val < node.val:
if node.left is None:
node.left = TreeNode(val) # found the empty spot
return root
node = node.left # smaller: go left
else:
if node.right is None:
node.right = TreeNode(val)
return root
node = node.right # larger: go right
What the code is doing, line by line:
- The search path is the only place
valcan go without breaking the ordering. - The new node is always a leaf, so no other node moves.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Empty tree.
valsmaller than every value (goes far left).- A skewed tree.
Complexity: O(h) time, O(1) extra space.
Medium 979. Distribute Coins in Binary Tree Google Amazon
Problem: Each node has some coins (total = number of nodes). One move shifts one coin across an edge. Return the minimum moves to give every node exactly one coin.
root = [3,0,0]2root = [0,3,0]3Constraints: The number of nodes in the tree is n. · 1 ≤ n ≤ 100 · 0 ≤ Node.val ≤ n · The sum of all Node.val is n.
Hint 1
Think about a single edge: how many coins must cross it?
Hint 2
A subtree with c coins and k nodes sends c − k coins across the edge above it.
From brute force to the pattern: Simulating coin moves is messy and slow; counting per-edge flow is one O(n) post-order walk.
Spot it: Look at an edge between a node and its parent: the subtree below has coins − nodes extra (or missing) coins, and exactly that many must cross this edge. That excess needs the children first: post-order DFS.
Why not another pattern? Simulating moves greedily is complicated and slow; counting flow per edge is exact.
Approach:
excess(node)=node.val − 1 + excess(left) + excess(right).- Add
|excess(left)| + |excess(right)|to the moves: that many coins cross each child edge. - Return the moves after visiting the root.
class Solution:
def distributeCoins(self, root: Optional[TreeNode]) -> int:
self.moves = 0
def excess(node): # coins this subtree must send up (negative = must receive)
if node is None:
return 0
left = excess(node.left)
right = excess(node.right)
self.moves += abs(left) + abs(right) # coins crossing the two child edges
return node.val - 1 + left + right
excess(root)
return self.moves
What the code is doing, line by line:
- Each subtree needs exactly one coin per node; its surplus or deficit must pass through the edge to its parent.
- Every crossing coin costs one move, and no coin needs to cross an edge twice, so summing
|excess|over edges is optimal. - The root's own excess is 0 because the total coins equal the number of nodes.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Every node already has one coin → 0.
- All coins at one leaf.
- A deficit in one branch and a surplus in another.
Complexity: O(n) time, O(h) recursion space.
Medium 1110. Delete Nodes And Return Forest Google
Problem: Delete every node whose value is in to_delete and return the roots of the remaining trees (any order).
root = [1,2,3,4,5,6,7], to_delete = [3,5][[1,2,null,4],[6],[7]]root = [1,2,4,null,3], to_delete = [3][[1,2,4]]Constraints: The number of nodes in the given tree is at most 1000. · Each node has a distinct value between 1 and 1000. · to_delete.length ≤ 1000 · to_delete contains distinct values between 1 and 1000.
Hint 1
When does a node become the root of a tree in the forest?
Hint 2
Pass down whether the parent was deleted; return None for deleted nodes so the parent drops the link.
From brute force to the pattern: Deleting nodes one by one and searching for orphans each time is O(n · k); one DFS is O(n).
Spot it: A node starts a new tree exactly when it is kept and its parent is deleted (or it's the root). Pass "is my parent gone?" down the DFS and return None for deleted nodes so parents cut them off.
Why not another pattern? Deleting nodes one at a time and searching for the new roots afterwards re-walks the tree for each deletion.
Approach:
- Put
to_deletein a set. walk(node, parent_deleted):deleted = node.val in remove. If kept andparent_deleted, it's a new root.- Recurse into children with
parent_deleted = deleted; assign the results back (they may beNone). - Return
Noneif this node is deleted, else the node.
class Solution:
def delNodes(self, root: Optional[TreeNode], to_delete: List[int]) -> List[TreeNode]:
remove = set(to_delete)
roots = []
def walk(node, parent_deleted):
if node is None:
return None
deleted = node.val in remove
if parent_deleted and not deleted:
roots.append(node) # its parent is gone: it heads a new tree
node.left = walk(node.left, deleted) # children cut off if they are deleted
node.right = walk(node.right, deleted)
return None if deleted else node
walk(root, True) # the root behaves as if its parent were deleted
return roots
What the code is doing, line by line:
- Calling
walk(root, True)makes the root a tree root if it's kept. - Assigning
node.left = walk(...)detaches a deleted child automatically, since the call returnsNone. - Children of a deleted node are checked with
parent_deleted = True, so kept ones become roots.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Deleting the root.
- Deleting nothing →
[root]. - Deleting a leaf (no new roots).
Complexity: O(n) time, O(h + |to_delete|) space.
Medium 1161. Maximum Level Sum of a Binary Tree Google
Problem: Levels are numbered from 1 at the root. Return the smallest level whose node values have the largest sum.
root = [1,7,0,7,-8,null,null]2root = [989,null,10250,98693,-89388,null,null,null,-32127]2Constraints: The number of nodes in the tree is in the range [1, 10⁴]. · -10⁵ ≤ Node.val ≤ 10⁵
Hint 1
Process the tree one level at a time.
Hint 2
Track the best sum; only replace it on a strictly bigger sum.
From brute force to the pattern: Computing each level's sum with a separate depth-limited DFS is O(n · h); one BFS is O(n).
Spot it: Per-level sums → BFS one level at a time. Update the answer only on a strictly larger sum so the smallest level wins ties.
Why not another pattern? A DFS with a depth-indexed sum array also works; BFS just makes the per-level loop explicit.
Approach:
- Queue with the root;
level = 1. - Sum the values of exactly
len(queue)nodes, pushing their children. - If the sum beats the best, record the level.
from collections import deque
class Solution:
def maxLevelSum(self, root: Optional[TreeNode]) -> int:
best_sum = float("-inf")
best_level = 1
level = 0
queue = deque([root])
while queue:
level = level + 1
total = 0
for _ in range(len(queue)): # one full level
node = queue.popleft()
total += node.val
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
if total > best_sum: # strictly larger: earlier levels win ties
best_sum = total
best_level = level
return best_level
What the code is doing, line by line:
- Each outer iteration handles exactly one level.
- Values can be negative, so the best sum starts at −∞.
- Using
>(not>=) keeps the smallest level on ties.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Negative values everywhere.
- Ties between levels (smallest wins).
- A single node.
Complexity: O(n) time, O(width) space.
Medium 2096. Step-By-Step Directions From a Binary Tree Node to Another Google
Problem: Return the shortest path from node startValue to node destValue as letters L, R (to a child) and U (to the parent).
root = [5,1,2,3,null,6,4], startValue = 3, destValue = 6"UURL"root = [2,1], startValue = 2, destValue = 1"L"Constraints: The number of nodes in the tree is n. · 2 ≤ n ≤ 10⁵ · 1 ≤ Node.val ≤ n · All the values in the tree are unique. · 1 ≤ startValue, destValue ≤ n · startValue != destValue
Hint 1
The shortest path goes through the lowest common ancestor.
Hint 2
Write the root-to-node path of both nodes; what does their common prefix mean?
From brute force to the pattern: BFS over an undirected copy of the tree plus parent tracking works but builds a whole graph; two root paths are enough.
Spot it: The path goes up from the start to their lowest common ancestor, then down to the destination. Record both paths from the root as L/R strings; their common prefix is the way to the LCA.
Why not another pattern? Converting the tree to an undirected graph and running BFS works but needs O(n) extra structure and a path rebuild.
Approach:
- Find the root-to-start path and root-to-dest path as strings of
L/R(iterative DFS). - Remove their common prefix.
- Answer:
'U' × len(rest of start path)+ rest of dest path.
class Solution:
def getDirections(self, root: Optional[TreeNode], startValue: int, destValue: int) -> str:
def path_to(target):
stack = [(root, "")]
while stack:
node, path = stack.pop()
if node.val == target:
return path # moves from the root down to target
if node.left:
stack.append((node.left, path + "L"))
if node.right:
stack.append((node.right, path + "R"))
to_start = path_to(startValue)
to_dest = path_to(destValue)
i = 0
while i < len(to_start) and i < len(to_dest) and to_start[i] == to_dest[i]:
i = i + 1 # shared part: the way down to the common ancestor
return "U" * (len(to_start) - i) + to_dest[i:]
What the code is doing, line by line:
- Both root paths share the moves down to the lowest common ancestor; the first difference is where they split.
- From the start, every remaining step of its path must be undone with a
U. - Then follow the rest of the destination path down.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- The start is an ancestor of the destination (no
U). - The destination is an ancestor of the start (only
U). - Both in different subtrees of the root.
Complexity: O(n) time; the string building is O(n · h) in the worst case (a list-based path brings it to O(n)).
Medium 2196. Create Binary Tree From Descriptions Google Amazon
Problem: Each row [parent, child, isLeft] describes one edge of a binary tree with unique values. Build the tree and return its root.
descriptions = [[20,15,1],[20,17,0],[50,20,1],[50,80,0],[80,19,1]][50,20,80,15,17,19]descriptions = [[1,2,1],[2,3,0],[3,4,1]][1,2,null,null,3,4]Constraints: 1 ≤ descriptions.length ≤ 10⁴ · descriptions[i].length == 3 · 1 ≤ parenti, childi ≤ 10⁵ · 0 ≤ isLefti ≤ 1 · The binary tree described by descriptions is valid.
Hint 1
Store nodes in a dictionary keyed by value.
Hint 2
Which value never appears as a child?
From brute force to the pattern: Building the tree by searching for each parent node is O(n²); a dictionary makes each edge O(1).
Spot it: A hash map value → node lets you link edges in any order. The root is the one parent that never appears as a child (a set).
Why not another pattern? Searching the partly built tree for each parent is O(n) per edge.
Approach:
- For each row, get or create the parent and child nodes in
nodes. - Attach the child to the left or right; add the child value to
children. - Return the node whose value isn't in
children.
class Solution:
def createBinaryTree(self, descriptions: List[List[int]]) -> Optional[TreeNode]:
nodes = {} # value -> node, created the first time it's seen
children = set()
for parent, child, is_left in descriptions:
if parent not in nodes:
nodes[parent] = TreeNode(parent)
if child not in nodes:
nodes[child] = TreeNode(child)
if is_left:
nodes[parent].left = nodes[child]
else:
nodes[parent].right = nodes[child]
children.add(child)
for parent, _, _ in descriptions:
if parent not in children:
return nodes[parent] # never a child: the root
What the code is doing, line by line:
- Nodes can be referenced as children before or after they appear as parents; the map makes that irrelevant.
- Every node except the root has exactly one parent, so the root is the only value missing from
children.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Edges given in any order.
- A path-shaped tree.
- One edge.
Complexity: O(n) time and space.
Hard 834. Sum of Distances in Tree Google
Problem: For every node of a tree, return the sum of its distances to all other nodes.
n = 6, edges = [[0,1],[0,2],[2,3],[2,4],[2,5]][8,12,6,10,10,10]n = 1, edges = [][0]Constraints: 1 ≤ n ≤ 3 * 10⁴ · edges.length == n - 1 · edges[i].length == 2 · 0 ≤ ai, bi < n · ai != bi · The given input represents a valid tree.
Hint 1
Can you compute the answer for one root in O(n)?
Hint 2
How does the answer change when the root moves from a node to its child? Think about subtree sizes.
From brute force to the pattern: BFS from every node: O(n²). Two passes with the rerooting formula: O(n).
Spot it: Running BFS from every node is O(n²). Rerooting: compute the answer for root 0 with one DFS (using subtree sizes), then derive each child's answer from its parent's: moving from p to child c brings size[c] nodes one step closer and the other n − size[c] one step further.
Why not another pattern? BFS from each node is O(n²) = 9·10⁸ for n = 3·10⁴.
Approach:
- Pass 1 (post-order from root 0):
size[u]anddown[u]= sum of distances fromuto nodes in its subtree.down[u] = Σ (down[c] + size[c]). answer[0] = down[0].- Pass 2 (pre-order):
answer[c] = answer[p] − size[c] + (n − size[c]).
class Solution:
def sumOfDistancesInTree(self, n: int, edges: List[List[int]]) -> List[int]:
adj = [[] for _ in range(n)]
for a, b in edges:
adj[a].append(b)
adj[b].append(a)
parent = [-1] * n
order = [0] # DFS order from root 0 (parents before children)
for u in order:
for v in adj[u]:
if v != parent[u]:
parent[v] = u
order.append(v)
size = [1] * n
down = [0] * n # sum of distances from u into its own subtree
for u in reversed(order): # children before parents
p = parent[u]
if p >= 0:
size[p] += size[u]
down[p] += down[u] + size[u] # every node under u is one step further from p
answer = [0] * n
answer[0] = down[0]
for u in order[1:]: # parents before children
answer[u] = answer[parent[u]] - size[u] + (n - size[u]) # reroot from parent to u
return answer
What the code is doing, line by line:
orderis built iteratively (no recursion limit problems) and lists every parent before its children.- Walking
orderbackwards accumulates subtree sizes and the subtree distance sums. - At the root, the subtree is the whole tree, so
down[0]is its full answer. - Moving the root from
ptou: thesize[u]nodes inu's subtree get 1 closer, the othern − size[u]get 1 further.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
n = 1→[0].- A path (long chain).
- A star.
Complexity: O(n) time and space.
Hard 987. Vertical Order Traversal of a Binary Tree Google Meta Amazon Microsoft
Problem: Root at (row 0, col 0); a left child is at (row + 1, col − 1), a right child at (row + 1, col + 1). Return the columns left to right, each top to bottom, ties in the same cell sorted by value.
root = [3,9,20,null,null,15,7][[9],[3,15],[20],[7]]root = [1,2,3,4,5,6,7][[4],[2],[1,5,6],[3],[7]]Constraints: The number of nodes in the tree is in the range [1, 1000]. · 0 ≤ Node.val ≤ 1000
Hint 1
What exactly decides the position of a node in the output?
Hint 2
Collect (column, row, value) for every node and sort.
From brute force to the pattern: Repeated scans per column are O(n · columns); one sort of the triples is O(n log n).
Spot it: The order rule is a sort key: (col, row, val). Collect that triple for every node with any traversal, sort once, then split into columns.
Why not another pattern? A BFS keeps rows in order but still needs sorting for equal (row, col) cells, so the sort is unavoidable.
Approach:
- DFS from the root carrying
(row, col); record(col, row, val)for each node. - Sort the triples.
- Walk them; start a new group whenever the column changes.
class Solution:
def verticalTraversal(self, root: Optional[TreeNode]) -> List[List[int]]:
cells = []
stack = [(root, 0, 0)]
while stack:
node, row, col = stack.pop()
if node:
cells.append((col, row, node.val)) # the sort key: column, then row, then value
stack.append((node.left, row + 1, col - 1))
stack.append((node.right, row + 1, col + 1))
cells.sort()
result = []
last_col = None
for col, row, val in cells:
if col != last_col:
result.append([]) # a new column starts
last_col = col
result[-1].append(val)
return result
What the code is doing, line by line:
- Any traversal order works because the sort fixes the final order.
- Sorting tuples compares column first, then row, then value — exactly the problem's rules.
- Consecutive triples with the same column form one output list.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Two nodes in the same row and column (sort by value).
- A single node.
- A tree leaning entirely left.
Complexity: O(n log n) time, O(n) space.
Hard 1483. Kth Ancestor of a Tree Node Google
Problem: Given a parent array (root 0), answer many queries: the k-th ancestor of node, or −1.
["TreeAncestor", "getKthAncestor", "getKthAncestor", "getKthAncestor"]
[[7, [-1, 0, 0, 1, 1, 2, 2]], [3, 1], [5, 2], [6, 3]][null, 1, 0, -1]Constraints: 1 ≤ k ≤ n ≤ 5 * 10⁴ · parent.length == n · parent[0] == -1 · 0 ≤ parent[i] < n for all 0 < i < n · 0 ≤ node < n · There will be at most 5 * 10⁴ queries.
Hint 1
Walking up one step at a time is too slow for many queries.
Hint 2
Precompute the 1st, 2nd, 4th, 8th, … ancestor of every node; then combine jumps using the binary form of k.
From brute force to the pattern: Following parents one step at a time is O(k) per query. Binary lifting answers in O(log k).
Spot it: Walking up k parents per query is O(k). Binary lifting precomputes jumps of size 1, 2, 4, …: up[j][v] = up[j − 1][up[j − 1][v]]. Any k is a sum of powers of two, so a query takes O(log k) jumps.
Why not another pattern? Storing every ancestor of every node is O(n²) memory; walking up is O(n) per query.
Approach:
up[0] = parent.- For
j ≥ 1:up[j][v] = up[j − 1][up[j − 1][v]](or −1 if the half-way node doesn't exist). - Query: for each set bit
jofk,node = up[j][node]; stop with −1 if it falls off the root.
class TreeAncestor:
def __init__(self, n: int, parent: List[int]):
self.up = [parent[:]] # up[j][v] = ancestor 2^j steps above v
for j in range(1, n.bit_length()):
prev = self.up[-1]
self.up.append([prev[prev[v]] if prev[v] != -1 else -1 for v in range(n)]) # two half jumps
def getKthAncestor(self, node: int, k: int) -> int:
j = 0
while k and node != -1:
if k & 1:
if j >= len(self.up):
return -1 # jump longer than any path in the tree
node = self.up[j][node] # take the 2^j jump
k >>= 1
j = j + 1
return node
What the code is doing, line by line:
- A jump of
2^jis two jumps of2^(j−1), which builds each table row from the previous one. n.bit_length()rows are enough: no path is longer thann − 1.- The query decomposes
kinto powers of two (its binary digits) and takes those jumps in any order. - Falling off the root gives −1, which then stays −1.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
klarger than the depth → −1.- Querying the root.
- A path-shaped tree (deepest possible).
Complexity: O(n log n) preprocessing and space, O(log k) per query.
Hard 3559. Number of Ways to Assign Edge Weights II Google
Problem: Each edge on the path between u and v gets weight 1 or 2. For each query, count weightings whose path cost is odd (mod 10⁹ + 7).
edges = [[1,2]], queries = [[1,1],[1,2]][0,1]edges = [[1,2],[1,3],[3,4],[3,5]], queries = [[1,4],[3,4],[2,5]][2,1,4]Constraints: 2 ≤ n ≤ 10⁵ · edges.length == n - 1 · edges[i] == [ui, vi] · 1 ≤ queries.length ≤ 10⁵ · queries[i] == [ui, vi] · 1 ≤ ui, vi ≤ n · edges represents a valid tree.
Hint 1
Only the parity of the cost matters. How many weightings of L edges give an odd sum?
Hint 2
You need the number of edges between u and v: use depths and the lowest common ancestor.
Hint 3
Answer many LCA queries quickly with binary lifting.
From brute force to the pattern: Walking each query's path is O(n) per query. Binary-lifting LCA is O(log n) per query.
Spot it: Only the parity matters, and only weight-1 edges change it. With L edges, exactly half of the 2^L choices have an odd number of 1s: 2^(L−1) (0 when L = 0). L = depth[u] + depth[v] − 2·depth[lca], and the LCA is a binary lifting query.
Why not another pattern? Walking the path for each query is O(n), O(n · q) overall.
Approach:
- BFS from node 1 to get
depthandparent. - Build the binary-lifting table
up[j][v]. - For each query: lift the deeper node to the same depth, then lift both while their ancestors differ; the LCA is the parent.
Lfrom depths; answerpow(2, L − 1, MOD)or 0.
from collections import deque
class Solution:
def assignEdgeWeights(self, edges: List[List[int]], queries: List[List[int]]) -> List[int]:
MOD = 10**9 + 7
n = len(edges) + 1
adj = [[] for _ in range(n + 1)]
for a, b in edges:
adj[a].append(b)
adj[b].append(a)
depth = [0] * (n + 1)
parent = [0] * (n + 1) # 0 means "above the root"
seen = [False] * (n + 1)
seen[1] = True
queue = deque([1])
while queue:
u = queue.popleft()
for v in adj[u]:
if not seen[v]:
seen[v] = True
parent[v] = u
depth[v] = depth[u] + 1
queue.append(v)
up = [parent] # up[j][v] = ancestor 2^j steps above v
for j in range(1, max(1, n.bit_length())):
prev = up[-1]
up.append([prev[prev[v]] for v in range(n + 1)])
def lca(u, v):
if depth[u] < depth[v]:
u, v = v, u
diff = depth[u] - depth[v]
for j in range(len(up)):
if diff >> j & 1:
u = up[j][u] # lift u to v's depth
if u == v:
return u
for j in range(len(up) - 1, -1, -1):
if up[j][u] != up[j][v]:
u = up[j][u] # jump both while they stay below the LCA
v = up[j][v]
return parent[u]
answer = []
for u, v in queries:
length = depth[u] + depth[v] - 2 * depth[lca(u, v)] # edges on the path
answer.append(pow(2, length - 1, MOD) if length else 0) # half of 2^length are odd
return answer
What the code is doing, line by line:
- Weight 2 never changes the parity; the cost is odd exactly when an odd number of edges get weight 1:
2^(L−1)ways. - BFS gives depths and parents; index 0 is a sentinel above the root, so jumps past the root stay at 0.
- LCA: equalise depths using the binary digits of the difference, then jump both from the largest power down while their ancestors differ.
- Path length =
depth[u] + depth[v] − 2·depth[lca].
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
u == v→ 0 (no edges).- Adjacent nodes → 1.
- A long chain (deep tree).
Complexity: O((n + q) log n) time, O(n log n) space.
Medium 113. Path Sum II Meta Amazon Microsoft
Problem: Return every root-to-leaf path whose values sum to targetSum.
root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22[[5,4,11,2],[5,8,4,5]]root = [1,2,3], targetSum = 5[]Constraints: The number of nodes in the tree is in the range [0, 5000]. · -1000 ≤ Node.val ≤ 1000 · -1000 ≤ targetSum ≤ 1000
Hint 1
Walk down from the root, keeping the current path.
Hint 2
Record the path at a leaf when the sum matches; undo on the way back up.
From brute force to the pattern: Listing every root-to-leaf path and summing each is the same DFS without pruning work; backtracking with one list avoids copies.
Spot it: All root-to-leaf paths → DFS with backtracking: keep the current path, add the node going down, remove it coming back.
Why not another pattern? Building a new list at every node copies paths over and over; one shared path with undo is cheaper.
Approach:
dfs(node, remaining): appendnode.val.- At a leaf with
remaining == node.val, record a copy of the path. - Recurse into children with
remaining − node.val; pop the node.
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
paths = []
path = []
def dfs(node, remaining):
if node is None:
return
path.append(node.val) # choose
if node.left is None and node.right is None and remaining == node.val:
paths.append(path[:]) # a complete root-to-leaf path with the right sum
dfs(node.left, remaining - node.val)
dfs(node.right, remaining - node.val)
path.pop() # un-choose
dfs(root, targetSum)
return paths
What the code is doing, line by line:
- Only leaves end a path, so internal nodes with the right partial sum are not recorded.
- Values can be negative, so you can't stop early when the sum overshoots.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Empty tree → [].
- Negative values.
- Several paths with the same sum.
Complexity: O(n · h) time in the worst case (copying paths), O(h) extra space.
Medium 314. Binary Tree Vertical Order Traversal Meta Apple
Problem: Return the node values column by column, left to right; within a column top to bottom, and left to right for nodes in the same row.
Solve it on LeetCode ↗ (Premium)
root = [3,9,20,null,null,15,7][[9],[3,15],[20],[7]]root = [3,9,8,4,0,1,7][[4],[9],[3,0,1],[8],[7]]root = [3,9,8,4,0,1,7,null,null,null,2,5][[4],[9,5],[3,0,1],[8,2],[7]]Constraints: The number of nodes in the tree is in the range [0, 100] · -100 ≤ Node.val ≤ 100
Hint 1
Give every node a column number.
Hint 2
Which traversal already visits nodes top to bottom and left to right?
From brute force to the pattern: DFS plus sorting by (column, row) is O(n log n); BFS gets the order for free in O(n).
Spot it: The required order inside a column is exactly BFS order (row by row, left to right). So run BFS with each node's column (left child col − 1, right col + 1) and append to a hash map of columns.
Why not another pattern? DFS visits a deeper left node before a shallower right one in the same column, which breaks the top-to-bottom rule (unless you sort by row).
Approach:
- Queue of
(node, col)starting at(root, 0). - Pop, append
node.valtocolumns[col], push children withcol ∓ 1. - Output columns from the smallest column index to the largest.
from collections import defaultdict, deque
class Solution:
def verticalOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
if root is None:
return []
columns = defaultdict(list)
queue = deque([(root, 0)])
while queue:
node, col = queue.popleft()
columns[col].append(node.val) # BFS order = top to bottom, left to right
if node.left:
queue.append((node.left, col - 1))
if node.right:
queue.append((node.right, col + 1))
return [columns[c] for c in range(min(columns), max(columns) + 1)]
What the code is doing, line by line:
- BFS processes rows in order and, inside a row, left to right, which is the tie-break the problem asks for.
- Columns are contiguous, so ranging from the minimum to the maximum column covers them all.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Empty tree → [].
- Two nodes in the same row and column (left one first).
- A skewed tree.
Complexity: O(n) time and space.
Medium 545. Boundary of Binary Tree Meta Microsoft
Problem: Return the boundary anticlockwise from the root: root, left boundary, leaves, then the right boundary bottom-up (no duplicates).
Solve it on LeetCode ↗ (Premium)
root = [1,null,2,3,4][1,3,4,2]root = [1,2,3,4,5,6,null,null,null,7,8,9,10][1,2,4,7,8,9,10,6,3]Constraints: The number of nodes in the tree is in the range [1, 10⁴] · -1000 ≤ Node.val ≤ 1000
Hint 1
Break the boundary into three parts.
Hint 2
Exclude leaves from the left and right edges so they aren't counted twice.
From brute force to the pattern: Several passes are already linear; the work is in getting the parts and their order right.
Spot it: Split the boundary into three traversals: walk down the left edge (prefer left child), collect leaves with a DFS, walk down the right edge (prefer right child) and reverse it. Leaves are excluded from the edges so they appear once.
Why not another pattern? One clever DFS with flags works but is easy to get wrong; three simple passes are clearer.
Approach:
- If the root is a leaf, return
[root.val]. - Left boundary: from
root.left, add non-leaves, moving left if possible else right. - Leaves: DFS left to right (skip the root).
- Right boundary: from
root.right, same with right first; reverse it.
class Solution:
def boundaryOfBinaryTree(self, root: Optional[TreeNode]) -> List[int]:
def is_leaf(node):
return node.left is None and node.right is None
if is_leaf(root):
return [root.val]
result = [root.val]
node = root.left
while node: # left edge, top to bottom
if not is_leaf(node):
result.append(node.val)
node = node.left if node.left else node.right
stack = [root]
while stack: # leaves, left to right
node = stack.pop()
if node is not root and is_leaf(node):
result.append(node.val)
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left) # popped first: left side first
right = []
node = root.right
while node: # right edge, collected top to bottom
if not is_leaf(node):
right.append(node.val)
node = node.right if node.right else node.left
return result + right[::-1] # right edge goes bottom to top
What the code is doing, line by line:
- Leaves are skipped on both edges so each leaf is added exactly once, in the leaf pass.
- An empty left subtree means there's no left boundary at all (the root isn't repeated).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A single node.
- No left subtree.
- A leaf that is also on an edge.
Complexity: O(n) time, O(h) space.
Medium 549. Binary Tree Longest Consecutive Sequence II Meta
Problem: Return the longest path of consecutive values (increasing or decreasing by 1 along the path); the path may go child-parent-child.
Solve it on LeetCode ↗ (Premium)
root = [1,2,3]2root = [2,1,3]3Constraints: The number of nodes in the tree is in the range [1, 3 * 10⁴] · -3 * 10⁴ ≤ Node.val ≤ 3 * 10⁴
Hint 1
A consecutive path bends at most once, at its highest node.
Hint 2
Return both the increasing and decreasing run lengths from each subtree.
From brute force to the pattern: Searching from every node is O(n²); one post-order pass is O(n).
Spot it: A path through a node is an increasing run on one side and a decreasing run on the other. Each node needs its children's runs → post-order DFS returning (inc, dec): longest runs starting at the node and going down.
Why not another pattern? Starting a search from every node is O(n²).
Approach:
walk(node)returns(inc, dec), both at least 1.- For each child: if
child.val == node.val + 1,inc = max(inc, child_inc + 1); if== node.val − 1,dec = max(dec, child_dec + 1). - Update
best = max(best, inc + dec − 1)(the node is counted twice).
class Solution:
def longestConsecutive(self, root: Optional[TreeNode]) -> int:
self.best = 0
def walk(node): # (longest +1 chain going down, longest -1 chain going down)
if node is None:
return 0, 0
inc = dec = 1
for child in (node.left, node.right):
if child:
c_inc, c_dec = walk(child)
if child.val == node.val + 1:
inc = max(inc, c_inc + 1)
elif child.val == node.val - 1:
dec = max(dec, c_dec + 1)
self.best = max(self.best, inc + dec - 1) # up one side, down the other
return inc, dec
walk(root)
return self.best
What the code is doing, line by line:
- An increasing run below and a decreasing run below form one consecutive path through the node, e.g. 1-2-3 with 2 in the middle.
− 1removes the double-counted node.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Single node → 1.
- Path through the root using both children.
- Equal values (not consecutive).
Complexity: O(n) time, O(h) space.
Medium 1382. Balance a Binary Search Tree Meta
Problem: Return a height-balanced BST with the same values (any valid answer).
root = [1,null,2,null,3,null,4,null,null][2,1,3,null,null,null,4]root = [2,1,3][2,1,3]Constraints: The number of nodes in the tree is in the range [1, 10⁴]. · 1 ≤ Node.val ≤ 10⁵
Hint 1
What does an in-order traversal of a BST give you?
Hint 2
How do you build a balanced BST from a sorted array?
From brute force to the pattern: Inserting values into a new BST in sorted order makes a chain; always choosing the middle balances it.
Spot it: A BST's in-order traversal is sorted. Rebuilding from a sorted array by taking the middle as the root (Convert Sorted Array to BST) gives a balanced tree.
Why not another pattern? Rotations (AVL / Day–Stout–Warren) also work but are far harder to write in an interview.
Approach:
- In-order traversal into a list.
build(lo, hi): middle element is the root; recurse on both halves.
class Solution:
def balanceBST(self, root: TreeNode) -> TreeNode:
values = []
def inorder(node):
if node:
inorder(node.left)
values.append(node.val) # sorted order
inorder(node.right)
def build(lo, hi):
if lo > hi:
return None
mid = (lo + hi) // 2 # middle value keeps both sides the same size
return TreeNode(values[mid], build(lo, mid - 1), build(mid + 1, hi))
inorder(root)
return build(0, len(values) - 1)
What the code is doing, line by line:
- Taking the middle splits the remaining values evenly, so the two subtree sizes differ by at most one at every node.
- The in-order list is sorted, so the rebuilt tree is a BST.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Already balanced.
- A chain-shaped tree.
- One node.
Complexity: O(n) time and space.
Medium 1650. Lowest Common Ancestor of a Binary Tree III Meta Amazon
Problem: Each node has a parent pointer (no root given). Return the lowest common ancestor of nodes p and q.
Solve it on LeetCode ↗ (Premium)
root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 13root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 45root = [1,2], p = 1, q = 21Constraints: The number of nodes in the tree is in the range [2, 10⁵] · -10⁹ ≤ Node.val ≤ 10⁹ · All Node.val are unique · p != q · p and q exist in the tree
Hint 1
With parent pointers, what does the path from a node to the root look like?
Hint 2
It's the intersection of two linked lists.
From brute force to the pattern: A set of p's ancestors costs O(h) space; the switching two-pointer walk uses O(1).
Spot it: Following parent from p and from q gives two linked lists that merge at the LCA and end at the root. It's Intersection of Two Linked Lists: two pointers that switch to the other start when they run out meet exactly at the merge point.
Why not another pattern? Storing all of p's ancestors in a set works but uses O(h) space.
Approach:
a = p,b = q.- While
a is not b:a = a.parent(orqwhen it runs off the top);b = b.parent(orp). - Return
a.
class Solution:
def lowestCommonAncestor(self, p: "Node", q: "Node") -> "Node":
a, b = p, q
while a is not b:
a = a.parent if a else q # ran past the root: restart from the other node
b = b.parent if b else p
return a # both walked the same total distance to the meeting point
What the code is doing, line by line:
- If
pisxsteps below the LCA andqisysteps below it, withzsteps from the LCA to the root: pointerawalksx + z + 1 + ybefore reaching the LCA the second time, andbwalksy + z + 1 + x. Equal lengths, so they meet there. - If one is an ancestor of the other, they meet at that ancestor.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- p is an ancestor of q.
- p and q are siblings.
- Two-node tree.
Complexity: O(h) time, O(1) space.
Medium 2265. Count Nodes Equal to Average of Subtree Meta
Problem: Count nodes whose value equals the floor of the average of their subtree's values.
root = [4,8,5,0,1,null,6]5root = [1]1Constraints: The number of nodes in the tree is in the range [1, 1000]. · 0 ≤ Node.val ≤ 1000
Hint 1
What two numbers do you need to know a subtree's average?
Hint 2
Return them from each child.
From brute force to the pattern: Summing every subtree separately is O(n²); one post-order pass is O(n).
Spot it: A subtree's average needs its sum and size, which come from the children → post-order DFS returning (sum, count).
Why not another pattern? Recomputing each subtree separately is O(n²).
Approach:
walk(node)returns(sum, count).- Combine children with the node; if
sum // count == node.val, count it.
class Solution:
def averageOfSubtree(self, root: TreeNode) -> int:
self.matches = 0
def walk(node): # (sum, number of nodes) of this subtree
if node is None:
return 0, 0
ls, lc = walk(node.left)
rs, rc = walk(node.right)
total, count = ls + rs + node.val, lc + rc + 1
if total // count == node.val:
self.matches += 1
return total, count
walk(root)
return self.matches
What the code is doing, line by line:
- Children report their totals before the parent needs them (post-order).
- Values are non-negative, so
//is the required rounding down.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Leaves always match.
- Rounding down (average 4.5 matches 4).
- Single node.
Complexity: O(n) time, O(h) space.
Medium 337. House Robber III Amazon Microsoft
Problem: Houses form a binary tree; robbing two directly linked houses alerts the police. Return the maximum loot.
root = [3,2,3,null,3,null,1]7root = [3,4,5,1,3,null,1]9Constraints: The number of nodes in the tree is in the range [1, 10⁴]. · 0 ≤ Node.val ≤ 10⁴
Hint 1
For each node, what do you need to know from its children?
Hint 2
Return two values: best with the node robbed and without it.
From brute force to the pattern: Trying both choices per node without memo is exponential; the pair-returning DFS is O(n).
Spot it: House Robber on a tree → DP on trees with a post-order DFS. Each node reports two numbers: the best for its subtree if it is robbed, and if it isn't.
Why not another pattern? Plain recursion that tries both choices at every node recomputes subtrees exponentially.
Approach:
walk(node)returns(rob, skip).rob = node.val + left.skip + right.skip.skip = max(left) + max(right).- Answer:
max(walk(root)).
class Solution:
def rob(self, root: Optional[TreeNode]) -> int:
def walk(node): # (best if node is robbed, best if it is not)
if node is None:
return 0, 0
l_rob, l_skip = walk(node.left)
r_rob, r_skip = walk(node.right)
rob = node.val + l_skip + r_skip # children must be skipped
skip = max(l_rob, l_skip) + max(r_rob, r_skip) # children are free to choose
return rob, skip
return max(walk(root))
What the code is doing, line by line:
- Robbing a node forbids its children but not its grandchildren.
- Returning both options means each node is visited once.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Single node.
- Grandchildren beat children.
- Skewed tree.
Complexity: O(n) time, O(h) space.
Medium 988. Smallest String Starting From Leaf Amazon
Problem: Node values 0–25 are letters. Return the lexicographically smallest string from any leaf up to the root.
root = [0,1,2,3,4,3,4]"dba"root = [25,1,3,1,3,0,2]"adz"Constraints: The number of nodes in the tree is in the range [1, 8500]. · 0 ≤ Node.val ≤ 25
Hint 1
Why doesn't choosing the smaller child greedily work?
Hint 2
Build each complete leaf-to-root string and compare.
From brute force to the pattern: This is the exhaustive DFS; the greedy shortcut is incorrect.
Spot it: All leaf-to-root strings → DFS with a path. Greedy choices fail (a shorter prefix can lose later), so compare complete strings at the leaves.
Why not another pattern? Choosing the smaller child at each step is wrong: the comparison depends on the whole leaf-to-root string.
Approach:
- DFS with
path(root to current). - At a leaf:
s = reversed path; keep the smallest. - Pop on the way back.
class Solution:
def smallestFromLeaf(self, root: Optional[TreeNode]) -> str:
self.best = None
path = []
def dfs(node):
if node is None:
return
path.append(chr(97 + node.val))
if node.left is None and node.right is None:
s = "".join(reversed(path)) # read from the leaf up
if self.best is None or s < self.best:
self.best = s
dfs(node.left)
dfs(node.right)
path.pop()
dfs(root)
return self.best
What the code is doing, line by line:
- Strings start at a leaf, so the root-to-leaf path is reversed.
- Only leaves end a string; internal nodes aren't valid starts.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Single node.
- One leaf string is a prefix of another (
ab<abc). - Equal letters.
Complexity: O(n · h) time (building strings at leaves), O(h) space.
Medium 1038. Binary Search Tree to Greater Sum Tree Amazon
Problem: Replace each BST node's value with the sum of all values greater than or equal to it.
root = [4,1,6,0,2,5,7,null,null,null,3,null,null,null,8][30,36,21,36,35,26,15,null,null,null,33,null,null,null,8]root = [0,null,1][1,null,1]Constraints: The number of nodes in the tree is in the range [1, 100]. · 0 ≤ Node.val ≤ 100 · All the values in the tree are unique.
Hint 1
Which traversal visits a BST in descending order?
Hint 2
Keep a running sum as you go.
From brute force to the pattern: Summing larger values per node is O(n²); one reverse in-order pass is O(n).
Spot it: In a BST, a reverse in-order traversal visits values from largest to smallest, so a running total is exactly the sum of everything ≥ the current node.
Why not another pattern? For each node, summing all larger values separately is O(n²).
Approach:
- Visit right subtree, then node, then left subtree.
total += node.val;node.val = total.
class Solution:
def bstToGst(self, root: TreeNode) -> TreeNode:
total = 0
stack = []
node = root
while stack or node:
while node:
stack.append(node)
node = node.right # biggest values first
node = stack.pop()
total += node.val
node.val = total # sum of all values >= this one
node = node.left
return root
What the code is doing, line by line:
- Reverse in-order of a BST is descending order.
- The running sum at a node includes itself and every larger value.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Single node.
- Skewed tree.
- Zero values.
Complexity: O(n) time, O(h) space.
Medium 1315. Sum of Nodes with Even-Valued Grandparent Amazon
Problem: Sum the values of nodes whose grandparent has an even value.
root = [6,7,8,2,7,1,3,9,null,1,4,null,null,null,5]18root = [1]0Constraints: The number of nodes in the tree is in the range [1, 10⁴]. · 1 ≤ Node.val ≤ 100
Hint 1
What does a node need to know about its ancestors?
Hint 2
Pass the parent's and grandparent's values down the recursion.
From brute force to the pattern: Searching for each node's grandparent is O(n · h); passing values down is O(n).
Spot it: A node only needs its ancestors' values → DFS carrying the parent and grandparent values.
Why not another pattern? Looking up grandparents separately needs parent pointers or repeated searches.
Approach:
dfs(node, parent, grand): addnode.valifgrandis even.- Recurse with
(child, node.val, parent).
class Solution:
def sumEvenGrandparent(self, root: TreeNode) -> int:
def dfs(node, parent, grand):
if node is None:
return 0
total = node.val if grand % 2 == 0 else 0 # grandparent value is even
total += dfs(node.left, node.val, parent) # shift the window down a level
total += dfs(node.right, node.val, parent)
return total
return dfs(root, 1, 1) # odd placeholders: the root has no grandparent
What the code is doing, line by line:
- Passing values down avoids parent pointers.
- Odd placeholders make the top two levels contribute nothing.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- No even values → 0.
- Tree with fewer than 3 levels → 0.
- All even.
Complexity: O(n) time, O(h) space.
Medium 2385. Amount of Time for Binary Tree to Be Infected Amazon Microsoft
Problem: Infection spreads each minute to adjacent nodes, starting from node start. How many minutes until the whole tree is infected?
root = [1,5,3,null,4,10,6,9,2], start = 34root = [1], start = 10Constraints: The number of nodes in the tree is in the range [1, 10⁵]. · 1 ≤ Node.val ≤ 10⁵ · Each node has a unique value. · A node with a value of start exists in the tree.
Hint 1
Infection moves to parents too.
Hint 2
Record parents, then BFS from the start node and count layers.
From brute force to the pattern: Simulating minute by minute over all nodes is O(n²); BFS is O(n).
Spot it: Spreading through neighbours (children and parent) one step per minute → BFS on the tree treated as an undirected graph.
Why not another pattern? DFS heights alone miss paths that go up through ancestors (unless combined carefully).
Approach:
- DFS to record each node's parent and find the start node.
- BFS from start over left, right and parent.
- Count layers minus one.
from collections import deque
class Solution:
def amountOfTime(self, root: Optional[TreeNode], start: int) -> int:
parent = {root: None}
source = None
stack = [root]
while stack:
node = stack.pop()
if node.val == start:
source = node
for child in (node.left, node.right):
if child:
parent[child] = node
stack.append(child)
seen = {source}
queue = deque([source])
minutes = -1
while queue:
minutes += 1 # one more minute of spreading
for _ in range(len(queue)):
node = queue.popleft()
for nb in (node.left, node.right, parent[node]):
if nb and nb not in seen:
seen.add(nb)
queue.append(nb)
return minutes
What the code is doing, line by line:
- Parent links make the tree an undirected graph.
- BFS layer
tis everything infected at minutet; the last layer gives the answer.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Start at the root.
- Start at a leaf.
- Single node → 0.
Complexity: O(n) time and space.
Medium 2583. Kth Largest Sum in a Binary Tree Amazon
Problem: Return the k-th largest level sum (not necessarily distinct), or −1 if there are fewer than k levels.
root = [5,8,9,2,1,3,7,4,6], k = 213root = [1,2,null,3], k = 13Constraints: The number of nodes in the tree is n. · 2 ≤ n ≤ 10⁵ · 1 ≤ Node.val ≤ 10⁶ · 1 ≤ k ≤ n
Hint 1
Compute the sum of each level.
Hint 2
Then find the k-th largest of those sums.
From brute force to the pattern: Sorting the level sums is fine too; a heap of size k is slightly better.
Spot it: Per-level sums come from BFS; then it's a top-k question on those sums.
Why not another pattern? Nothing smarter is needed; the levels are at most n.
Approach:
- BFS one level at a time, summing values.
- If fewer than
klevels, −1; else the k-th largest sum.
import heapq
from collections import deque
class Solution:
def kthLargestLevelSum(self, root: Optional[TreeNode], k: int) -> int:
sums = []
queue = deque([root])
while queue:
total = 0
for _ in range(len(queue)): # one level
node = queue.popleft()
total += node.val
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
sums.append(total)
if len(sums) < k:
return -1
return heapq.nlargest(k, sums)[-1] # k-th largest
What the code is doing, line by line:
- Each BFS round is one level.
nlargest(k, …)[-1]is the k-th largest using a size-k heap.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Fewer than k levels → −1.
- Equal sums (counted separately).
- Single node.
Complexity: O(n + L log k) time for L levels, O(n) space.
Medium 3558. Number of Ways to Assign Edge Weights I Amazon
Problem: In a tree rooted at 1, take a deepest node. Count weightings (1 or 2 per edge) of the root-to-node path with an odd total (mod 10⁹ + 7).
edges = [[1,2]]1edges = [[1,2],[1,3],[3,4],[3,5]]2Constraints: 2 ≤ n ≤ 10⁵ · edges.length == n - 1 · edges[i] == [ui, vi] · 1 ≤ ui, vi ≤ n · edges represents a valid tree.
Hint 1
Does the choice of deepest node matter?
Hint 2
How many of the 2^d weightings give an odd sum?
From brute force to the pattern: Enumerating 2^d weightings is exponential; parity gives 2^(d−1) directly.
Spot it: Only the path length d matters (any deepest node has the same depth). Weight 2 doesn't change parity, so the cost is odd when an odd number of edges weigh 1: exactly half of the 2^d choices. Find d with a BFS over the tree.
Why not another pattern? Enumerating weightings is 2^d.
Approach:
- BFS from node 1 to get the maximum depth
d. - Return
2^(d − 1) mod p.
from collections import deque
class Solution:
def assignEdgeWeights(self, edges: List[List[int]]) -> int:
n = len(edges) + 1
adj = [[] for _ in range(n + 1)]
for a, b in edges:
adj[a].append(b)
adj[b].append(a)
depth = [-1] * (n + 1)
depth[1] = 0
queue = deque([1])
while queue:
u = queue.popleft()
for v in adj[u]:
if depth[v] < 0:
depth[v] = depth[u] + 1
queue.append(v)
d = max(depth) # edges on the path to a deepest node
return pow(2, d - 1, 10**9 + 7) # half of all 2^d weightings are odd
What the code is doing, line by line:
- Flipping the first edge between 1 and 2 pairs up odd and even weightings, so exactly half are odd.
- n ≥ 2 guarantees
d ≥ 1.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Two nodes → 1.
- A long path.
- Many deepest nodes.
Complexity: O(n) time and space.
Hard 968. Binary Tree Cameras Amazon
Problem: A camera monitors its parent, itself and its children. Return the fewest cameras to monitor every node.
root = [0,0,null,0,0]1root = [0,0,null,0,null,0,null,null,0]2Constraints: The number of nodes in the tree is in the range [1, 1000]. · Node.val == 0
Hint 1
Should a leaf ever get a camera?
Hint 2
Decide from the bottom up with a few states per node.
From brute force to the pattern: Trying placements is exponential; tree DP with 3 states (or this greedy) is O(n).
Spot it: Leaves should never hold cameras: their parent covers more. Decide bottom-up (post-order): each node reports one of three states, and a node puts a camera exactly when a child is uncovered.
Why not another pattern? Trying camera placements is exponential; a top-down greedy misplaces cameras.
Approach:
state(node): 0 = not covered, 1 = has a camera, 2 = covered without a camera.Nonecounts as covered (2).- If any child is 0 → place a camera here (1).
- Else if any child is 1 → covered (2). Else → 0 (let the parent handle it).
- If the root ends up 0, add one camera.
class Solution:
def minCameraCover(self, root: Optional[TreeNode]) -> int:
self.cameras = 0
NEEDS, CAMERA, COVERED = 0, 1, 2
def state(node):
if node is None:
return COVERED # empty spots never need cameras
left, right = state(node.left), state(node.right)
if left == NEEDS or right == NEEDS:
self.cameras += 1 # a child is unmonitored: put a camera here
return CAMERA
if left == CAMERA or right == CAMERA:
return COVERED # a child's camera watches this node
return NEEDS # leave it to the parent
if state(root) == NEEDS:
self.cameras += 1 # the root has no parent to cover it
return self.cameras
What the code is doing, line by line:
- Delaying cameras to the parent of an uncovered node covers the most nodes per camera.
- Treating
Noneas covered stops leaves from asking for cameras below them.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Single node → 1.
- A long chain.
- Full binary tree.
Complexity: O(n) time, O(h) space.
Hard 1028. Recover a Tree From Preorder Traversal Amazon
Problem: The preorder output writes each node as D dashes (its depth) followed by its value. Rebuild the tree (a single child is always a left child).
traversal = "1-2--3--4-5--6--7"[1,2,5,3,4,6,7]traversal = "1-2--3---4-5--6---7"[1,2,5,3,null,6,null,4,null,7]Constraints: The number of nodes in the original tree is in the range [1, 1000]. · 1 ≤ Node.val ≤ 10⁹
Hint 1
Turn the string into (depth, value) pairs.
Hint 2
Keep the current path in a stack; the parent of a node at depth d is the path's d-th element.
From brute force to the pattern: Recursive splitting is O(n²) in the worst case; the stack parse is O(n).
Spot it: Preorder with depths: the stack of the current path tells you the parent. Pop until the stack has exactly depth nodes; the top is the parent; attach as left child, or right if left is taken.
Why not another pattern? Recursively splitting the string by depth markers is fiddly and quadratic in the worst case.
Approach:
- Read dashes →
depth, digits →value. - Pop the stack while its size >
depth. - Attach to the top (left first), push the new node.
class Solution:
def recoverFromPreorder(self, traversal: str) -> Optional[TreeNode]:
stack = [] # the path from the root to the last node added
i, n = 0, len(traversal)
while i < n:
depth = 0
while traversal[i] == "-":
depth += 1
i += 1
j = i
while j < n and traversal[j].isdigit():
j += 1
node = TreeNode(int(traversal[i:j]))
i = j
while len(stack) > depth:
stack.pop() # climb back up to the parent's level
if stack:
parent = stack[-1]
if parent.left is None:
parent.left = node
else:
parent.right = node
stack.append(node)
return stack[0]
What the code is doing, line by line:
- In preorder, a node's parent is the most recent node one level up, which is the stack element at index
depth − 1. - Values can have several digits, so they're parsed as runs of digits.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Multi-digit values.
- Single node.
- Deep left chain.
Complexity: O(n) time, O(h) space.
Hard 1373. Maximum Sum BST in Binary Tree Amazon
Problem: Return the maximum sum of keys of any subtree that is a BST (0 if all such sums are negative).
root = [1,4,3,2,4,2,5,null,null,null,null,null,null,4,6]20root = [4,3,null,1,2]2Constraints: The number of nodes in the tree is in the range [1, 4 * 10⁴]. · -4 * 10⁴ ≤ Node.val ≤ 4 * 10⁴
Hint 1
What does a node need from its children to decide if it's a BST root?
Hint 2
Return validity, min, max and sum from each subtree.
From brute force to the pattern: Validating every subtree is O(n²); one post-order pass is O(n).
Spot it: A subtree is a BST if both children are BSTs and left.max < node.val < right.min. Those facts come from the children → post-order DFS returning (is_bst, min, max, sum).
Why not another pattern? Validating each subtree separately is O(n²).
Approach:
- Empty subtree: BST with
min = +∞,max = −∞, sum 0. - Node is a BST if both are and
l.max < val < r.min; update the best sum. - Otherwise mark it not a BST.
class Solution:
def maxSumBST(self, root: Optional[TreeNode]) -> int:
self.best = 0
INF = float("inf")
def walk(node): # (is BST, min, max, sum)
if node is None:
return True, INF, -INF, 0
l_ok, l_min, l_max, l_sum = walk(node.left)
r_ok, r_min, r_max, r_sum = walk(node.right)
if l_ok and r_ok and l_max < node.val < r_min:
total = l_sum + node.val + r_sum
self.best = max(self.best, total)
return True, min(l_min, node.val), max(r_max, node.val), total
return False, 0, 0, 0 # not a BST: ancestors can't be BSTs either
walk(root)
return self.best
What the code is doing, line by line:
- The empty subtree's
min = +∞,max = −∞makes the comparison pass for leaves. beststarts at 0 because an empty BST is allowed.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All negative → 0.
- The whole tree is a BST.
- Duplicate values (not a BST).
Complexity: O(n) time, O(h) space.
Hard 1932. Merge BSTs to Create Single BST Amazon Microsoft
Problem: Given n small BSTs with distinct roots, repeatedly replace a leaf with the tree whose root has that value. Return the final BST if all merge into one valid BST, else None.
trees = [[2,1],[3,2,5],[5,4]][3,2,5,1,null,4]trees = [[5,3,8],[3,2,6]][]Constraints: n == trees.length · 1 ≤ n ≤ 5 * 10⁴ · The number of nodes in each tree is in the range [1, 3]. · Each node in the input may have children but no grandchildren. · No two roots of trees have the same value. · All the trees in the input are valid BSTs. · 1 ≤ TreeNode.val ≤ 5 * 10⁴.
Hint 1
Which tree's root must be the final root?
Hint 2
Attach trees at matching leaves during a DFS that also checks BST bounds.
From brute force to the pattern: Trying merge orders is exponential; the forced structure plus one validating DFS is linear.
Spot it: Each tree root can attach under exactly one matching leaf, so the final root is the only root that isn't a leaf value anywhere (a hash map of roots and a count of leaves). Then DFS from that root, replacing leaves from the map, and validate the BST with value bounds.
Why not another pattern? Trying merge orders is exponential; any order of valid attachments gives the same tree.
Approach:
- Map root value → tree; collect all leaf values.
- The final root is a tree whose root value isn't a leaf value (must be exactly one).
- DFS with bounds
(lo, hi): a leaf matching a stored root is replaced by that tree (removing it from the map); fail if any value breaks the bounds. - Succeed only if every tree was used.
class Solution:
def canMerge(self, trees: List[TreeNode]) -> Optional[TreeNode]:
roots = {t.val: t for t in trees}
leaves = set()
for t in trees:
for child in (t.left, t.right):
if child:
leaves.add(child.val)
top = [t for t in trees if t.val not in leaves]
if len(top) != 1:
return None # no unique final root
root = top[0]
del roots[root.val]
def build(node, lo, hi): # attach trees at leaves and check BST bounds
if node is None:
return True
if not lo < node.val < hi:
return False
if node.left is None and node.right is None and node.val in roots:
sub = roots.pop(node.val) # replace this leaf with its tree
node.left, node.right = sub.left, sub.right
return build(node.left, lo, node.val) and build(node.right, node.val, hi)
ok = build(root, float("-inf"), float("inf"))
return root if ok and not roots else None # every tree must be used
What the code is doing, line by line:
- Root values are distinct and each leaf can take one tree, so the merge structure is forced; only the root is free.
- Checking bounds while attaching validates the whole merged tree in one pass.
- Leftover trees mean some couldn't be attached (or a cycle), so the answer is
None.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Two trees attaching to each other (cycle) → None.
- Already a single tree.
- A merge that breaks BST order.
Complexity: O(n) time and space (each tree has at most 3 nodes).
Medium 1443. Minimum Time to Collect All Apples in a Tree Cisco
Problem: Starting and ending at vertex 0, collect every apple in an undirected tree; each edge takes 1 second. Return the minimum time.
n = 7, edges = [[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]], hasApple = [false,false,true,false,true,true,false]8n = 7, edges = [[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]], hasApple = [false,false,true,false,false,true,false]6Constraints: 1 ≤ n ≤ 10⁵ · edges.length == n - 1 · edges[i].length == 2 · 0 ≤ ai < bi ≤ n - 1 · hasApple.length == n
Hint 1
Which edges must you walk?
Hint 2
Compute, bottom-up, whether each subtree has an apple and what collecting it costs.
From brute force to the pattern: Simulating walks is complicated; counting the needed edges is O(n).
Spot it: An edge must be walked (twice) exactly when the subtree below it holds an apple. That's a post-order DFS on the tree rooted at 0: each child reports its cost and whether it has an apple.
Why not another pattern? Simulating walks or trying orders is unnecessary; only which edges are used matters.
Approach:
- Root the tree at 0 (iteratively).
- Children first:
cost[u] = Σ (cost[child] + 2)over children whose subtree has an apple. - Return
cost[0].
class Solution:
def minTime(self, n: int, edges: List[List[int]], hasApple: List[bool]) -> int:
adj = [[] for _ in range(n)]
for a, b in edges:
adj[a].append(b)
adj[b].append(a)
parent = [-1] * n
order = [0]
for u in order:
for v in adj[u]:
if v != parent[u]:
parent[v] = u
order.append(v)
cost = [0] * n
has = hasApple[:] # does the subtree contain an apple?
for u in reversed(order[1:]): # children before parents
if has[u]:
p = parent[u]
cost[p] += cost[u] + 2 # walk down to u and back
has[p] = True
return cost[0]
What the code is doing, line by line:
- An edge is needed iff the subtree below it contains an apple, and then it's walked once down and once back.
- Processing children before parents accumulates costs bottom-up.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- No apples → 0.
- Apple only at the root → 0.
- Edges listed in any direction.
Complexity: O(n) time and space.
Hard 428. Serialize and Deserialize N-ary Tree Microsoft Apple
Problem: Design serialize(root) and deserialize(data) for an N-ary tree.
Solve it on LeetCode ↗ (Premium)
root = [1,null,3,2,4,null,5,6][1,null,3,2,4,null,5,6]root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14][1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]root = [][]Constraints: The number of nodes in the tree is in the range [0, 10⁴] · 0 ≤ Node.val ≤ 10⁴ · The height of the n-ary tree is less than or equal to 1000 · Do not use class member/global/static variables to store states. Your encode and decode algorithms should be stateless
Hint 1
Preorder alone can't show where a node's children end.
Hint 2
Write the number of children after each value.
From brute force to the pattern: This is already linear; the design question is what to write so it can be decoded.
Spot it: A preorder traversal that writes each node's value and its number of children is enough to rebuild the tree: read a value and a count, then recursively read that many subtrees.
Why not another pattern? Writing only values loses the structure; null markers per possible child don't work when the number of children varies.
Approach:
- Serialize:
val countthen each child's serialization. - Deserialize with an iterator: read
val,count, buildcountchildren recursively.
class Codec:
def serialize(self, root: "Node") -> str:
out = []
def write(node):
out.append(str(node.val))
out.append(str(len(node.children))) # how many subtrees follow
for child in node.children:
write(child)
if root:
write(root)
return " ".join(out)
def deserialize(self, data: str) -> "Node":
if not data:
return None
tokens = iter(data.split())
def read():
node = Node(int(next(tokens)), [])
for _ in range(int(next(tokens))):
node.children.append(read()) # exactly that many children
return node
return read()
What the code is doing, line by line:
- The child count tells the reader exactly where each node's subtree list ends.
- An empty tree serializes to an empty string.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Empty tree.
- A single node.
- Wide nodes with many children.
Complexity: O(n) time and space for both directions.
Hard 2791. Count Paths That Can Form a Palindrome in a Tree Microsoft
Problem: Count pairs u < v whose path's edge letters can be rearranged into a palindrome.
parent = [-1,0,0,1,1,2], s = "acaabc"8parent = [-1,0,0,0,0], s = "aaaaa"10Constraints: n == parent.length == s.length · 1 ≤ n ≤ 10⁵ · 0 ≤ parent[i] ≤ n - 1 for all i ≥ 1 · parent[0] == -1 · parent represents a valid tree. · s consists of only lowercase English letters.
Hint 1
When can letters be rearranged into a palindrome?
Hint 2
Represent letter parities from the root as a bitmask; what is the mask of a u–v path?
From brute force to the pattern: All pairs with path walks are O(n²) or worse; masks + hash map are O(26n).
Spot it: A multiset forms a palindrome iff at most one letter has odd count. Store each node's parity mask from the root; the path's mask is mask[u] ^ mask[v] (the shared part cancels). Count earlier nodes with an equal mask or one bit off using a hash map.
Why not another pattern? Checking every pair of nodes is O(n²).
Approach:
- Compute
mask[v] = mask[parent] ^ bit(s[v])in BFS order. - For each node: add
count[mask]andcount[mask ^ (1 << b)]for each of 26 bits; then record its mask.
from collections import Counter
class Solution:
def countPalindromePaths(self, parent: List[int], s: str) -> int:
n = len(parent)
children = [[] for _ in range(n)]
for v in range(1, n):
children[parent[v]].append(v)
mask = [0] * n
order = [0]
for u in order: # parents before children
for v in children[u]:
mask[v] = mask[u] ^ (1 << (ord(s[v]) - 97)) # letter parity from the root
order.append(v)
seen = Counter()
pairs = 0
for v in order:
m = mask[v]
pairs += seen[m] # every letter even
for b in range(26):
pairs += seen[m ^ (1 << b)] # exactly one odd letter
seen[m] += 1
return pairs
What the code is doing, line by line:
- Edges above the meeting point appear on both root paths, so they cancel in the XOR.
- Counting before inserting counts each pair once.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All the same letter.
- A single node → 0.
- Deep chains.
Complexity: O(26 · n) time, O(n) space.
Medium 889. Construct Binary Tree from Preorder and Postorder Traversal Apple
Problem: Rebuild any binary tree matching the given preorder and postorder traversals (distinct values).
preorder = [1,2,4,5,3,6,7], postorder = [4,5,2,6,7,3,1][1,2,3,4,5,6,7]preorder = [1], postorder = [1][1]Constraints: 1 ≤ preorder.length ≤ 30 · 1 ≤ preorder[i] ≤ preorder.length · All the values of preorder are unique. · postorder.length == preorder.length · 1 ≤ postorder[i] ≤ postorder.length · All the values of postorder are unique. · It is guaranteed that preorder and postorder are the preorder traversal and postorder traversal of the same binary tree.
Hint 1
Where is the left subtree's root in preorder?
Hint 2
Where does that root appear in postorder, and what does that tell you?
From brute force to the pattern: Linear searches for split points make it O(n²); a position map makes it O(n).
Spot it: Like building from preorder + inorder: the root is pre[0]; the next value pre[1] roots the left subtree, and in postorder that root is the last value of the left subtree, so its index gives the subtree size. Recurse (divide and conquer) with a hash map of postorder positions.
Why not another pattern? Searching for the split position linearly each time is O(n²).
Approach:
pos[v]= index ofvin postorder.build(pre_lo, pre_hi, post_lo): rootpre[pre_lo]; if more nodes,left_size = pos[pre[pre_lo + 1]] − post_lo + 1.- Recurse for left and right parts.
class Solution:
def constructFromPrePost(self, preorder: List[int], postorder: List[int]) -> Optional[TreeNode]:
pos = {v: i for i, v in enumerate(postorder)}
def build(pre_lo, pre_hi, post_lo): # preorder[pre_lo..pre_hi] starting at postorder[post_lo]
if pre_lo > pre_hi:
return None
root = TreeNode(preorder[pre_lo])
if pre_lo == pre_hi:
return root
left_root = preorder[pre_lo + 1]
left_size = pos[left_root] - post_lo + 1 # left subtree ends at its root in postorder
root.left = build(pre_lo + 1, pre_lo + left_size, post_lo)
root.right = build(pre_lo + left_size + 1, pre_hi, post_lo + left_size)
return root
return build(0, len(preorder) - 1, 0)
What the code is doing, line by line:
- In postorder a subtree's root comes last, so the left child's index ends the left block.
- When a node has a single child, preorder and postorder can't tell left from right; treating it as left is accepted (any valid tree).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A single node.
- Nodes with one child (ambiguous).
- A full binary tree.
Complexity: O(n) time, O(n) space.