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).
Term Definition Root The topmost node, with no parent Leaf A node with no children Depth Distance from the root to a node (root has depth 0) Height Distance from a node to its deepest descendant (leaf has height 0) Level All nodes at the same depth Subtree A node and all its descendants Degree Number of children of a node Path Sequence of nodes from one node to another
A binary tree is a tree where each node has at most two children: left and right.
Type Property Nodes in tree of height h h h Full Every node has 0 or 2 children 2 h + 1 2h + 1 2 h + 1 (odd)Complete All levels filled except possibly the last, which is filled left to right 2 h 2^h 2 h to 2 h + 1 − 1 2^{h+1} - 1 2 h + 1 − 1 Perfect All internal nodes have 2 children, all leaves at same depth 2 h + 1 − 1 2^{h+1} - 1 2 h + 1 − 1 Balanced Height is O ( log n ) O(\log n) O ( log n ) Varies
def __init__ ( self , val = 0 , left = None , right = None ):
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
"""Left -> Root -> Right. Yields sorted order for BST."""
Preorder (Root, Left, Right): 1, 2, 4, 5, 3, 6, 7
"""Root -> Left -> Right. Used for tree serialisation."""
Postorder (Left, Right, Root): 4, 5, 2, 6, 7, 3, 1
"""Left -> Right -> Root. Used for tree deletion, expression evaluation."""
Level-order (BFS): 1, 2, 3, 4, 5, 6, 7
from collections import deque
"""BFS traversal by level."""
for _ in range (level_size):
def inorder_iterative ( root ):
"""Iterative inorder using explicit stack. O(n) time, O(h) space."""
result.append(current.val)
def preorder_iterative ( root ):
"""Iterative preorder. O(n) time, O(h) space."""
# Push right first so left is processed first
"""Maximum depth of a binary tree. O(n) time, O(h) space."""
return 1 + max (max_depth(root.left), max_depth(root.right))
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.
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 )
_, balanced = check(root)
"""Check if two binary trees are identical. O(n) time."""
if p is None and q is None :
if p is None or q is None :
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."""
def bst_insert ( root , val ):
"""Insert a value into a BST. O(h) time."""
root.left = bst_insert(root.left, val)
root.right = bst_insert(root.right, val)
def bst_delete ( root , val ):
Delete a value from a BST.
2. One child: replace with child
3. Two children: replace with in-order successor, delete successor
root.left = bst_delete(root.left, val)
root.right = bst_delete(root.right, val)
# Found the node to delete
# Two children: find in-order successor (smallest in right subtree)
successor = successor.left
root.right = bst_delete(root.right, successor.val)
Operation Average (balanced) Worst (degenerate) Search O ( log n ) O(\log n) O ( log n ) O ( n ) O(n) O ( n ) Insert O ( log n ) O(\log n) O ( log n ) O ( n ) O(n) O ( n ) Delete O ( log n ) O(\log n) O ( log n ) O ( n ) O(n) O ( n ) Min/Max O ( log n ) O(\log n) O ( log n ) O ( n ) O(n) O ( n ) In-order traversal O ( n ) O(n) O ( n ) O ( n ) O(n) O ( n )
Check if a binary tree is a valid BST.
def validate ( node , min_val , max_val ):
if node.val <= min_val or node.val >= max_val:
return (validate(node.left, min_val, node.val) and
validate(node.right, node.val, max_val))
return validate(root, float ( ' -inf ' ), float ( ' inf ' ))
Insufficient — the BST property requires that **all** values in the left subtree are less than `node.val`Not just the immediate left child. A node with value 5, left child with value 1, and Left-left grandchild with value 6 fails the BST property but passes the naive check.An AVL tree is a self-balancing BST where the heights of the two child subtrees of any node differ By at most 1. Named after Adelson-Velsky and Landis (1962).
The balance factor of a node is: \mathrm{bf(node) = \mathrm{height(\mathrm{left) - \mathrm{height(\mathrm{right) .
Valid balance factors: { − 1 , 0 , 1 } \{-1, 0, 1\} { − 1 , 0 , 1 } . If the balance factor is outside this range, rotations are Needed to restore balance.
Right rotation at node y.
# Update heights (if tracking)
Imbalance Case Condition Fix Left-Left bf(node) = 2, bf(left) = 1 Right rotate at node Right-Right bf(node) = -2, bf(right) = -1 Left rotate at node Left-Right bf(node) = 2, bf(left) = -1 Left rotate at left, then right rotate at node Right-Left bf(node) = -2, bf(right) = 1 Right rotate at right, then left rotate at node
Operation Time Search O ( log n ) O(\log n) O ( log n ) Insert O ( log n ) O(\log n) O ( log n ) Delete O ( log n ) O(\log n) O ( log n ) Rotations per operation At most 2
AVL trees guarantee O ( log n ) O(\log n) O ( log n ) height, which is at most 1.44 log 2 ( n + 2 ) − 0.328 1.44 \log_2(n+2) - 0.328 1.44 log 2 ( n + 2 ) − 0.328 . In practice, AVL trees are taller and require more rotations than red-black trees, but provide faster lookups Because the tree is more strictly balanced.
A red-black tree is a self-balancing BST with the following properties:
Every node is either red or black The root is black Every leaf (NIL) is black If a node is red, both its children are black (no two consecutive reds) Every path from a node to its descendant NIL nodes contains the same number of black nodes Red-black trees guarantee O ( log n ) O(\log n) O ( log n ) height — specifically, the height is at most 2 log 2 ( n + 1 ) 2 \log_2(n+1) 2 log 2 ( n + 1 ) . This is less strict than AVL trees, meaning red-black trees are shorter but may have Slower individual lookups.
Red-black trees are used in the Linux kernel (for CFS scheduler, mm memory management), Java’s TreeMap/TreeSetC++ std::map/std::setAnd many other standard library implementations.
Insertions and deletions are frequent (schedulers, event queues). In practice, the difference is Small for most workloads.B-trees are balanced search trees designed for systems that read and write large blocks of data (disk pages, cache lines). Unlike binary trees, each node in a B-tree can have multiple children.
A B-tree of order m m m (minimum degree) satisfies:
Every node has at most 2 m − 1 2m - 1 2 m − 1 keys and 2 m 2m 2 m children Every non-root node has at least m − 1 m - 1 m − 1 keys and m m m children The root has at least 1 key All leaves appear at the same depth A database index stored as a binary tree with 10 million rows has height ≈ 24 \approx 24 ≈ 24 . Each node Access is a disk seek (~10ms), so a lookup costs ~240ms. A B-tree with order m = 100 m = 100 m = 100 (fitting in a 4KB disk page) has height ≈ 3 \approx 3 ≈ 3 So a lookup costs ~30ms. This 8x improvement is why every Major database uses B-tree variants (B+ trees) for indexing.
Structure Height for n = 10 7 n = 10^7 n = 1 0 7 Disk seeks Binary tree ~24 ~24 AVL tree ~24 ~24 B-tree (order 100) ~3 ~3 B-tree (order 400) ~2 ~2
A heap is a complete binary tree with the heap property: in a max-heap, every node is greater than Or equal to its children; in a min-heap, every node is less than or equal to its children.
In-place heapsort. O(n log n) time, O(1) space.
for i in range (n // 2 - 1 , - 1 , - 1 ):
# Extract elements one by one: O(n log n)
for i in range (n - 1 , 0 , - 1 ):
arr[ 0 ], arr[i] = arr[i], arr[ 0 ] # move max to end
return arr # sorted in ascending order
def _sift_down ( arr , n , i ):
"""Maintain max-heap property starting from index i."""
if left < n and arr[left] > arr[largest]:
if right < n and arr[right] > arr[largest]:
arr[i], arr[largest] = arr[largest], arr[i]
_sift_down(arr, n, largest)
Application Heap Type Key Idea Priority queue Min-heap or max-heap Extract min/max efficiently Heapsort Max-heap (ascending) Repeatedly extract max Median maintenance Two heaps (min + max) Balance heaps for median K largest elements Min-heap of size k k k Keep smallest of the top k k k Dijkstra’s algorithm Min-heap Always process closest vertex Huffman coding Min-heap Build optimal prefix code
A trie is a tree where each node represents a character in a prefix. Words are stored as paths from The root. The root represents the empty string.
Trie (prefix tree) for string operations.
insert: O(k), search: O(k), starts_with: O(k)
where k = length of the string
Space: O(total characters in all inserted strings)
if c not in node.children:
node.children[c] = TrieNode()
if c not in node.children:
def starts_with ( self , prefix ):
if c not in node.children:
"""Delete a word from the trie. O(k)."""
def _delete ( node , word , depth ):
return False # word not in trie
return len (node.children) == 0
if c not in node.children:
should_delete = _delete(node.children[c], word, depth + 1 )
return len (node.children) == 0 and not node.is_end
_delete( self .root, word, 0 )
Operation Trie Hash Set Insert O ( k ) O(k) O ( k ) O ( k ) O(k) O ( k ) Exact search O ( k ) O(k) O ( k ) O ( k ) O(k) O ( k ) averagePrefix search O ( k ) O(k) O ( k ) O ( n ⋅ k ) O(n \cdot k) O ( n ⋅ k ) Space O(\mathrm{total chars) O ( n ⋅ k ) O(n \cdot k) O ( n ⋅ k ) Min string prefix O ( k ) O(k) O ( k ) Not supported Longest common prefix O ( k ) O(k) O ( k ) Not directly supported
Tries are the right choice when you need prefix-based operations: autocomplete, spell checking, IP Routing (longest prefix match), and word games.
A graph G = ( V , E ) G = (V, E) G = ( V , E ) consists of vertices V V V and edges E E E .
Each vertex stores a list (or set) of its neighbours.
Graph using adjacency list.
self .adj = {} # vertex -> list of (neighbour, weight)
def add_edge ( self , u , v , weight = 1 , directed = False ):
self .adj[u].append((v, weight))
self .adj[v].append((u, weight))
return self .adj.get(v, [])
A V × V V \times V V × V matrix where matrix[u][v] represents the edge weight (or 0/True/False for Unweighted/unweighted).
Graph using adjacency matrix.
self .matrix = [[ 0 ] * n for _ in range (n)]
def add_edge ( self , u , v , weight = 1 , directed = False ):
self .matrix[u][v] = weight
self .matrix[v][u] = weight
Representation Space Check edge u u u -v v v Iterate neighbours Sparse graph Adjacency list O ( V + E ) O(V + E) O ( V + E ) O(\mathrm{degree(u)) O(\mathrm{degree(u)) Efficient Adjacency matrix O ( V 2 ) O(V^2) O ( V 2 ) O ( 1 ) O(1) O ( 1 ) O ( V ) O(V) O ( V ) Wasteful
Networks). Use adjacency matrices for dense graphs (fully connected or nearly so) or when you need $O(1)$ edge existence checks.Explore neighbours before going deeper. Uses a queue. Finds shortest path in unweighted graphs.
from collections import deque
Time: O(V + E), Space: O(V)
for neighbour, _ in graph.neighbours(vertex):
if neighbour not in visited:
def bfs_shortest_path ( graph , start , end ):
Shortest path in unweighted graph using BFS.
Time: O(V + E), Space: O(V)
queue = deque([(start, [start])])
vertex, path = queue.popleft()
for neighbour, _ in graph.neighbours(vertex):
return path + [neighbour]
if neighbour not in visited:
queue.append((neighbour, path + [neighbour]))
return None # no path exists
graph TD
A[Start: A] --> B[B]
A --> C[C]
B --> D[D]
B --> E[E]
C --> F[F]
C --> G[G]
style A fill:#e74c3c,color:#fff
style B fill:#e67e22,color:#fff
style C fill:#e67e22,color:#fff
style D fill:#3498db,color:#fff
style E fill:#3498db,color:#fff
style F fill:#3498db,color:#fff
style G fill:#3498db,color:#fff BFS order: A, B, C, D, E, F, G
Explore as deep as possible before backtracking. Uses a stack (or recursion).
def dfs_recursive ( graph , start , visited = None ):
DFS from start vertex (recursive).
Time: O(V + E), Space: O(V)
for neighbour, _ in graph.neighbours(start):
if neighbour not in visited:
result.extend(dfs_recursive(graph, neighbour, visited))
def dfs_iterative ( graph , start ):
DFS from start vertex (iterative with explicit stack).
Time: O(V + E), Space: O(V)
# Push neighbours in reverse order to match recursive order
for neighbour, _ in reversed (graph.neighbours(vertex)):
if neighbour not in visited:
Property BFS DFS Data structure Queue Stack / recursion Space O ( V ) O(V) O ( V ) (queue)O ( V ) O(V) O ( V ) (stack)Shortest path Yes (unweighted) No Memory for deep graphs Uses more (wide frontier) Uses less (one path) Topological sort With Kahn’s algorithm With post-order Cycle detection Yes Yes Connected components Yes Yes
A topological ordering of a DAG is a linear ordering of vertices such that for every directed edge u → v u \to v u → v , u u u comes before v v v in the ordering.
def topological_sort_kahn ( graph ):
Kahn's algorithm for topological sort using BFS.
Time: O(V + E), Space: O(V)
Returns None if the graph has a cycle.
in_degree = {v: 0 for v in graph.adj}
for neighbour, _ in graph.neighbours(v):
in_degree[neighbour] += 1
queue = deque([v for v in graph.adj if in_degree[v] == 0 ])
for neighbour, _ in graph.neighbours(vertex):
in_degree[neighbour] -= 1
if in_degree[neighbour] == 0 :
if len (result) != len (graph.adj):
return None # cycle detected
def topological_sort_dfs ( graph ):
DFS-based topological sort.
Time: O(V + E), Space: O(V)
WHITE , GRAY , BLACK = 0 , 1 , 2
color = {v: WHITE for v in graph.adj}
for neighbour, _ in graph.neighbours(v):
if color[neighbour] == GRAY :
return False # back edge = cycle
if color[neighbour] == WHITE :
return result[ :: - 1 ] # reverse for correct order
def count_components ( graph ):
Count connected components in an undirected graph.
Time: O(V + E), Space: O(V)
if vertex not in visited:
dfs_recursive(graph, vertex, visited)
A graph is bipartite if its vertices can be divided into two sets such that no edge connects Vertices within the same set. Equivalently, the graph is 2-colourable.
Check if a graph is bipartite using BFS colouring.
Time: O(V + E), Space: O(V)
colour = {} # vertex -> 0 or 1
for neighbour, _ in graph.neighbours(vertex):
if neighbour not in colour:
colour[neighbour] = 1 - colour[vertex]
elif colour[neighbour] == colour[vertex]:
def has_cycle_undirected ( graph ):
"""Detect cycle in undirected graph using DFS. O(V + E)."""
for neighbour, _ in graph.neighbours(v):
if neighbour not in visited:
elif neighbour != parent:
def has_cycle_directed ( graph ):
"""Detect cycle in directed graph using DFS with three colours. O(V + E)."""
WHITE , GRAY , BLACK = 0 , 1 , 2
colour = {v: WHITE for v in graph.adj}
for neighbour, _ in graph.neighbours(v):
if colour[neighbour] == GRAY :
if colour[neighbour] == WHITE :
return any (dfs(v) for v in graph.adj if colour[v] == WHITE )
If you insert or delete nodes while iterating over a tree (e.g., deleting all nodes matching a Condition), the traversal may skip nodes or follow stale pointers. Either collect nodes to modify And apply changes after traversal, or use a recursive approach that handles modification safely.
When implementing BST operations manually, forgetting to check the BST invariant after deletion can Leave the tree in an invalid state. The in-order successor replacement must be applied correctly: Replace the node’s value with the successor’s value, then delete the successor from the right Subtree.
For deeply unbalanced trees (or worst-case linked lists masquerading as trees), recursive depth can Exceed the stack limit. For trees with n n n nodes, the worst-case recursion depth is n n n . Use Iterative traversal or explicitly check tree balance before recursion.
In BFS/DFS, the visited set must be marked when a vertex is enqueued (BFS) or pushed (DFS), Not when it is dequeued/popped . Marking on dequeue causes duplicate vertices in the queue, Leading to exponential blowup for dense graphs.
Many graph algorithms assume the graph is connected. For disconnected graphs, you must wrap the Algorithm in a loop over all vertices, starting a new BFS/DFS from each unvisited vertex. This Applies to cycle detection, bipartite checking, and component counting.
A naive trie implementation using dictionaries can use 5-10x more memory than a hash set for the Same data, because each node stores a dictionary object with its own overhead. For Memory-constrained environments, use arrays (indexed by character) or radix trees ( Patricia tries) Which compress chains of single-child nodes.
A DAG can have multiple disconnected components. Both Kahn’s algorithm and DFS-based topological Sort must process all vertices, not just those reachable from a single start vertex. Kahn’s Algorithm handles this (all vertices with in-degree 0 are enqueued initially). DFS-based Sort must loop over all vertices.
This topic covers the mathematical techniques and concepts related to trees and graphs, including key theorems, methods, and problem-solving approaches.
Key concepts include:
complex number arithmetic Argand diagrams modulus and argument De Moivre’s theorem roots of complex numbers Regular practice with a variety of question types is essential to build fluency and confidence in applying these mathematical techniques.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.