<< Home

Chapter 6: Graph and Tree Algorithms

Graphs and trees model connections and relationships. They form the foundation of countless problems in computing, from social networks and navigation systems to compilers and search engines. Understanding their structure and the algorithms that operate on them is essential to advanced coding and efficient design.

💡 A tree is a special kind of graph that contains no cycles. Every two nodes are connected by exactly one path, making trees simpler to reason about but still rich in algorithmic variety.

Understanding Graphs and Networks

A graph consists of vertices (also called nodes) and edges that connect them. Edges may be directed or undirected, and they can also carry weights that represent cost, distance, or strength of connection. Graphs can be stored as adjacency lists, adjacency matrices, or edge lists depending on the type of operations required.

Common graph representations

An adjacency list stores for each vertex the list of its neighbours. This is efficient for sparse graphs with relatively few edges. An adjacency matrix uses a two-dimensional array and is convenient for dense graphs but requires more memory.

# adjacency list example
graph = {
  'A': ['B', 'C'],
  'B': ['A', 'D'],
  'C': ['A', 'D'],
  'D': ['B', 'C']
}
⚠️ The choice of representation directly affects performance. For algorithms that frequently test whether an edge exists between two vertices, a matrix is faster. For algorithms that explore only part of a graph, lists are more memory-efficient.

Directed, weighted, and undirected graphs

In directed graphs, edges have directio, such as links on the web where one page points to another. Weighted graphs attach a numerical value to each edge, representing distance, time, or cost. Many real-world systems combine both properties, creating directed weighted networks.

Breadth-First and Depth-First Search

Traversal algorithms explore the structure of a graph. Breadth-first search (BFS) visits vertices level by level, while depth-first search (DFS) explores as far as possible along each branch before backtracking. Both are key for discovering paths, detecting connectivity, and exploring problem states.

Breadth-first search

BFS uses a queue to visit vertices in order of distance from the starting point. It guarantees the shortest path in an unweighted graph and forms the foundation of many pathfinding algorithms.

from collections import deque

def bfs(graph, start):
  visited = set([start])
  queue = deque([start])
  order = []

  while queue:
    v = queue.popleft()
    order.append(v)
    for n in graph[v]:
      if n not in visited:
        visited.add(n)
        queue.append(n)
  return order

Depth-first search

DFS uses a stack (explicitly or through recursion) to dive deep into each branch before exploring siblings. It is often used for tasks like cycle detection, topological sorting, and component analysis.

def dfs(graph, start, visited=None):
  if visited is None:
    visited = set()
  visited.add(start)
  for n in graph[start]:
    if n not in visited:
      dfs(graph, n, visited)
  return visited
💡 Although BFS and DFS seem similar, their order of exploration changes their usefulness. BFS is better for shortest paths and minimal steps; DFS is better for exploring structures and solving puzzles with backtracking.

Minimum Spanning Trees

A spanning tree connects all vertices of a graph with the smallest possible number of edges. A minimum spanning tree (MST) is one whose total edge weight is minimal. MSTs are important in network design, clustering, and circuit layout.

Kruskal’s algorithm

Kruskal’s algorithm builds an MST by sorting all edges by weight and adding them one by one, skipping those that would form a cycle. It uses a disjoint-set data structure to track which vertices are already connected.

def kruskal(n, edges):
  parent = list(range(n))
  def find(x):
    if parent[x] != x:
      parent[x] = find(parent[x])
    return parent[x]
  def union(a, b):
    pa, pb = find(a), find(b)
    if pa != pb:
      parent[pb] = pa

  mst = []
  for u, v, w in sorted(edges, key=lambda x: x[2]):
    if find(u) != find(v):
      union(u, v)
      mst.append((u, v, w))
  return mst

Prim’s algorithm

Prim’s algorithm starts from any vertex and grows the MST by repeatedly adding the smallest edge that connects a new vertex to the tree. It works well with adjacency lists and a priority queue.

import heapq

def prim(n, adj):
  visited = [False] * n
  pq = [(0, 0, -1)]  # weight, vertex, parent
  mst = []
  while pq:
    w, u, p = heapq.heappop(pq)
    if visited[u]:
      continue
    visited[u] = True
    if p != -1:
      mst.append((p, u, w))
    for v, w2 in adj[u]:
      if not visited[v]:
        heapq.heappush(pq, (w2, v, u))
  return mst
⚠️ Kruskal’s and Prim’s algorithms both produce the same MST for a connected, weighted, undirected graph, but their internal strategies differ. Kruskal sorts edges globally; Prim grows one component at a time.

Balanced Trees and Heaps

Trees are hierarchical data structures where each node can have children. They are widely used for indexing, searching, and maintaining sorted order. Balanced trees and heaps keep operations efficient even as data grows.

Binary search trees

In a binary search tree (BST), all keys in the left subtree are smaller than the node’s key, and all keys in the right subtree are larger. Searching, insertion, and deletion can all run in O(log n) time if the tree remains balanced.

Heaps and priority queues

A heap is a complete binary tree that satisfies the heap property: each node is smaller (in a min-heap) or larger (in a max-heap) than its children. Python’s heapq module provides an efficient heap-based priority queue implementation.

import heapq

nums = [8, 3, 6, 1, 9]
heapq.heapify(nums)
heapq.heappush(nums, 2)
smallest = heapq.heappop(nums)
💡 Heaps are not sorted structures. They only guarantee that the smallest (or largest) element is accessible at the root in constant time. Full ordering still requires a sort.

Graphs in Real-World Systems

Graphs and trees are everywhere in technology. They describe routes, dependencies, and hierarchies, providing structure to complex systems. Algorithms built on them drive search engines, compilers, and social platforms alike.

Network routing and logistics

Routing algorithms in the Internet or in transport networks rely on graph traversal and pathfinding to find efficient routes. Dijkstra’s and A* algorithms both operate on graph principles to minimise total cost or time.

Data structures and hierarchies

Trees model parent–child relationships naturally, which makes them ideal for representing file systems, XML and JSON documents, or organisational charts. Their recursive definition also simplifies many algorithms that process nested data.

Recommendation and influence networks

Social and recommendation systems use graph algorithms to evaluate relationships between entities. Concepts such as centrality, clustering, and flow all stem from the fundamental graph algorithms introduced in this chapter.