Graph Algorithms: Representations, Traversals, Shortest Paths, and Spanning Trees
A graph $G = (V, E)$ consists of a set of vertices (nodes) $V$ and edges $E$ connecting pairs of vertices. Graph algorithms form the backbone of network routing protocols (OSPF, BGP), dependency resolution engines, social network mapping, and recommendation systems.
⚡ Quick Dive
Graph Algorithm Complexity Matrix
| Algorithm | Problem Domain | Time Complexity | Space Complexity | Supports Negative Edges? |
|---|---|---|---|---|
| Breadth-First Search (BFS) | Shortest path (Unweighted), Level traversal | $O(V + E)$ | $O(V)$ | N/A (Unweighted) |
| Depth-First Search (DFS) | Cycle detection, Connected components, Mazes | $O(V + E)$ | $O(V)$ | N/A |
| Topological Sort (Kahn's) | Dependency ordering (DAGs) | $O(V + E)$ | $O(V)$ | N/A (Directed Acyclic) |
| Dijkstra's Algorithm | Single-Source Shortest Path (SSSP) | $O((V + E) \log V)$ | $O(V)$ | ❌ (Non-negative weights only) |
| Bellman-Ford Algorithm | SSSP (with negative cycle detection) | $O(V \cdot E)$ | $O(V)$ | ✅ (Detects negative cycles) |
| Floyd-Warshall | All-Pairs Shortest Path (APSP) | $O(V^3)$ | $O(V^2)$ | ✅ (No negative cycles) |
| Kruskal's Algorithm | Minimum Spanning Tree (MST) | $O(E \log E)$ | $O(V)$ | ✅ |
| Prim's Algorithm | Minimum Spanning Tree (MST) | $O((V + E) \log V)$ | $O(V)$ | ✅ |
📖 Extended Guide
1. Graph Representations
Graph:
(0) ─── [1] ─── (1)
│ │
[4] [2]
│ │
(3) ─── [3] ─── (2)
1. Adjacency Matrix (Dense Graphs: E ≈ V²):
Space: O(V²) | Edge Lookup: O(1)
0 1 2 3
0 [ 0, 1, 0, 4 ]
1 [ 1, 0, 2, 0 ]
2 [ 0, 2, 0, 3 ]
3 [ 4, 0, 3, 0 ]
2. Adjacency List (Sparse Graphs: E ≪ V² - Standard Choice):
Space: O(V + E) | Edge Lookup: O(degree(u))
0: -> (1, wt:1) -> (3, wt:4)
1: -> (0, wt:1) -> (2, wt:2)
2: -> (1, wt:2) -> (3, wt:3)
3: -> (0, wt:4) -> (2, wt:3)
2. Traversals: BFS vs. DFS
Breadth-First Search (BFS)
- Traverses layer by layer using a Queue.
- Guarantees the shortest path on unweighted graphs.
Depth-First Search (DFS)
- Explores as deep as possible along each branch before backtracking using a Stack (or recursion).
- Ideal for topological sorting, strongly connected components (Kosaraju / Tarjan), and cycle detection.
// Cycle detection in Directed Graph using 3-color DFS
const (
White = 0 // Unvisited
Gray = 1 // Currently visiting (in recursion stack)
Black = 2 // Fully visited
)
func hasCycle(u int, adj [][]int, colors []int) bool {
colors[u] = Gray
for _, v := range adj[u] {
if colors[v] == Gray {
return true // Back-edge detected (Cycle)
}
if colors[v] == White && hasCycle(v, adj, colors) {
return true
}
}
colors[u] = Black
return false
}
3. Topological Sorting (Kahn's Algorithm)
Topological ordering arranges vertices in a Directed Acyclic Graph (DAG) linearly such that for every directed edge $u \to v$, vertex $u$ comes before $v$.
func TopologicalSort(numNodes int, adj [][]int) ([]int, bool) {
inDegree := make([]int, numNodes)
for u := 0; u < numNodes; u++ {
for _, v := range adj[u] {
inDegree[v]++
}
}
queue := []int{}
for i := 0; i < numNodes; i++ {
if inDegree[i] == 0 {
queue = append(queue, i)
}
}
order := []int{}
for len(queue) > 0 {
curr := queue[0]
queue = queue[1:]
order = append(order, curr)
for _, neighbor := range adj[curr] {
inDegree[neighbor]--
if inDegree[neighbor] == 0 {
queue = append(queue, neighbor)
}
}
}
// If order contains all nodes, a valid topological sort exists; else graph has a cycle
return order, len(order) == numNodes
}
4. Single-Source Shortest Path: Dijkstra's Algorithm
Dijkstra's algorithm finds the shortest path from a starting vertex to all other vertices in a weighted graph with non-negative edge weights using a greedy min-priority queue:
package main
import (
"container/heap"
"fmt"
"math"
)
type Edge struct {
to, weight int
}
type Item struct {
node, dist int
}
type PriorityQueue []Item
func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool { return pq[i].dist < pq[j].dist }
func (pq PriorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] }
func (pq *PriorityQueue) Push(x any) { *pq = append(*pq, x.(Item)) }
func (pq *PriorityQueue) Pop() any {
old := *pq
n := len(old)
item := old[n-1]
*pq = old[:n-1]
return item
}
func Dijkstra(numNodes, start int, adj [][]Edge) []int {
dist := make([]int, numNodes)
for i := range dist {
dist[i] = math.MaxInt32
}
dist[start] = 0
pq := &PriorityQueue{}
heap.Init(pq)
heap.Push(pq, Item{node: start, dist: 0})
for pq.Len() > 0 {
curr := heap.Pop(pq).(Item)
u := curr.node
d := curr.dist
if d > dist[u] {
continue // Outdated entry
}
for _, edge := range adj[u] {
if dist[u]+edge.weight < dist[edge.to] {
dist[edge.to] = dist[u] + edge.weight
heap.Push(pq, Item{node: edge.to, dist: dist[edge.to]})
}
}
}
return dist
}
func main() {
n := 4
adj := make([][]Edge, n)
adj[0] = []Edge{{1, 4}, {2, 1}}
adj[2] = []Edge{{1, 2}, {3, 5}}
adj[1] = []Edge{{3, 1}}
dist := Dijkstra(n, 0, adj)
for i, d := range dist {
fmt.Printf("Shortest distance from 0 to %d: %d\n", i, d)
}
}
5. Minimum Spanning Trees: Kruskal's vs. Prim's
A Minimum Spanning Tree (MST) connects all vertices in an undirected weighted graph with minimum total edge weight and zero cycles ($V-1$ edges).
- Kruskal's Algorithm: Sorts all edges globally by weight and uses a Disjoint Set Union (DSU / Union-Find) data structure to greedily add edges that don't create cycles ($O(E \log E)$).
- Prim's Algorithm: Grows a connected tree outward from a starting node using a priority queue ($O((V+E) \log V)$). Preferred for dense graphs ($E \approx V^2$).