Trees — The Complete Visual Reference

Tree fundamentals, BSTs, traversals, tries, segment trees, Fenwick trees — visual diagrams, complexity tags & 24 algorithm patterns
What Is a Tree?
CEO ← root / \ CTO CFO ← children of root / \ \ Dev QA Acct ← leaves
TermDefinition
RootTop node, no parent
NodeElement storing data
EdgeLink between parent & child
ParentNode directly above
ChildNode directly below
LeafNode with no children
DepthDistance from root (root = 0)
HeightLongest path to a leaf
SubtreeA node + all its descendants
TypeDescription
BinaryAt most 2 children per node
BSTBinary + left < parent < right
Balanced BSTHeight = O(log n)
CompleteAll levels full except possibly last (filled left)
FullEvery node has 0 or 2 children
PerfectAll internal nodes have 2 children, all leaves same level
N-aryUp to N children per node
TriePrefix tree for strings
Segment TreeRange queries on arrays
HeapComplete tree with heap property
Binary Tree — Structure & Properties
Full Complete Perfect Degenerate 1 1 1 1 / \ / \ / \ \ 2 3 2 3 2 3 2 / \ / \ / \ / \ \ 4 5 4 5 4 5 6 7 3 \ 4
class TreeNode( var `val`: Int, var left: TreeNode? = null, var right: TreeNode? = null )
PropertyFormula
Max nodes at level d2d
Max nodes height h2(h+1) − 1
Min heightfloor(log₂(n))
ShapeHeightSearch
BalancedO(log n)O(log n)
DegenerateO(n)O(n)
Memory Model & Representations
TreeNode on JVM (~40 bytes) ┌─────────────────────┐ │ header 16 bytes │ │ val: Int 4 bytes │ │ left: ref 8 bytes │ │ right: ref 8 bytes │ │ padding 4 bytes │ └─────────────────────┘
Array representation (complete binary tree) Index: 0 1 2 3 4 5 6 Value: [1, 2, 3, 4, 5, 6, 7] Parent of i → (i - 1) / 2 Left child of i → 2 * i + 1 Right child of i→ 2 * i + 2
DFS — Inorder, Preorder, Postorder
1 / \ 2 3 / \ \ 4 5 6 Inorder (L, Root, R): 4, 2, 5, 1, 3, 6 Preorder (Root, L, R): 1, 2, 4, 5, 3, 6 Postorder (L, R, Root): 4, 5, 2, 6, 3, 1
// Inorder: left → root → right fun inorder(node: TreeNode?, result: MutableList<Int>) { if (node == null) return inorder(node.left, result) result.add(node.`val`) inorder(node.right, result) } // Preorder: root → left → right fun preorder(node: TreeNode?, result: MutableList<Int>) { if (node == null) return result.add(node.`val`) preorder(node.left, result) preorder(node.right, result) } // Postorder: left → right → root fun postorder(node: TreeNode?, result: MutableList<Int>) { if (node == null) return postorder(node.left, result) postorder(node.right, result) result.add(node.`val`) }
OrderWhen to Use
InorderBST sorted order
PreorderSerialize / copy tree
PostorderDelete tree / compute heights
BFS — Level-Order Traversal
fun levelOrder(root: TreeNode?): List<Int> { if (root == null) return emptyList() val result = mutableListOf<Int>() val queue: Queue<TreeNode> = LinkedList() queue.add(root) while (queue.isNotEmpty()) { val node = queue.poll() result.add(node.`val`) node.left?.let { queue.add(it) } node.right?.let { queue.add(it) } } return result } // Grouped by level fun levelOrderGrouped(root: TreeNode?): List<List<Int>> { if (root == null) return emptyList() val result = mutableListOf<List<Int>>() val queue: Queue<TreeNode> = LinkedList() queue.add(root) while (queue.isNotEmpty()) { val size = queue.size // snapshot! val level = mutableListOf<Int>() repeat(size) { val node = queue.poll() level.add(node.`val`) node.left?.let { queue.add(it) } node.right?.let { queue.add(it) } } result.add(level) } return result }
AspectDFSBFS
Data structureStack (recursion)Queue
SpaceO(h)O(w)
Best forPath problemsLevel / distance queries
Iterative Traversals (Stack)
// Inorder iterative fun inorderIterative(root: TreeNode?): List<Int> { val result = mutableListOf<Int>() val stack = ArrayDeque<TreeNode>() var cur = root while (cur != null || stack.isNotEmpty()) { while (cur != null) { stack.addFirst(cur) cur = cur.left // push all left } cur = stack.removeFirst() result.add(cur.`val`) // visit cur = cur.right // go right } return result } // Preorder iterative fun preorderIterative(root: TreeNode?): List<Int> { if (root == null) return emptyList() val result = mutableListOf<Int>() val stack = ArrayDeque<TreeNode>() stack.addFirst(root) while (stack.isNotEmpty()) { val node = stack.removeFirst() result.add(node.`val`) node.right?.let { stack.addFirst(it) } node.left?.let { stack.addFirst(it) } } return result } // Postorder iterative (modified preorder + reverse) fun postorderIterative(root: TreeNode?): List<Int> { if (root == null) return emptyList() val result = LinkedList<Int>() val stack = ArrayDeque<TreeNode>() stack.addFirst(root) while (stack.isNotEmpty()) { val node = stack.removeFirst() result.addFirst(node.`val`) node.left?.let { stack.addFirst(it) } node.right?.let { stack.addFirst(it) } } return result }
Morris Traversal — O(1) Space
fun morrisInorder(root: TreeNode?): List<Int> { val result = mutableListOf<Int>() var cur = root while (cur != null) { if (cur.left == null) { result.add(cur.`val`) cur = cur.right } else { var pred = cur.left!! while (pred.right != null && pred.right != cur) pred = pred.right!! if (pred.right == null) { pred.right = cur // create thread cur = cur.left } else { pred.right = null // remove thread result.add(cur.`val`) cur = cur.right } } } return result }
ProsCons
O(1) extra spaceTemporarily modifies tree
O(n) timeMore complex logic
BST Fundamentals
Valid BST Invalid BST 8 8 / \ / \ 3 10 3 10 / \ \ / \ \ 1 6 14 1 9 14 9 > 8 ✗
Search for 6: 8 → go left / 3 → go right \ 6 ✓ found!
OperationBST (balanced)BST (unbalanced)Sorted Array
SearchO(log n)O(n)O(log n)
InsertO(log n)O(n)O(n)
DeleteO(log n)O(n)O(n)
BST Operations — Search, Insert, Delete
// Search (iterative) fun searchBST(root: TreeNode?, target: Int): TreeNode? { var node = root while (node != null) { node = when { target < node.`val` -> node.left target > node.`val` -> node.right else -> return node } } return null } // Insert fun insertBST(root: TreeNode?, v: Int): TreeNode { if (root == null) return TreeNode(v) if (v < root.`val`) root.left = insertBST(root.left, v) else root.right = insertBST(root.right, v) return root } // Delete (3 cases) fun deleteBST(root: TreeNode?, key: Int): TreeNode? { if (root == null) return null 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 == null) return root.right // case 1 & 2 if (root.right == null) return root.left val succ = findMin(root.right!!) // case 3 root.`val` = succ.`val` root.right = deleteBST(root.right, succ.`val`) } return root }
Delete cases: Leaf: remove directly One child: replace with child Two children: replace with inorder successor (smallest in right subtree)
fun findMin(node: TreeNode): TreeNode { var cur = node while (cur.left != null) cur = cur.left!! return cur } fun findMax(node: TreeNode): TreeNode { var cur = node while (cur.right != null) cur = cur.right!! return cur }
BST Validation
// Correct: check with min/max bounds fun isValidBST(root: TreeNode?): Boolean { fun validate(node: TreeNode?, min: Long, max: Long): Boolean { if (node == null) return true if (node.`val` <= min || node.`val` >= max) return false return validate(node.left, min, node.`val`.toLong()) && validate(node.right, node.`val`.toLong(), max) } return validate(root, Long.MIN_VALUE, Long.MAX_VALUE) } // WRONG: only checks immediate children fun isValidBSTWrong(node: TreeNode?): Boolean { if (node == null) return true if (node.left != null && node.left!!.`val` >= node.`val`) return false // ✗ Misses: grandchild could violate ancestor constraint return isValidBSTWrong(node.left) && isValidBSTWrong(node.right) }
Balanced BSTs — AVL & Red-Black
AVL Rotations: Right (LL) Left (RR) Left-Right (LR) Right-Left (RL) z z z z / \ / \ y y y y / \ \ / x x x x ↓ ↓ ↓ ↓ y y y y / \ / \ / \ / \ x z z x x z z x
PropertyAVLRed-Black
BalanceStrictly balanced (|bf| ≤ 1)Loosely balanced
Height~1.44 log n~2 log n
Rotations on insertUp to 2Up to 2
Rotations on deleteUp to O(log n)Up to 3
Used inDatabases, lookupsJava TreeMap, Linux CFS
TreeMap & TreeSet
// TreeMap — sorted key-value store val map = TreeMap<Int, String>() map[5] = "five"; map[2] = "two"; map[8] = "eight" map.firstKey() // 2 map.lastKey() // 8 map.higherKey(5) // 8 (strictly greater) map.lowerKey(5) // 2 (strictly less) map.ceilingKey(5) // 5 (≥) map.floorKey(5) // 5 (≤) map.subMap(2, 8) // {2=two, 5=five} [2, 8) map.headMap(5) // {2=two} [first, 5) map.tailMap(5) // {5=five, 8=eight} [5, last] // TreeSet — sorted set val set = TreeSet<Int>() set.addAll(listOf(5, 2, 8, 1)) set.first() // 1 set.last() // 8 set.higher(2) // 5 set.lower(5) // 2 set.ceiling(3) // 5 set.floor(3) // 2 set.subSet(2, 8) // [2, 5]
Iterative Traversals → DFS with explicit stack (see Stacks page)  |  BFS / Level-Order → queue-based traversal (see Queues page)  |  Flatten to Linked List → tree to linked list conversion (see Linked Lists page)
Max & Min Depth
O(n) time O(h) space
fun maxDepth(root: TreeNode?): Int { if (root == null) return 0 return maxOf(maxDepth(root.left), maxDepth(root.right)) + 1 } fun minDepth(root: TreeNode?): Int { if (root == null) return 0 val l = minDepth(root.left) val r = minDepth(root.right) if (l == 0 || r == 0) return l + r + 1 // single-child case return minOf(l, r) + 1 } fun minDepthBFS(root: TreeNode?): Int { if (root == null) return 0 val queue: Queue<TreeNode> = LinkedList() queue.add(root) var depth = 0 while (queue.isNotEmpty()) { depth++ repeat(queue.size) { val node = queue.poll() if (node.left == null && node.right == null) return depth node.left?.let { queue.add(it) } node.right?.let { queue.add(it) } } } return depth }
When to Use
maximum depthWhy: Classic bottom-up: max(left, right) + 1. Base case null → 0. Every tree height question uses this. minimum depthWhy: Shortest root-to-leaf path. Must handle single-child nodes (not leaves). BFS is more efficient — stops at first leaf. balanced tree checkWhy: Height at each node feeds into |left - right| ≤ 1 check. Combines depth with validation. count nodes at depthWhy: Depth calculation is the foundation. Level = depth + 1. Count by tracking depth during traversal.
▶ Visualize
Diameter of Binary Tree
O(n) time O(h) space
fun diameterOfBinaryTree(root: TreeNode?): Int { var maxDiam = 0 fun height(node: TreeNode?): Int { if (node == null) return 0 val l = height(node.left) val r = height(node.right) maxDiam = maxOf(maxDiam, l + r) return maxOf(l, r) + 1 } height(root) return maxDiam }
When to Use
diameter of binary treeWhy: Longest path between any two nodes. At each node: height(left) + height(right). Track global max — path may not go through root. longest path in treeWhy: Same as diameter. Bottom-up height calculation with global max tracking. farthest nodesWhy: Diameter = farthest pair. One DFS computes all heights and tracks max left+right sum.
▶ Visualize
Balanced & Symmetric
O(n) time O(h) space
fun isBalanced(root: TreeNode?): Boolean { fun check(node: TreeNode?): Int { if (node == null) return 0 val l = check(node.left); if (l == -1) return -1 val r = check(node.right); if (r == -1) return -1 if (Math.abs(l - r) > 1) return -1 return maxOf(l, r) + 1 } return check(root) != -1 } fun isSymmetric(root: TreeNode?): Boolean { fun isMirror(a: TreeNode?, b: TreeNode?): Boolean { if (a == null && b == null) return true if (a == null || b == null) return false return a.`val` == b.`val` && isMirror(a.left, b.right) && isMirror(a.right, b.left) } return isMirror(root?.left, root?.right) }
When to Use
check if balancedWhy: Return height or -1 (unbalanced). One DFS, O(n). Avoids the naive O(n²) of computing height separately at each node. symmetric / mirror treeWhy: Compare left subtree with mirror of right. Recursive: left.left↔right.right AND left.right↔right.left. check if completeWhy: BFS level-order: once a non-full node is seen, all subsequent nodes must be leaves.
▶ Visualize
Invert Binary Tree
O(n) time O(h) space
fun invertTree(root: TreeNode?): TreeNode? { if (root == null) return null val tmp = root.left root.left = invertTree(root.right) root.right = invertTree(tmp) return root } // Iterative BFS version fun invertTreeIterative(root: TreeNode?): TreeNode? { if (root == null) return null val queue: Queue<TreeNode> = LinkedList() queue.add(root) while (queue.isNotEmpty()) { val node = queue.poll() val tmp = node.left node.left = node.right node.right = tmp node.left?.let { queue.add(it) } node.right?.let { queue.add(it) } } return root }
When to Use
invert binary treeWhy: Swap left↔right at every node, recurse. The canonical "simple tree recursion" interview question. mirror a treeWhy: Identical to invert. Create mirror image for comparison or display purposes. flip equivalent treesWhy: Two trees are flip-equivalent if one can become the other by swapping children at some nodes. Recursive check with both swap/no-swap options.
▶ Visualize
Path Sum Problems
O(n) time O(h-n) space
// Path Sum I: root-to-leaf fun hasPathSum(root: TreeNode?, target: Int): Boolean { if (root == null) return false if (root.left == null && root.right == null) return root.`val` == target val rem = target - root.`val` return hasPathSum(root.left, rem) || hasPathSum(root.right, rem) } // Path Sum II: all root-to-leaf paths (backtracking) fun pathSum(root: TreeNode?, target: Int): List<List<Int>> { val result = mutableListOf<List<Int>>() fun dfs(node: TreeNode?, rem: Int, path: MutableList<Int>) { if (node == null) return path.add(node.`val`) if (node.left == null && node.right == null && rem == node.`val`) result.add(ArrayList(path)) dfs(node.left, rem - node.`val`, path) dfs(node.right, rem - node.`val`, path) path.removeAt(path.size - 1) // backtrack } dfs(root, target, mutableListOf()) return result } // Path Sum III: any-start/end (prefix sum + HashMap) fun pathSumIII(root: TreeNode?, target: Long): Int { val prefixMap = HashMap<Long, Int>() prefixMap[0L] = 1 var count = 0 fun dfs(node: TreeNode?, curSum: Long) { if (node == null) return val sum = curSum + node.`val` count += prefixMap.getOrDefault(sum - target, 0) prefixMap[sum] = prefixMap.getOrDefault(sum, 0) + 1 dfs(node.left, sum) dfs(node.right, sum) prefixMap[sum] = prefixMap[sum]!! - 1 // backtrack } dfs(root, 0L) return count }
When to Use
path sum (root to leaf)Why: Pass remaining target down. At leaf: check if remaining == 0. Classic top-down parameter passing. all paths with target sumWhy: Same as path sum but collect ALL paths. Backtrack: add node to path, recurse, remove after. path sum iii (any start/end)Why: Prefix sum + HashMap on tree paths. If currentSum - target exists in map → valid subpath. Backtrack map entry after subtree. binary tree max path sumWhy: Related pattern: at each node compute best path through it (left + right + node). Report one branch up to parent. sum root to leaf numbersWhy: Each root-to-leaf path forms a number. Pass accumulated value × 10 + node.val down. Sum at leaves.
▶ Visualize
Lowest Common Ancestor
O(n) / O(h) time O(h) space
// Binary tree LCA — O(n) fun lowestCommonAncestor( root: TreeNode?, p: TreeNode, q: TreeNode ): TreeNode? { if (root == null || root == p || root == q) return root val left = lowestCommonAncestor(root.left, p, q) val right = lowestCommonAncestor(root.right, p, q) if (left != null && right != null) return root return left ?: right } // BST LCA — O(h) fun lcaBST(root: TreeNode?, p: TreeNode, q: TreeNode): TreeNode? { var node = root while (node != null) { node = when { p.`val` < node.`val` && q.`val` < node.`val` -> node.left p.`val` > node.`val` && q.`val` > node.`val` -> node.right else -> return node // split point } } return null }
When to Use
lowest common ancestorWhy: If p in left subtree and q in right → current node is LCA. If both same side → LCA is deeper. O(n) for binary tree. lca in bstWhy: BST ordering: if both < node go left, both > go right, else split point = LCA. O(h) — much faster than general LCA. distance between two nodesWhy: Find LCA, then dist(p,q) = dist(root,p) + dist(root,q) - 2×dist(root,LCA). LCA is the key step. lca with parent pointersWhy: Like intersection of two linked lists: walk both pointers up, they meet at LCA. O(h) time, O(1) space.
▶ Visualize
Serialize & Deserialize
O(n) time O(n) space
class Codec { fun serialize(root: TreeNode?): String { val sb = StringBuilder() fun preorder(node: TreeNode?) { if (node == null) { sb.append("#,"); return } sb.append("${node.`val`},") preorder(node.left) preorder(node.right) } preorder(root) return sb.toString() } fun deserialize(data: String): TreeNode? { val queue: Queue<String> = LinkedList(data.split(",")) fun build(): TreeNode? { val token = queue.poll() ?: return null if (token == "#") return null val node = TreeNode(token.toInt()) node.left = build() node.right = build() return node } return build() } }
When to Use
serialize / deserialize treeWhy: Convert tree ↔ string for storage, transmission, or comparison. Preorder + null markers preserves structure completely. encode n-ary as binaryWhy: Serialize N-ary to string, deserialize as binary (left-child right-sibling). Same serialize/deserialize pattern. tree comparison via stringWhy: Serialize both trees, compare strings. Useful for subtree matching — is serialized(t) a substring of serialized(s)?
▶ Visualize
Construct from Traversals
O(n) time O(n) space
fun buildTreeFromPreIn( preorder: IntArray, inorder: IntArray ): TreeNode? { val inMap = HashMap<Int, Int>() inorder.forEachIndexed { i, v -> inMap[v] = i } var preIdx = 0 fun build(lo: Int, hi: Int): TreeNode? { if (lo > hi) return null val rootVal = preorder[preIdx++] val node = TreeNode(rootVal) val mid = inMap[rootVal]!! node.left = build(lo, mid - 1) node.right = build(mid + 1, hi) return node } return build(0, inorder.size - 1) }
When to Use
build from preorder + inorderWhy: Preorder[0] = root. Find root in inorder → everything left = left subtree, right = right subtree. Recurse. O(n) with HashMap. build from inorder + postorderWhy: Postorder[last] = root. Same split logic but process right subtree first (postorder visits right before root). build from preorder + postorderWhy: Only unique for full binary trees. Preorder[1] = left root, find it in postorder to determine left subtree size.
▶ Visualize
Right Side View & Zigzag
O(n) time O(w) space
fun rightSideView(root: TreeNode?): List<Int> { if (root == null) return emptyList() val result = mutableListOf<Int>() val queue: Queue<TreeNode> = LinkedList() queue.add(root) while (queue.isNotEmpty()) { val size = queue.size repeat(size) { i -> val node = queue.poll() if (i == size - 1) result.add(node.`val`) node.left?.let { queue.add(it) } node.right?.let { queue.add(it) } } } return result } fun zigzagLevelOrder(root: TreeNode?): List<List<Int>> { if (root == null) return emptyList() val result = mutableListOf<List<Int>>() val queue: Queue<TreeNode> = LinkedList() queue.add(root) var leftToRight = true while (queue.isNotEmpty()) { val level = LinkedList<Int>() repeat(queue.size) { val node = queue.poll() if (leftToRight) level.addLast(node.`val`) else level.addFirst(node.`val`) node.left?.let { queue.add(it) } node.right?.let { queue.add(it) } } result.add(level) leftToRight = !leftToRight } return result }
When to Use
right side viewWhy: BFS: last node in each level is the rightmost visible. Or DFS right-first: first node at each new depth is the answer. zigzag level-orderWhy: BFS level-by-level, reverse every other level. Queue.size snapshot separates levels cleanly. bottom-up level-orderWhy: Standard level-order then reverse the result list. Or collect levels and return reversed. largest value in each rowWhy: BFS level snapshot: track max per level. Single pass, no sorting needed.
▶ Visualize
Flatten to Linked List
O(n) time O(1) space
fun flatten(root: TreeNode?) { var cur = root while (cur != null) { if (cur.left != null) { // Find rightmost of left subtree var pred = cur.left!! while (pred.right != null) pred = pred.right!! // Connect to right subtree pred.right = cur.right // Move left to right cur.right = cur.left cur.left = null } cur = cur.right } }
When to Use
flatten to linked listWhy: Convert tree to right-pointer-only chain in preorder. Morris-style: find rightmost of left subtree, link to right subtree. O(1) space. tree to doubly linked listWhy: Inorder traversal linking prev↔current. Similar flatten but with bidirectional links. preorder without recursion/stackWhy: Flatten first, then iterate right pointers. Trades tree structure for simple iteration.
▶ Visualize
Kth Smallest & BST Iterator
O(h+k) time O(h) space
fun kthSmallest(root: TreeNode?, k: Int): Int { val stack = ArrayDeque<TreeNode>() var cur = root var count = 0 while (cur != null || stack.isNotEmpty()) { while (cur != null) { stack.addFirst(cur); cur = cur.left } cur = stack.removeFirst() if (++count == k) return cur.`val` cur = cur.right } return -1 } class BSTIterator(root: TreeNode?) { private val stack = ArrayDeque<TreeNode>() init { pushLeft(root) } private fun pushLeft(node: TreeNode?) { var n = node while (n != null) { stack.addFirst(n); n = n.left } } fun hasNext(): Boolean = stack.isNotEmpty() fun next(): Int { val node = stack.removeFirst() pushLeft(node.right) return node.`val` } }
When to Use
kth smallest in bstWhy: Inorder traversal of BST = sorted order. Stop at kth element. O(h + k) time. bst iteratorWhy: Controlled inorder using a stack: pushAllLeft, then pop+process+pushAllLeft(right). O(h) space, O(1) amortized next(). kth largest in bstWhy: Reverse inorder (right→root→left). Stop at kth element. Same technique, opposite direction. closest value in bstWhy: Binary search down the tree tracking closest so far. O(h) — no need for full traversal.
▶ Visualize
Max Path Sum & Good Nodes
O(n) time O(h) space
fun maxPathSum(root: TreeNode?): Int { var maxSum = Int.MIN_VALUE fun gain(node: TreeNode?): Int { if (node == null) return 0 val l = maxOf(0, gain(node.left)) // ignore negatives val r = maxOf(0, gain(node.right)) maxSum = maxOf(maxSum, l + r + node.`val`) // use both return maxOf(l, r) + node.`val` // contribute one } gain(root) return maxSum } fun goodNodes(root: TreeNode?): Int { fun dfs(node: TreeNode?, maxSoFar: Int): Int { if (node == null) return 0 val good = if (node.`val` >= maxSoFar) 1 else 0 val newMax = maxOf(maxSoFar, node.`val`) return good + dfs(node.left, newMax) + dfs(node.right, newMax) } return dfs(root, Int.MIN_VALUE) }
When to Use
binary tree max path sumWhy: At each node: pathSum = val + leftGain + rightGain (both branches). Report upward = val + max(left, right) (one branch). Track global max. count good nodesWhy: Pass maxSoFar top-down. Node is "good" if its value ≥ maxSoFar on the path from root. longest univalue pathWhy: Similar to diameter: at each node compute matching-value path through it. Track global max. Bottom-up return. house robber iiiWhy: DP on tree: each node returns (rob, skip). Rob = val + skip(left) + skip(right). Skip = max of children's rob/skip.
▶ Visualize
Same Tree, Subtree, Merge Trees
O(n) time O(h) space
fun isSameTree(p: TreeNode?, q: TreeNode?): Boolean { if (p == null && q == null) return true if (p == null || q == null) return false return p.`val` == q.`val` && isSameTree(p.left, q.left) && isSameTree(p.right, q.right) } fun isSubtree(root: TreeNode?, sub: TreeNode?): Boolean { if (root == null) return sub == null return isSameTree(root, sub) || isSubtree(root.left, sub) || isSubtree(root.right, sub) } fun mergeTrees(t1: TreeNode?, t2: TreeNode?): TreeNode? { if (t1 == null) return t2 if (t2 == null) return t1 t1.`val` += t2.`val` t1.left = mergeTrees(t1.left, t2.left) t1.right = mergeTrees(t1.right, t2.right) return t1 }
When to Use
same treeWhy: Recursively compare: both null → true. One null → false. Values equal AND both subtrees same → true. O(min(n,m)). subtree of another treeWhy: At every node of s, check if subtree rooted there equals t (using isSameTree). O(m×n) brute force, O(m+n) with serialization. merge two binary treesWhy: Overlapping nodes: sum values. Non-overlapping: take whichever exists. Natural recursive structure. leaf-similar treesWhy: Collect leaf sequences via DFS on both trees, compare the sequences. Uses same isSameTree building block.
▶ Visualize
Vertical Order & Sorted Array → BST
O(n log n) / O(n) time O(n) space
fun verticalOrder(root: TreeNode?): List<List<Int>> { if (root == null) return emptyList() val colMap = TreeMap<Int, MutableList<Int>>() val queue: Queue<Pair<TreeNode, Int>> = LinkedList() queue.add(root to 0) while (queue.isNotEmpty()) { val (node, col) = queue.poll() colMap.getOrPut(col) { mutableListOf() }.add(node.`val`) node.left?.let { queue.add(it to col - 1) } node.right?.let { queue.add(it to col + 1) } } return colMap.values.toList() } fun sortedArrayToBST(nums: IntArray): TreeNode? { fun build(lo: Int, hi: Int): TreeNode? { if (lo > hi) return null val mid = lo + (hi - lo) / 2 val node = TreeNode(nums[mid]) node.left = build(lo, mid - 1) node.right = build(mid + 1, hi) return node } return build(0, nums.size - 1) }
When to Use
vertical order traversalWhy: Assign column to each node (root=0, left=col-1, right=col+1). BFS + TreeMap groups by column. Sort by row within column. sorted array to balanced bstWhy: Middle element = root ensures balance. Recurse on left half and right half. O(n), produces minimum-height BST. sorted list to bstWhy: Same middle-as-root idea but use slow/fast pointers to find middle. Or inorder simulation with advancing pointer. boundary of binary treeWhy: Left boundary (top-down) + leaves (DFS) + right boundary (bottom-up). Combines vertical/level thinking.
▶ Visualize
🔠
Trie (Prefix Tree)
O(L) per op O(N·L) space
Trie for: apple, app, ape, bat, bar (root) / \ a b / \ p a / \ / \ p e* t* r* | l | e* (* = end of word) app* is also end of word
class TrieNode { val children = arrayOfNulls<TrieNode>(26) var isEnd = false } class Trie { private val root = TrieNode() fun insert(word: String) { var node = root for (c in word) { val i = c - 'a' if (node.children[i] == null) node.children[i] = TrieNode() node = node.children[i]!! } node.isEnd = true } fun search(word: String): Boolean { val node = findNode(word) return node?.isEnd == true } fun startsWith(prefix: String): Boolean = findNode(prefix) != null private fun findNode(s: String): TrieNode? { var node = root for (c in s) { node = node.children[c - 'a'] ?: return null } return node } fun wordsWithPrefix(prefix: String): List<String> { val result = mutableListOf<String>() val node = findNode(prefix) ?: return result fun collect(n: TrieNode, sb: StringBuilder) { if (n.isEnd) result.add(sb.toString()) for (i in 0..25) { if (n.children[i] != null) { sb.append('a' + i) collect(n.children[i]!!, sb) sb.deleteCharAt(sb.length - 1) } } } collect(node, StringBuilder(prefix)) return result } } // HashMap-based for flexible character sets class TrieMap { val children = HashMap<Char, TrieMap>() var isEnd = false }
When to Use
autocomplete / prefix searchWhy: startsWith(prefix) in O(L). Then DFS from prefix node to enumerate all completions. Foundation of search bars. word search ii (trie pruning)Why: Build trie from word list, DFS the board following trie edges. Prune branches not in trie — dramatically faster than checking each word. longest common prefixWhy: Insert all strings, walk trie until a node has >1 child or is end-of-word. Path so far = longest common prefix. spell checkerWhy: O(L) exact lookup + prefix search. Can also find words within edit distance by exploring adjacent trie branches. count distinct substringsWhy: Insert all suffixes of string into trie. Number of trie nodes = number of distinct substrings. maximum xorWhy: Binary trie (bit-by-bit). For each number, greedily take opposite bit at each level to maximize XOR.
▶ Visualize
Segment Tree
O(log n) query/update O(n) space
Segment Tree for sum: [1, 3, 5, 7, 9, 11] [36] (0..5) / \ [9] [27] (0..2)(3..5) / \ / \ [4] [5] [16] [11] (0..1)(2)(3..4)(5) / \ / \ [1][3] [7][9]
class SegmentTree(private val n: Int) { private val tree = IntArray(4 * n) fun build(arr: IntArray, node: Int = 1, s: Int = 0, e: Int = n - 1) { if (s == e) { tree[node] = arr[s]; return } val mid = (s + e) / 2 build(arr, 2 * node, s, mid) build(arr, 2 * node + 1, mid + 1, e) tree[node] = tree[2 * node] + tree[2 * node + 1] } fun query(l: Int, r: Int, node: Int = 1, s: Int = 0, e: Int = n - 1): Int { if (r < s || e < l) return 0 if (l <= s && e <= r) return tree[node] val mid = (s + e) / 2 return query(l, r, 2 * node, s, mid) + query(l, r, 2 * node + 1, mid + 1, e) } fun update(idx: Int, v: Int, node: Int = 1, s: Int = 0, e: Int = n - 1) { if (s == e) { tree[node] = v; return } val mid = (s + e) / 2 if (idx <= mid) update(idx, v, 2 * node, s, mid) else update(idx, v, 2 * node + 1, mid + 1, e) tree[node] = tree[2 * node] + tree[2 * node + 1] } }
When to Use
range sum queryWhy: Query any range [l,r] in O(log n). Supports sum, min, max, GCD — any associative operation. Build in O(n). range update + range queryWhy: Lazy propagation: defer updates to children until needed. Both range update and range query in O(log n). count elements in rangeWhy: Merge-sort tree variant or segment tree with sorted lists at nodes. Count elements in [lo, hi] within index range [l, r]. interval scheduling queriesWhy: Segment tree on timeline: update ranges (add/remove intervals), query point for active count.
▶ Visualize
Fenwick Tree (BIT)
O(log n) query/update O(n) space
class FenwickTree(private val n: Int) { private val tree = IntArray(n + 1) fun update(i: Int, delta: Int) { var idx = i + 1 while (idx <= n) { tree[idx] += delta idx += idx and (-idx) // add lowest set bit } } fun prefixSum(i: Int): Int { var idx = i + 1 var sum = 0 while (idx > 0) { sum += tree[idx] idx -= idx and (-idx) // remove lowest set bit } return sum } fun rangeSum(l: Int, r: Int): Int = prefixSum(r) - if (l > 0) prefixSum(l - 1) else 0 }
AspectFenwickSegment Tree
Code~15 lines~40 lines
SpaceO(n)O(4n)
SupportsSum onlySum, min, max, etc.
Range updateLimitedLazy propagation
When to Use
prefix sum with point updatesWhy: O(log n) update + O(log n) prefix sum query. Simpler and faster constant than segment tree for sum-only problems. count inversionsWhy: Process elements right-to-left (or by value), BIT tracks how many smaller elements seen so far. O(n log n). range sum with updatesWhy: rangeSum(l, r) = prefixSum(r) - prefixSum(l-1). Point update O(log n). Much simpler than segment tree. count smaller after selfWhy: Process right-to-left, BIT indexed by value. Query prefix sum up to current value = count of smaller elements to the right.
▶ Visualize
Tree Pitfalls
// ✗ Height vs depth confusion // Height = edges from node to deepest leaf (bottom-up) // Depth = edges from root to node (top-down) // ✗ Forgetting null root check fun bad(root: TreeNode): Int = root.`val` // NPE if null! fun good(root: TreeNode?): Int = root?.`val` ?: 0 // ✗ Wrong BST validation (only checking parent-child) // Must check entire subtree constraint with bounds // ✗ Integer overflow in BST validation // Use Long.MIN_VALUE / Long.MAX_VALUE for bounds // ✗ Forgetting to backtrack in path problems path.add(node.`val`) dfs(node.left, path) dfs(node.right, path) path.removeAt(path.size - 1) // ← don't forget! // ✗ === vs == for node identity // Use === for reference equality (is it the same node?) // Use == for value equality // ✗ Stack overflow on skewed trees // Use iterative traversal for trees that could be deep // ✗ Modifying tree when you shouldn't // Some problems expect the original tree unchanged // ✗ Assuming inorder = sorted for non-BST trees // Inorder is only sorted for Binary Search Trees
Tree Operations Complexity
OperationBalanced BSTUnbalanced BSTNotes
SearchO(log n)O(n)Binary search on tree
InsertO(log n)O(n)Find position + insert
DeleteO(log n)O(n)Find + reconnect
Min / MaxO(log n)O(n)Follow left / right
Inorder successorO(log n)O(n)Next in sorted order
TraversalTimeSpaceNotes
DFS (recursive)O(n)O(h)h = log n balanced, n skewed
DFS (iterative)O(n)O(h)Explicit stack
BFSO(n)O(w)w = max width ≤ n/2
MorrisO(n)O(1)Modifies tree temporarily
Algorithm Complexities
AlgorithmTimeSpace
Max / Min DepthO(n)O(h)
DiameterO(n)O(h)
Balanced / SymmetricO(n)O(h)
Invert TreeO(n)O(h)
Path Sum I, IIO(n)O(h)
Path Sum IIIO(n)O(n)
LCA (binary tree)O(n)O(h)
LCA (BST)O(h)O(1)
Serialize / DeserializeO(n)O(n)
Construct from traversalsO(n)O(n)
Right Side View / ZigzagO(n)O(w)
Flatten to Linked ListO(n)O(1)
Kth Smallest / BST IteratorO(h+k)O(h)
Max Path SumO(n)O(h)
Same / Subtree / MergeO(n)O(h)
Vertical OrderO(n log n)O(n)
Sorted Array → BSTO(n)O(log n)
Trie insert / searchO(L)O(N·L)
Segment Tree query/updateO(log n)O(n)
Fenwick Tree query/updateO(log n)O(n)
Which Pattern?
Problem SignalPattern
Depth / height / balancedDFS return value (bottom-up)
Path sum / constraints on pathDFS with parameters (top-down)
BST sorted order / kth elementInorder traversal
Level query / shortest distanceBFS level-by-level
Serialize / copy / reconstructPreorder traversal
Delete / compute heightsPostorder traversal
Prefix matching / autocompleteTrie
Range sum / min / max querySegment tree or Fenwick tree
Compare two treesTwo-node parallel DFS
Any-path sumPrefix sum on paths (HashMap)
BST search / insert / deleteBST binary search (go left / right)
Deep / skewed treeIterative traversal (avoid stack overflow)
🎯
Seven Essential Tree Patterns
1. DFS with return value — height, balanced, diameter 2. DFS with parameters — path sum, good nodes, BST validate 3. BFS level-by-level — level order, right view, zigzag 4. BST binary search — search, insert, delete, LCA 5. Build from traversals — preorder+inorder, serialize 6. Two-node comparison — same tree, symmetric, merge 7. Prefix sum on paths — path sum III, HashMap tracking