One place to revise everything about Trees before your upcoming interview.
1οΈβ£ What is a Tree?
A Tree is a non-linear, hierarchical data structure where a single node (the root) connects to zero or more child nodes, forming a recursive structure.
void postorder(TreeNode* root, vector<int>& res) { if (!root) return; postorder(root->left, res); postorder(root->right, res); res.push_back(root->val); // Process at root}
When to Use Which DFS Order
Preorder β when you need to process root before children (e.g., cloning a tree, serialization)
Inorder β when you need sorted order in BST
Postorder β when you need to process children before root (e.g., computing height, deleting nodes, freeing memory)
4οΈβ£ DFS Traversals β Iterative
Unified Pattern
Both iterative inorder and preorder follow the same skeleton β the only difference is where you record root->val. This makes them easy to memorize together.
Trick: Modified preorder (Root β Right β Left) then reverse gives (Left β Right β Root).
vector<int> postorderTraversal(TreeNode* root) { vector<int> res; if (!root) return res; stack<TreeNode*> st; st.push(root); while (!st.empty()) { TreeNode* curr = st.top(); st.pop(); res.push_back(curr->val); if (curr->left) st.push(curr->left); // Left first (opposite of preorder) if (curr->right) st.push(curr->right); // Right second } reverse(res.begin(), res.end()); // Reverse entire result return res;}
All-in-One Traversal (Pre + In + Post in Single Pass)
Implementation & Explanation
Using a stack<pair<node*, int>> to track state:
State
Order
Action
0
Preorder
Record β push left
1
Inorder
Record β push right
2
Postorder
Record β pop
void allTraversals(TreeNode* root) { if (!root) return; stack<pair<TreeNode*, int>> st; st.push({root, 0}); vector<int> pre, in, post; while (!st.empty()) { auto [node, state] = st.top(); st.pop(); if (state == 0) { pre.push_back(node->val); st.push({node, 1}); if (node->left) st.push({node->left, 0}); } else if (state == 1) { in.push_back(node->val); st.push({node, 2}); if (node->right) st.push({node->right, 0}); } else { post.push_back(node->val); } }}
// TC: O(3N) β O(N) | SC: O(N)
5οΈβ£ Morris Traversal β O(1) Space, No Stack, No Recursion
Why Morris?
Both recursive and iterative DFS use O(H) space (call stack or explicit stack).
Morris Traversal achieves O(1) extra space by temporarily modifying the tree β creating threaded links from the rightmost node of the left subtree back to the current node, then restoring the tree afterward.
Idea: For each node, find the inorder predecessor (rightmost node in left subtree). Thread its right pointer back to the current node. This lets us return to the parent without a stack.
vector<int> inorderTraversal(TreeNode* root) { vector<int> ans; while (root != NULL) { if (root->left == NULL) { ans.push_back(root->val); // No left subtree β process & go right root = root->right; } else { auto leftNode = root->left; while (leftNode->right) leftNode = leftNode->right; // Find predecessor leftNode->right = root; // Create thread back to root auto tem = root; root = root->left; // Move to left subtree tem->left = NULL; // Break original left link (avoid infinite loop) } } return ans;}// TC: O(N) β each edge traversed at most 2 times | SC: O(1)
How Morris Works β Step by Step
Original: 1
/ \
2 3
/ \
4 5
Step 1: root=1, left exists β find predecessor of 1 in left subtree
Predecessor = 5 (rightmost in left subtree of 1)
Thread: 5->right = 1, move to root->left (root=2), break 1->left
Step 2: root=2, left exists β predecessor = 4
Thread: 4->right = 2, move to root->left (root=4), break 2->left
Step 3: root=4, no left β PRINT 4, go right (thread takes us to 2)
Step 4: root=2, no left β PRINT 2, go right (thread takes us to 5)
Step 5: root=5, no left β PRINT 5, go right (thread takes us to 1)
Step 6: root=1, no left β PRINT 1, go right (root=3)
Step 7: root=3, no left β PRINT 3, go right (NULL) β done
Output: 4 2 5 1 3 β (correct inorder)
TheΒ diameterΒ of a binary tree is theΒ lengthΒ of the longest path between any two nodes in a tree. This path may or may not pass through theΒ root.
The diameter passes through a node = leftHeight + rightHeight. Track global max.
int diameterOfBinaryTree(TreeNode* root) { int diameter = 0; height(root, diameter); return diameter;}int height(TreeNode* root, int& diameter) { if (!root) return 0; int lh = height(root->left, diameter); int rh = height(root->right, diameter); diameter = max(diameter, lh + rh); // Update diameter at this node return 1 + max(lh, rh); // Return height}
vector<int> rightSideView(TreeNode* root) { if(!root) return {}; queue<TreeNode*> q; q.push(root); vector<int> ans; while (!q.empty()) { int sz = q.size(); while (sz--) { auto curr = q.front(); q.pop(); if (sz == 0) ans.push_back(curr->val); // Last of level if (curr->left) q.push(curr->left); if (curr->right) q.push(curr->right); } } return ans;}
DFS approach β visit right child first, add first node at each depth:
vector<int> rightSideView(TreeNode* root) { vector<int> res; dfs(root, res, 0); return res;}void dfs(TreeNode* root, vector<int>& res, int level) { if (!root) return; if (res.size() == level) // First node at this depth res.push_back(root->val); dfs(root->right, res, level + 1); // Right first for right view dfs(root->left, res, level + 1);}
void dfs(TreeNode* root, vector<int>& res, int level) { if (!root) return; if (res.size() == level) res.push_back(root->val); dfs(root->left, res, level + 1); // Left first for left view dfs(root->right, res, level + 1);}
Top View & Bottom View
Use Horizontal Distance (HD): root = 0, left child = HDβ1, right child = HD+1.
Top View β BFS, keep first node at each HD:
vector<int> topView(TreeNode* root) { if (!root) return {}; map<int, int> hdMap; // HD β node value (ordered map) queue<pair<TreeNode*, int>> q; // node, hd q.push({root, 0}); while (!q.empty()) { auto [node, hd] = q.front(); q.pop(); if (!hdMap.count(hd)) // First node at this HD wins hdMap[hd] = node->val; if (node->left) q.push({node->left, hd - 1}); if (node->right) q.push({node->right, hd + 1}); } vector<int> res; for (auto& [hd, val] : hdMap) res.push_back(val); return res;}
Bottom View β same BFS, but overwrite at each HD (last node wins):
// Only difference from Top View: remove the if-check, always overwritehdMap[hd] = node->val; // Every node overwrites β last (deepest) wins
Top View vs Bottom View
Top View β if (!map.count(hd)) β first node at each HD (BFS ensures shallowest first)
Bottom View β always overwrite β last node at each HD (BFS ensures deepest last)
Both use map<int, int> (ordered by HD) to get left-to-right order.
Any node to any node (not necessarily root-to-leaf). Classic interview problem.
int maxPathSum(TreeNode* root) { int maxSum = INT_MIN; maxGain(root, maxSum); return maxSum;}int maxGain(TreeNode* root, int& maxSum) { if (!root) return 0; int left = max(0, maxGain(root->left, maxSum)); // Ignore negative paths int right = max(0, maxGain(root->right, maxSum)); maxSum = max(maxSum, left + root->val + right); // Path through this node return root->val + max(left, right); // Return best single branch}
Key Insight
At each node, the path through it = leftGain + val + rightGain (this updates global max).
But we can only return one branch upward (left OR right, not both) β a path canβt fork.
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { if (!root || root == p || root == q) return root; TreeNode* left = lowestCommonAncestor(root->left, p, q); TreeNode* right = lowestCommonAncestor(root->right, p, q); if (left && right) return root; // p and q are in different subtrees return left ? left : right; // Both in same subtree}// TC: O(N) | SC: O(H)
LCA Intuition
If p is found in the left subtree and q in the right subtree β current root is the LCA.
If both found on one side β the LCA is deeper on that side (returned by recursive call).
Preorder: [3, 9, 20, 15, 7] Inorder: [9, 3, 15, 20, 7]
β root β left | right β
preorder[0] = 3 = root
In inorder: everything left of 3 = left subtree, right of 3 = right subtree
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) { unordered_map<int, int> inMap; for (int i = 0; i < inorder.size(); i++) inMap[inorder[i]] = i; int preIdx = 0; return build(preorder, preIdx, 0, inorder.size() - 1, inMap);}TreeNode* build(vector<int>& preorder, int& preIdx, int inLeft, int inRight, unordered_map<int, int>& inMap) { if (inLeft > inRight) return nullptr; TreeNode* root = new TreeNode(preorder[preIdx++]); int inIdx = inMap[root->val]; root->left = build(preorder, preIdx, inLeft, inIdx - 1, inMap); root->right = build(preorder, preIdx, inIdx + 1, inRight, inMap); return root;}// TC: O(N) | SC: O(N) for hashmap
Build a parent map, then BFS from target node for k levels:
vector<int> distanceK(TreeNode* root, TreeNode* target, int k) { unordered_map<TreeNode*, TreeNode*> parent; buildParentMap(root, nullptr, parent); queue<TreeNode*> q; unordered_set<TreeNode*> visited; q.push(target); visited.insert(target); while (!q.empty() && k > 0) { int sz = q.size(); while (sz--) { auto curr = q.front(); q.pop(); // Go left, right, AND parent for (auto next : {curr->left, curr->right, parent[curr]}) { if (next && !visited.count(next)) { visited.insert(next); q.push(next); } } } k--; } vector<int> ans; while (!q.empty()) { ans.push_back(q.front()->val); q.pop(); } return ans;}void buildParentMap(TreeNode* root, TreeNode* par, unordered_map<TreeNode*, TreeNode*>& parent) { if (!root) return; parent[root] = par; buildParentMap(root->left, root, parent); buildParentMap(root->right, root, parent);}
The Parent Map Trick
Trees only have downward pointers. To traverse upward, build a parent map first.
Then BFS works in all 3 directions: left, right, parent β like a graph.
π§ Master Cheatsheet
The Decision Framework
What does the problem ask?
β
βββ "Traverse the tree"
β βββ Level-by-level? β BFS with queue
β βββ Path / subtree? β DFS (recursion or stack)
β
βββ "Tree property" (depth, balanced, symmetric, same)
β βββ DFS β compute bottom-up, return value from recursion
β
βββ "Side view" (left, right, top, bottom)
β βββ Left/Right β BFS (first/last of level) or DFS (depth tracking)
β βββ Top/Bottom β BFS with Horizontal Distance + ordered map
β
βββ "Path sum" / "root to leaf"
β βββ Single path exists? β DFS with target subtraction
β βββ All paths? β DFS + backtracking
β βββ Any-to-any path? β Prefix sum + hashmap on tree
β
βββ "Lowest Common Ancestor"
β βββ Binary Tree β Recursive: left/right both found = root is LCA
β βββ BST β Use BST property to pick direction
β
βββ "Construct tree from traversals"
β βββ Find root from pre/postorder β split using inorder position
β
βββ "BST operation" (validate, insert, delete, kth)
β βββ Use BST property: left < root < right
β βββ Inorder = sorted β kth smallest, validate, recover
β
βββ "Serialize / distance / misc"
βββ Serialize: preorder + null markers
βββ Distance K: parent map + BFS
Complexity Reference
Operation
Time
Space
Any single traversal
O(N)
O(H)
Level-order BFS
O(N)
O(W) β€ O(N)
BST Search / Insert / Delete
O(H)
O(H)
Balanced BST operations
O(logN)
O(logN)
Build tree from traversals
O(N)
O(N)
Serialize / Deserialize
O(N)
O(N)
Where H = height (logN for balanced, N for skewed), W = max width of tree.
Pattern Recognition Table
Signal in problem
Pattern to use
βLevel orderβ / βby levelβ / βzigzagβ
BFS with queue, level-by-level template
βDepthβ / βheightβ / βbalancedβ
DFS, bottom-up return
βSameβ / βsymmetricβ / βmirrorβ
DFS, compare two trees simultaneously
βPath sumβ / βroot to leafβ
DFS with target subtraction
βMaximum path sumβ (any to any)
DFS, max(0, child) + global max
βPath sum IIIβ (any node downward)
Prefix sum + hashmap
βRight/left side viewβ
BFS (first/last of level) or DFS with depth
βTop/bottom viewβ
BFS + horizontal distance + ordered map
βBoundary traversalβ
Left boundary + leaves + right boundary
βLCAβ
Recursive check left/right subtrees
βValidate BSTβ
DFS with (min, max) range
βKth smallest in BSTβ
Iterative inorder, count to k
βConstruct from preorder + inorderβ
Preorder[0] = root, split by inorder index
βSerialize / deserializeβ
Preorder with null markers + queue
βDistance Kβ / βall nodes at distanceβ
Parent map + BFS
βDiameterβ
At each node: leftH + rightH, track global
Common Gotchas
1. minDepth: if one child is null, DON'T return 0 β go to the other child
2. BST validation: use long (LONG_MIN/MAX) to handle INT_MIN/INT_MAX edge cases
3. Path Sum: check at LEAF (both children null), not just any null node
4. LCA: return root when root == p || root == q (even before checking children)
5. Postorder iterative: the 1-stack version needs careful backtracking logic
6. Tree construction: preIdx must be passed by REFERENCE (not value)
7. Side views: DFS needs level tracking; BFS needs level-by-level snapshot
8. Prefix sum on tree: MUST backtrack the map when returning from subtree
9. Max Path Sum: return one branch only (can't fork), but update global with both
10. Diameter: it's EDGES not NODES β don't add 1 to leftH + rightH