| Term | Definition |
|---|---|
| Root | Topmost node — has no parent |
| Parent | Node directly above another in the hierarchy |
| Child | Node directly below — each node may have zero or more children |
| Leaf | Node with no children (external node) |
| Internal node | Node with at least one child |
| Edge | Link between a parent and a child |
| Depth | Number of edges from root to node — root has depth 0 |
| Height | Longest path from node down to a leaf — leaf has height 0 |
| Level | Set of all nodes at the same depth |
| Subtree | A node and all its descendants |
A tree with n nodes always has exactly n - 1 edges.
A binary tree is a tree in which every node has at most two children, conventionally labelled left and right.
d: 2dh: 2h+1 - 1n nodes: floor(log2(n))n nodes: n + 1n internal nodes has n + 1 leavesclass TreeNode:
def __init__(self, val=0,
left=None,
right=None):
self.val = val
self.left = left
self.right = right
The recursive structure of TreeNode mirrors the recursive nature of every binary tree algorithm.
The two-child constraint yields elegant recursive decomposition: every problem on a binary tree reduces to solving left subtree + right subtree + combining at the root.
Every node has 0 or 2 children — no node has exactly one child.
i internal nodes: i + 1 leaves, 2i + 1 totalL leaves: 2L - 1 total nodes 1
/ \
2 3
/ \
4 5
All levels filled except possibly the last, which is filled left to right.
floor(log2(n)) — guarantees O(log n)i has children at 2i+1 and 2i+2 1
/ \
2 3
/ \ /
4 5 6
Complete trees give the most compact shape for a given number of nodes — this is why heaps use them.
Every internal node has 2 children. All leaves at the same depth.
2h+1 - 12h 1
/ \
2 3
/ \ / \
4 5 6 7
Height difference between left and right subtrees of every node is at most 1.
O(log n) height 4
/ \
2 6
/ / \
1 5 7
Every internal node has exactly one child — effectively a linked list.
n - 1O(n) 1
\
2
\
3
\
4
Visit the left subtree, then the current node, then the right subtree. For a BST, inorder produces sorted output.
def inorder(root):
if root is None:
return
inorder(root.left)
visit(root.val)
inorder(root.right)
Inorder: 1, 2, 3, 4, 5, 6, 7 (sorted!)
Time: O(n). Space: O(h) for the recursion stack.
def preorder(root):
if root is None:
return
visit(root.val)
preorder(root.left)
preorder(root.right)
def postorder(root):
if root is None:
return
postorder(root.left)
postorder(root.right)
visit(root.val)
Same tree: Preorder: 4, 2, 1, 3, 6, 5, 7 | Postorder: 1, 3, 2, 5, 7, 6, 4
Recursion uses O(h) stack space implicitly. For very deep trees, this risks stack overflow. Iterative versions use an explicit stack.
def inorder_iterative(root):
stack, current = [], root
while stack or current:
while current:
stack.append(current)
current = current.left
current = stack.pop()
visit(current.val)
current = current.right
def preorder_iterative(root):
if not root:
return
stack = [root]
while stack:
node = stack.pop()
visit(node.val)
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
Trickiest of the three. Two approaches: (1) use two stacks, or (2) reverse a modified preorder (Root → Right → Left) to get (Left → Right → Root). Both are O(n) time, O(h) space.
Visit all nodes at depth d before depth d + 1. Uses a queue instead of a stack.
from collections import deque
def level_order(root):
if not root:
return []
result, queue = [], deque([root])
while queue:
level_size = len(queue)
level = []
for _ in range(level_size):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return result
Output: [[4], [2, 6], [1, 3, 5, 7]]
Time: O(n). Space: O(w) where w is max width.
O(1) space, no stack, no recursion. Achieves O(n) time by temporarily modifying the tree using threaded pointers.
def morris_inorder(root):
current = root
while current:
if current.left is None:
visit(current.val)
current = current.right
else:
# Find inorder predecessor
pred = current.left
while (pred.right and
pred.right != current):
pred = pred.right
if pred.right is None:
# Create thread
pred.right = current
current = current.left
else:
# Remove thread, visit
pred.right = None
visit(current.val)
current = current.right
1. No left child → visit, move right
2. Left child exists → find inorder predecessor (rightmost in left subtree)
3. Predecessor's right is NULL → create temporary thread back to current, go left
4. Predecessor's right points to current → thread exists, remove it, visit current, go right
The tree is restored to its original shape. Each edge is traversed at most twice.
| Pair | Unique tree? |
|---|---|
| Inorder + Preorder | Yes |
| Inorder + Postorder | Yes |
| Inorder + Level-order | Yes |
| Preorder + Postorder | Only for full binary trees |
Inorder is essential — it tells you which nodes belong to the left vs right subtree.
def build_tree(preorder, inorder):
if not inorder:
return None
root_val = preorder.pop(0)
root = TreeNode(root_val)
mid = inorder.index(root_val)
root.left = build_tree(
preorder, inorder[:mid])
root.right = build_tree(
preorder, inorder[mid+1:])
return root
Use a hash map for O(1) index lookup and a pointer into preorder instead of pop(0). Reduces time from O(n2) to O(n).
The LCA of two nodes p and q is the deepest node that is an ancestor of both.
def lca(root, p, q):
if (root is None or
root == p or root == q):
return root
left = lca(root.left, p, q)
right = lca(root.right, p, q)
if left and right:
return root # split point
return left or right # same side
Time: O(n). Space: O(h) recursion stack.
If the current node is p or q, return it upward.
Recurse left and right. If both sides return non-NULL, current node is the LCA — p and q are in different subtrees.
If only one side returns non-NULL, the LCA is deeper in that subtree.
If both values < root, go left. If both > root, go right. Otherwise root is the LCA. Time: O(h).
The diameter is the longest path between any two nodes, measured in edges. The path may or may not pass through the root.
def diameter(root):
max_d = 0
def height(node):
nonlocal max_d
if node is None:
return -1
lh = height(node.left)
rh = height(node.right)
max_d = max(max_d, lh + rh + 2)
return max(lh, rh) + 1
height(root)
return max_d
At each node, the longest path through that node is left_height + right_height + 2. The diameter is the maximum across all nodes.
Compute a local answer at each node while returning info upward. This single-pass DFS pattern applies to many tree problems.
def serialise(root):
if root is None:
return "# "
return (str(root.val) + " "
+ serialise(root.left)
+ serialise(root.right))
def deserialise(data):
tokens = iter(data.split())
def build():
val = next(tokens)
if val == "#":
return None
node = TreeNode(int(val))
node.left = build()
node.right = build()
return node
return build()
1
/ \
2 3
/ \
4 5
Serialised: "1 2 # # 3 4 # # 5 # #"
| Method | Notes |
|---|---|
| Level-order | NULL markers per level; used by LeetCode |
| Inorder + preorder | No NULLs needed but two sequences |
| Parenthesised | 1(2)(3(4)(5)) — human readable |
Serialisation is O(n) time and space regardless of approach. Essential for distributed systems, caching, and persistence.
In a binary tree with n nodes, there are n + 1 NULL pointers (wasted space). Threaded binary trees repurpose these NULLs to point to inorder predecessors/successors.
| Type | Description |
|---|---|
| Single-threaded | Right NULLs → inorder successor |
| Double-threaded | Left NULLs → predecessor, right NULLs → successor |
class ThreadedNode:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
self.left_thread = False
self.right_thread = False
O(1) spaceO(1) amortisedThreaded trees trade insertion/deletion complexity for traversal efficiency. Rarely used in practice but conceptually important — they inspired Morris traversal.
A binary tree representing an arithmetic expression. Leaves are operands, internal nodes are operators.
| Traversal | Notation | Output |
|---|---|---|
| Inorder | Infix | 3+4*5-2 (needs parens) |
| Preorder | Prefix (Polish) | * + 3 4 - 5 2 |
| Postorder | Postfix (RPN) | 3 4 + 5 2 - * |
def build_expr_tree(postfix):
stack = []
ops = {'+', '-', '*', '/'}
for token in postfix:
node = TreeNode(token)
if token in ops:
node.right = stack.pop()
node.left = stack.pop()
stack.append(node)
return stack[0]
File systems are naturally tree-structured. Each directory is an internal node; files are leaves.
/
├── home/
│ ├── alice/
│ │ ├── docs/
│ │ │ └── report.pdf
│ │ └── .bashrc
│ └── bob/
│ └── code/
│ └── main.py
├── etc/
│ └── config.yaml
└── var/
└── log/
└── syslog
| Operation | Tree algorithm |
|---|---|
ls -R | Preorder DFS |
du -s | Postorder DFS — children before parent |
find | BFS or DFS |
rm -rf | Postorder — delete children first |
| Path resolution | Root-to-leaf traversal |
Every cd, ls, and mkdir is a tree operation. Understanding tree traversals helps you reason about file system performance.
Every web page is a tree. The browser parses HTML into a tree of nodes.
document
└── html
├── head
│ ├── title
│ └── link
└── body
├── div#header
│ └── h1
└── div#content
├── p
└── ul
├── li
└── li
getElementById() is a tree searchEach internal node is a yes/no question. Each leaf is a classification.
Is temp > 30?
/ \
Yes No
Is humid? Is windy?
/ \ / \
Yes No Yes No
Stay Beach Stay Park
inside inside
| Operation | Average | Worst (unbalanced) | Balanced |
|---|---|---|---|
| Search | O(log n) | O(n) | O(log n) |
| Insert | O(log n) | O(n) | O(log n) |
| Delete | O(log n) | O(n) | O(log n) |
| Traversal (any) | O(n) | O(n) | O(n) |
| Find min/max | O(log n) | O(n) | O(log n) |
| LCA | O(n) | O(n) | O(log n) with parent ptrs |
| Method | Space |
|---|---|
| Recursive DFS | O(h) — recursion stack |
| Iterative DFS | O(h) — explicit stack |
| BFS (level-order) | O(w) — queue width, up to O(n) |
| Morris traversal | O(1) — no extra space |
Always state whether the tree is balanced. It changes the answer from O(n) to O(log n) for most operations.
O(log n), degenerate gives O(n)O(1) space via temporary threads| Source | Description |
|---|---|
| CLRS | Introduction to Algorithms — Ch. 12 (BSTs), Ch. 13 (Red-Black Trees) |
| Skiena | The Algorithm Design Manual — tree traversal and applications |
| LeetCode | Tree tag — 200+ problems, easy to hard |
| Knuth | TAOCP Vol. 1 — fundamental tree algorithms, threaded trees |
| Sedgewick | Algorithms — BSTs, balanced trees, applications |
Inorder Preorder Postorder BFS Morris LCA Serialise