Skip to content

Trees and Graphs

Connections and hierarchies: Trees are hierarchies (file systems, organisational charts), graphs are networks (roads, social connections). Trees are special graphs — connected, acyclic, with a clear parent-child relationship. Understanding both is essential for modelling real-world relationships.

Why it matters: Trees and graphs appear everywhere — DOM trees in web browsers, dependency graphs in package managers, social networks, and GPS navigation. Knowing the right traversal and algorithm for each structure is fundamental.

The key insight: DFS uses a stack (or recursion) and explores depth-first — good for detecting cycles and topological sorting. BFS uses a queue and explores breadth-first — good for shortest paths in unweighted graphs.

A tree is a connected, acyclic, undirected graph. In computer science, trees are rooted and Directed (parent to child).

TermDefinition
RootThe topmost node, with no parent
LeafA node with no children
DepthDistance from the root to a node (root has depth 0)
HeightDistance from a node to its deepest descendant (leaf has height 0)
LevelAll nodes at the same depth
SubtreeA node and all its descendants
DegreeNumber of children of a node
PathSequence of nodes from one node to another

A binary tree is a tree where each node has at most two children: left and right.

TypePropertyNodes in tree of height hh
FullEvery node has 0 or 2 children2h+12h + 1 (odd)
CompleteAll levels filled except possibly the last, which is filled left to right2h2^h to 2h+112^{h+1} - 1
PerfectAll internal nodes have 2 children, all leaves at same depth2h+112^{h+1} - 1
BalancedHeight is O(logn)O(\log n)Varies
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
graph TD
    R[1] --> L[2]
    R --> RI[3]
    L --> LL[4]
    L --> LR[5]
    RI --> RL[6]
    RI --> RR[7]

    style R fill:#e74c3c,color:#fff
    style L fill:#e67e22,color:#fff
    style RI fill:#e67e22,color:#fff
    style LL fill:#3498db,color:#fff
    style LR fill:#3498db,color:#fff
    style RL fill:#3498db,color:#fff
    style RR fill:#3498db,color:#fff

Inorder (Left, Root, Right): 4, 2, 5, 1, 6, 3, 7

def inorder(root):
"""Left -> Root -> Right. Yields sorted order for BST."""
if root is None:
return
inorder(root.left)
print(root.val, end=" ')
inorder(root.right)

Preorder (Root, Left, Right): 1, 2, 4, 5, 3, 6, 7

def preorder(root):
"""Root -> Left -> Right. Used for tree serialisation."""
if root is None:
return
print(root.val, end=' ')
preorder(root.left)
preorder(root.right)

Postorder (Left, Right, Root): 4, 5, 2, 6, 7, 3, 1

def postorder(root):
"""Left -> Right -> Root. Used for tree deletion, expression evaluation."""
if root is None:
return
postorder(root.left)
postorder(root.right)
print(root.val, end=' ')

Level-order (BFS): 1, 2, 3, 4, 5, 6, 7

from collections import deque
def level_order(root):
"""BFS traversal by level."""
if root is None:
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
def inorder_iterative(root):
"""Iterative inorder using explicit stack. O(n) time, O(h) space."""
result = []
stack = []
current = root
while current or stack:
while current:
stack.append(current)
current = current.left
current = stack.pop()
result.append(current.val)
current = current.right
return result
def preorder_iterative(root):
"""Iterative preorder. O(n) time, O(h) space."""
if not root:
return []
result = []
stack = [root]
while stack:
node = stack.pop()
result.append(node.val)
# Push right first so left is processed first
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return result
def max_depth(root):
"""Maximum depth of a binary tree. O(n) time, O(h) space."""
if root is None:
return 0
return 1 + max(max_depth(root.left), max_depth(root.right))
def is_balanced(root):
"""
Check if a binary tree is height-balanced.
A tree is balanced if the heights of the two subtrees of every node differ by at most 1.
Time: O(n), Space: O(h)
"""
def check(node):
if node is None:
return 0, True
left_height, left_balanced = check(node.left)
right_height, right_balanced = check(node.right)
height = 1 + max(left_height, right_height)
balanced = (left_balanced and right_balanced and
abs(left_height - right_height) <= 1)
return height, balanced
_, balanced = check(root)
return balanced
def is_same_tree(p, q):
"""Check if two binary trees are identical. O(n) time."""
if p is None and q is None:
return True
if p is None or q is None:
return False
return (p.val == q.val and
is_same_tree(p.left, q.left) and
is_same_tree(p.right, q.right))

A BST is a binary tree where for every node: all values in the left subtree are less than the node’s Value, and all values in the right subtree are greater.

def bst_search(root, val):
"""Search for a value in a BST. O(h) time where h = height."""
current = root
while current:
if val == current.val:
return current
elif val < current.val:
current = current.left
else:
current = current.right
return None
def bst_insert(root, val):
"""Insert a value into a BST. O(h) time."""
if root is None:
return TreeNode(val)
if val < root.val:
root.left = bst_insert(root.left, val)
elif val > root.val:
root.right = bst_insert(root.right, val)
return root
def bst_delete(root, val):
"""
Delete a value from a BST.
O(h) time. Three cases:
1. Leaf: just remove
2. One child: replace with child
3. Two children: replace with in-order successor, delete successor
"""
if root is None:
return None
if val < root.val:
root.left = bst_delete(root.left, val)
elif val > root.val:
root.right = bst_delete(root.right, val)
else:
# Found the node to delete
if root.left is None:
return root.right
if root.right is None:
return root.left
# Two children: find in-order successor (smallest in right subtree)
successor = root.right
while successor.left:
successor = successor.left
root.val = successor.val
root.right = bst_delete(root.right, successor.val)
return root
OperationAverage (balanced)Worst (degenerate)
SearchO(logn)O(\log n)O(n)O(n)
InsertO(logn)O(\log n)O(n)O(n)
DeleteO(logn)O(\log n)O(n)O(n)
Min/MaxO(logn)O(\log n)O(n)O(n)
In-order traversalO(n)O(n)O(n)O(n)
def is_valid_bst(root):
"""
Check if a binary tree is a valid BST.
Time: O(n), Space: O(h)
"""
def validate(node, min_val, max_val):
if node is None:
return True
if node.val <= min_val or node.val >= max_val:
return False
return (validate(node.left, min_val, node.val) and
validate(node.right, node.val, max_val))
return validate(root, float('-inf'), float('inf'))