Trees
Published:
Node Definition
What it does:
- Defines the basic building block of a binary tree.
Core idea:
- Every node stores one value and at most two child pointers.
- A full tree is formed by linking nodes recursively.
How it works:
valstores data.leftpoints to left subtree.rightpoints to right subtree.
When to use:
- Any binary tree / BST / recursion question.
Complexity:
- Node access is
O(1).
Edge-case checklist:
- Forgetting to initialize child pointers to
nullptr. - Mixing ownership rules when deleting nodes.
#include <queue>
#include <stack>
#include <vector>
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};DFS Traversals (Recursive)
What it does:
- Visits all tree nodes in depth-first order.
Core idea:
- Recursion gives an implicit stack for backtracking.
- Changing the “visit node” position gives different orders.
How it works:
- Inorder: visit left subtree, then node, then right subtree.
- Preorder: visit node first, then left and right.
- Postorder: visit children first, then node.
When to use:
- Inorder for BST sorted output.
- Preorder for tree serialization/copy.
- Postorder for delete/free and bottom-up logic.
Complexity:
- Time
O(n)for all traversals. - Extra space
O(h)recursion stack (h= tree height).
Edge-case checklist:
- Missing base case
root == nullptr. - Assuming recursion depth is safe for skewed trees.
Dry-run:
- Tree:
1 / \ 2 3 - Inorder traversal output:
[2, 1, 3]. - Preorder traversal output:
[1, 2, 3]. - Postorder traversal output:
[2, 3, 1].
Inorder
void inorderRecursive(TreeNode* root, std::vector<int>& out) {
if (root == nullptr) {
return;
}
inorderRecursive(root->left, out);
out.push_back(root->val);
inorderRecursive(root->right, out);
}Preorder
void preorderRecursive(TreeNode* root, std::vector<int>& out) {
if (root == nullptr) {
return;
}
out.push_back(root->val);
preorderRecursive(root->left, out);
preorderRecursive(root->right, out);
}Postorder
void postorderRecursive(TreeNode* root, std::vector<int>& out) {
if (root == nullptr) {
return;
}
postorderRecursive(root->left, out);
postorderRecursive(root->right, out);
out.push_back(root->val);
}DFS Traversals (Iterative)
What it does:
- Performs DFS without recursion.
Core idea:
- Use an explicit
stackto simulate function calls. - Push order decides traversal order.
How it works:
- Inorder: push all left nodes, process node, move right.
- Preorder: pop node, visit, then push right and left.
- Postorder: use two stacks (or one stack + state) to reverse processing.
When to use:
- When recursion depth might overflow.
- When interviewer asks for iterative version.
Complexity:
- Time
O(n). - Extra space
O(h)average,O(n)worst-case skewed tree.
Edge-case checklist:
- Wrong push order in preorder.
- Not handling empty tree before first push.
Dry-run:
- Same tree as above.
- Iterative preorder:
- Start stack
[1]. - Pop
1, output[1], push3, then2. - Pop
2, output[1, 2]. - Pop
3, output[1, 2, 3].
Inorder
std::vector<int> inorderIterative(TreeNode* root) {
std::vector<int> out;
std::stack<TreeNode*> st;
TreeNode* curr = root;
while (curr != nullptr || !st.empty()) {
while (curr != nullptr) {
st.push(curr);
curr = curr->left;
}
curr = st.top();
st.pop();
out.push_back(curr->val);
curr = curr->right;
}
return out;
}Preorder
std::vector<int> preorderIterative(TreeNode* root) {
std::vector<int> out;
if (root == nullptr) {
return out;
}
std::stack<TreeNode*> st;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top();
st.pop();
out.push_back(node->val);
if (node->right != nullptr) {
st.push(node->right);
}
if (node->left != nullptr) {
st.push(node->left);
}
}
return out;
}Postorder
std::vector<int> postorderIterative(TreeNode* root) {
std::vector<int> out;
if (root == nullptr) {
return out;
}
std::stack<TreeNode*> st1;
std::stack<TreeNode*> st2;
st1.push(root);
while (!st1.empty()) {
TreeNode* node = st1.top();
st1.pop();
st2.push(node);
if (node->left != nullptr) {
st1.push(node->left);
}
if (node->right != nullptr) {
st1.push(node->right);
}
}
while (!st2.empty()) {
out.push_back(st2.top()->val);
st2.pop();
}
return out;
}Level Order (BFS)
What it does:
- Visits nodes level by level from top to bottom.
Core idea:
- Queue preserves first-in-first-out order for each level.
- Queue size at loop start gives current level width.
How it works:
- Start with root in queue.
- Pop
level_sizenodes to form one level. - Push each popped node’s children.
When to use:
- Level-wise output.
- Shortest path in unweighted tree edges.
- Problems like right view, zigzag, level sums.
Complexity:
- Time
O(n). - Extra space
O(w)wherewis max tree width.
Edge-case checklist:
- Mixing nodes from two levels if
level_sizeis not fixed per round. - Forgetting null checks before enqueueing children.
Dry-run:
- Tree:
1 / \ 2 3 / \ \ 4 5 6 - Level 0:
[1] - Level 1:
[2, 3] - Level 2:
[4, 5, 6]
std::vector<std::vector<int>> levelOrder(TreeNode* root) {
std::vector<std::vector<int>> levels;
if (root == nullptr) {
return levels;
}
std::queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
int sz = static_cast<int>(q.size());
std::vector<int> level;
for (int i = 0; i < sz; ++i) {
TreeNode* node = q.front();
q.pop();
level.push_back(node->val);
if (node->left != nullptr) {
q.push(node->left);
}
if (node->right != nullptr) {
q.push(node->right);
}
}
levels.push_back(level);
}
return levels;
}BST: Search, Insert, Delete
What it does:
- Supports ordered insert/search/delete on tree data.
Core idea:
- BST invariant: left subtree values < node < right subtree values.
- Each comparison discards half of remaining subtree path.
How it works:
- Search: keep moving left/right by comparison.
- Insert: recurse/iterate until null spot and attach new node.
- Delete: leaf -> remove; one child -> promote child; two children -> copy inorder successor, then delete successor node.
When to use:
- Dynamic ordered set/map style operations.
- Problems needing predecessor/successor and in-order traversal.
Complexity:
- Average
O(log n)for search/insert/delete. - Worst-case
O(n)on skewed tree.
Edge-case checklist:
- Not handling duplicate keys consistently.
- Memory leaks when deleting nodes.
- Forgetting to return updated subtree root after recursion.
Dry-run:
- BST insert sequence:
5, 3, 7, 2, 4, 6, 8. - Search
4:5 -> 3 -> 4found. - Delete
7: - Node
7has two children (6,8). - Inorder successor is
8. - Replace
7with8, then delete old8leaf.
Search
TreeNode* searchBst(TreeNode* root, int target) {
TreeNode* curr = root;
while (curr != nullptr) {
if (curr->val == target) {
return curr;
}
curr = (target < curr->val) ? curr->left : curr->right;
}
return nullptr;
}Insert
TreeNode* insertBst(TreeNode* root, int x) {
if (root == nullptr) {
return new TreeNode(x);
}
if (x < root->val) {
root->left = insertBst(root->left, x);
} else if (x > root->val) {
root->right = insertBst(root->right, x);
}
return root;
}Delete
TreeNode* findMin(TreeNode* root) {
while (root != nullptr && root->left != nullptr) {
root = root->left;
}
return root;
}
TreeNode* deleteBst(TreeNode* root, int key) {
if (root == nullptr) {
return nullptr;
}
if (key < root->val) {
root->left = deleteBst(root->left, key);
} else if (key > root->val) {
root->right = deleteBst(root->right, key);
} else {
if (root->left == nullptr) {
TreeNode* right = root->right;
delete root;
return right;
}
if (root->right == nullptr) {
TreeNode* left = root->left;
delete root;
return left;
}
TreeNode* succ = findMin(root->right);
root->val = succ->val;
root->right = deleteBst(root->right, succ->val);
}
return root;
}LCA (Binary Tree)
What it does:
- Finds the lowest node that is ancestor of both target nodes.
Core idea:
- Postorder-like recursion asks left and right subtrees where targets exist.
- Split between left and right means current node is the answer.
How it works:
- If current node is null or matches a target, return it.
- Recurse into left and right.
- If both sides return non-null, current node is LCA.
- Otherwise return whichever side is non-null.
When to use:
- Relationship queries in trees.
- Distance/path problems between two nodes.
Complexity:
- Time
O(n). - Extra space
O(h)recursion stack.
Edge-case checklist:
- Confusing binary-tree LCA with BST-specific LCA optimization.
- Not clarifying whether both targets are guaranteed to exist.
Dry-run:
- Tree:
3 / \ 5 1 / \ / \ 6 2 0 8 - Query
p=6,q=2. 6is in left subtree of5,2is also in left subtree of5.- Lowest node covering both is
5.
TreeNode* lcaBinaryTree(TreeNode* root, int p, int q) {
if (root == nullptr || root->val == p || root->val == q) {
return root;
}
TreeNode* left = lcaBinaryTree(root->left, p, q);
TreeNode* right = lcaBinaryTree(root->right, p, q);
if (left != nullptr && right != nullptr) {
return root;
}
return (left != nullptr) ? left : right;
}Diameter (Binary Tree)
What it does:
- Computes longest path length between any two nodes (in edges).
Core idea:
- Best path through one node equals
left_height + right_height. - While computing height, keep updating global best diameter.
How it works:
- Recursively compute left/right heights.
- Update
diameter = max(diameter, left + right). - Return
1 + max(left, right)as height to parent.
When to use:
- Longest path problems in trees.
- Foundation for many tree DP questions.
Complexity:
- Time
O(n). - Extra space
O(h).
Edge-case checklist:
- Mixing “number of edges” vs “number of nodes” definition.
- Recomputing height separately causing
O(n^2).
Dry-run:
- Tree:
1 / \ 2 3 / 4 - At node
2: left height1, right height0, local diameter1. - At node
1: left height2, right height1, local diameter3. - Final diameter (edges) =
3(4 -> 2 -> 1 -> 3).
int heightForDiameter(TreeNode* root, int& diameter) {
if (root == nullptr) {
return 0;
}
int lh = heightForDiameter(root->left, diameter);
int rh = heightForDiameter(root->right, diameter);
diameter = std::max(diameter, lh + rh);
return 1 + std::max(lh, rh);
}
int diameterBinaryTree(TreeNode* root) {
int diameter = 0;
heightForDiameter(root, diameter);
return diameter;
}