Trees & Binary Search Tree
Think of it like this
A tree is like a company org chart: the CEO is at the top (root), each manager has direct reports (children), and everyone has exactly one boss (parent). There are no loops, you can't be your own manager's manager.
A binary tree means each node has at most two children: left and right.
A binary search tree (BST) adds an ordering rule: everything to the left is smaller, everything to the right is larger. This one rule is what gives you fast search, insert, and sorted traversal.
Vocabulary
50 ← Root (no parent)
/ \
30 70 ← Internal nodes (have children)
/ \ / \
20 40 60 80 ← Leaf nodes (no children)
Terms:
Height of tree = 2 (longest root-to-leaf path, counting edges)
Depth of node 60 = 2 (root-to-60 edges)
Subtree rooted at 30 = {20, 30, 40}The BST Property
For every node N in a BST:
- Every value in N's left subtree is strictly less than N.val
- Every value in N's right subtree is strictly greater than N.val
Valid BST: Invalid (8 is in right subtree of 10
but 8 < 10, violating the rule):
10 10
/ \ / \
5 15 5 15
/ \ / \ / \ /
3 7 12 20 3 7 8 ← 8 should be left of 10, not right of 15The payoff: inorder traversal (left → node → right) visits all values in sorted order.
Node Implementation
class TreeNode {
constructor(val) {
this.val = val;
this.left = null;
this.right = null;
}
}
// Insert into BST
function insert(root, val) {
if (!root) return new TreeNode(val);
if (val < root.val) root.left = insert(root.left, val);
else if (val > root.val) root.right = insert(root.right, val);
// equal: BST typically ignores duplicates (or you choose a side)
return root;
}
// Search BST
function search(root, val) {
if (!root || root.val === val) return root;
return val < root.val
? search(root.left, val)
: search(root.right, val);
}
// Delete from BST (the trickiest operation)
function deleteNode(root, val) {
if (!root) return null;
if (val < root.val) { root.left = deleteNode(root.left, val); }
else if (val > root.val) { root.right = deleteNode(root.right, val); }
else {
// Found the node to delete
if (!root.left) return root.right; // no left child → replace with right
if (!root.right) return root.left; // no right child → replace with left
// Two children: replace val with inorder successor (min of right subtree)
let successor = root.right;
while (successor.left) successor = successor.left;
root.val = successor.val;
root.right = deleteNode(root.right, successor.val);
}
return root;
}Time Complexity
| Operation | Average (balanced) | Worst (skewed) | Why worst is bad |
|---|---|---|---|
| Search | O(log n) | O(n) | Skewed tree = linked list |
| Insert | O(log n) | O(n) | Must find correct position |
| Delete | O(log n) | O(n) | Must find node + rebalance |
| Inorder traversal | O(n) | O(n) | Must visit every node |
| Find min/max | O(log n) | O(n) | Leftmost/rightmost node |
Critical insight: A BST inserted in sorted order degrades to a linked list (height = n). This is why balanced BSTs (AVL, Red-Black) exist.
Four Traversal Orders
Tree: 4
/ \
2 6
/ \ / \
1 3 5 7
Inorder (L → Root → R): 1, 2, 3, 4, 5, 6, 7 ← SORTED! 🎉
Preorder (Root → L → R): 4, 2, 1, 3, 6, 5, 7 ← used to serialize/copy trees
Postorder (L → R → Root): 1, 3, 2, 5, 7, 6, 4 ← delete subtrees bottom-up
Level-order (BFS by row): 4, 2, 6, 1, 3, 5, 7 ← used for "right side view"// Recursive traversals (clean but uses O(h) call stack space)
function inorder(node, result = []) {
if (!node) return result;
inorder(node.left, result);
result.push(node.val); // ← visit in the middle
inorder(node.right, result);
return result;
}
function preorder(node, result = []) {
if (!node) return result;
result.push(node.val); // ← visit first
preorder(node.left, result);
preorder(node.right, result);
return result;
}
// Iterative inorder, often required in interviews to avoid stack overflow
function inorderIterative(root) {
const result = [], stack = [];
let curr = root;
while (curr || stack.length) {
// Go as far left as possible
while (curr) { stack.push(curr); curr = curr.left; }
curr = stack.pop();
result.push(curr.val); // ← visit
curr = curr.right; // ← then go right
}
return result;
}
// Level-order (BFS)
function levelOrder(root) {
if (!root) return [];
const result = [], queue = [root];
while (queue.length) {
const levelSize = queue.length;
const level = [];
for (let i = 0; i < levelSize; i++) {
const node = queue.shift();
level.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
result.push(level);
}
return result;
}Binary Search Tree: Search for 7
Start at the root (8). We are looking for 7. Since 7 < 8, we go left.
Key Recursive Patterns
Most tree problems use one of two DFS patterns:
// Pattern A: Compute something bottom-up (return a value from subtrees)
function height(node) {
if (!node) return 0;
const leftH = height(node.left);
const rightH = height(node.right);
return 1 + Math.max(leftH, rightH); // combine left + right results
}
// Pattern B: Carry information top-down (pass parameters down)
function isValidBST(node, min = -Infinity, max = Infinity) {
if (!node) return true;
if (node.val <= min || node.val >= max) return false;
return isValidBST(node.left, min, node.val) && // left: tighten max
isValidBST(node.right, node.val, max); // right: tighten min
}
// Pattern C: Path problems with a global variable
let maxPathSum = -Infinity;
function maxGain(node) {
if (!node) return 0;
const left = Math.max(0, maxGain(node.left)); // ignore negative gains
const right = Math.max(0, maxGain(node.right));
maxPathSum = Math.max(maxPathSum, node.val + left + right); // update global
return node.val + Math.max(left, right); // only one branch goes upward
}Balanced BSTs to Know (Reference)
| Structure | Balance Guarantee | Used In |
|---|---|---|
| AVL Tree | Height ≤ 1.44 log₂ n | Databases needing fast lookup |
| Red-Black Tree | Height ≤ 2 log₂(n+1) | C++ std::map, Java TreeMap |
| B-Tree / B+ Tree | Disk-page-sized nodes, multi-key | File systems, database indices |
You won't implement these in interviews, but knowing why they exist (to prevent O(n) worst case) is expected.
Real-World Frontend Application
- The DOM is a tree: Every HTML element is a node; children are nested elements. DOM traversal APIs (
parentNode,firstChild,nextSibling) walk this tree - React Virtual DOM: A massive tree of React elements. The reconciler DFS-traverses the old tree and new tree to find differences
- File system explorer: Every folder is an internal node; files are leaves. Expanding a folder in VS Code is tree traversal
- Abstract Syntax Tree (AST): Babel, ESLint, TypeScript all parse your code into a tree of nodes before transforming or checking it
Common Mistakes
| Mistake | Example | Fix |
|---|---|---|
| Only checking node val, not bounds | Validating BST: if (left.val < node.val) | Pass min/max bounds recursively |
| Off-by-one on height vs depth | Mixing edges vs nodes for height | Pick one convention and be consistent |
| Modifying tree while traversing | In-place delete during traversal | Collect deletions, apply after |
| Not handling null root | root.val when tree is empty | Always check if (!root) return ... first |
Key Problems to Solve
| # | Problem | Pattern | Difficulty |
|---|---|---|---|
| 1 | Maximum Depth of Binary Tree | Bottom-up DFS | Easy |
| 2 | Symmetric Tree | Mirror comparison | Easy |
| 3 | Invert Binary Tree | DFS swap | Easy |
| 4 | Validate Binary Search Tree | Top-down bounds | Medium |
| 5 | Lowest Common Ancestor of BST | Divergence point | Easy |
| 6 | Lowest Common Ancestor (general BT) | DFS + combine | Medium |
| 7 | Binary Tree Right Side View | Level-order, take last | Medium |
| 8 | Kth Smallest Element in BST | Inorder k-th | Medium |
| 9 | Path Sum II | DFS backtracking | Medium |
| 10 | Construct BST from Preorder | Bounds splitting | Medium |
| 11 | Serialize and Deserialize Binary Tree | BFS/DFS encode + decode | Hard |
| 12 | Binary Tree Maximum Path Sum | DFS with global max | Hard |
