Skip to content
Unverified — AI-generated content. Help verify this page

Graphs

Graphs model relationships. Social networks, road maps, dependency chains, internet routing, circuit design — if there are entities with connections between them, there is a graph. Graph algorithms are among the most practically important in all of computer science, and they appear frequently in interviews at every level.

Representations

Adjacency List

Each vertex stores a list of its neighbors. Best for sparse graphs (EV2).

TypeScript:

typescript
// Unweighted adjacency list
type AdjList = Map<number, number[]>;

function buildAdjList(edges: [number, number][], directed = false): AdjList {
  const graph: AdjList = new Map();

  for (const [u, v] of edges) {
    if (!graph.has(u)) graph.set(u, []);
    if (!graph.has(v)) graph.set(v, []);
    graph.get(u)!.push(v);
    if (!directed) graph.get(v)!.push(u);
  }

  return graph;
}

// Weighted adjacency list
type WeightedAdjList = Map<number, [number, number][]>; // [neighbor, weight]

Python:

python
from collections import defaultdict

def build_adj_list(edges: list[tuple[int, int]], directed=False) -> dict[int, list[int]]:
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        if not directed:
            graph[v].append(u)
    return graph

# Weighted: graph[u] = [(v, weight), ...]

Adjacency Matrix

A V×V matrix where matrix[i][j] indicates an edge from i to j. Best for dense graphs or when you need O(1) edge lookup.

Python:

python
def build_adj_matrix(n: int, edges: list[tuple[int, int]], directed=False) -> list[list[int]]:
    matrix = [[0] * n for _ in range(n)]
    for u, v in edges:
        matrix[u][v] = 1
        if not directed:
            matrix[v][u] = 1
    return matrix

Comparison

Adjacency ListAdjacency Matrix
SpaceO(V+E)O(V2)
Edge lookupO(degree)O(1)
Iterate neighborsO(degree)O(V)
Add edgeO(1)O(1)
Best forSparse graphsDense graphs, quick edge queries

Breadth-First Search (BFS)

BFS explores all nodes at distance d before exploring nodes at distance d+1. It finds shortest paths in unweighted graphs.

TypeScript:

typescript
function bfs(graph: AdjList, start: number): Map<number, number> {
  const distances = new Map<number, number>();
  const queue: number[] = [start];
  distances.set(start, 0);

  while (queue.length > 0) {
    const node = queue.shift()!;
    const dist = distances.get(node)!;

    for (const neighbor of graph.get(node) || []) {
      if (!distances.has(neighbor)) {
        distances.set(neighbor, dist + 1);
        queue.push(neighbor);
      }
    }
  }

  return distances;
}

Python:

python
from collections import deque

def bfs(graph: dict[int, list[int]], start: int) -> dict[int, int]:
    distances = {start: 0}
    queue = deque([start])

    while queue:
        node = queue.popleft()
        for neighbor in graph.get(node, []):
            if neighbor not in distances:
                distances[neighbor] = distances[node] + 1
                queue.append(neighbor)

    return distances

Complexity: O(V+E) time, O(V) space.

BFS Applications

  • Shortest path in unweighted graph
  • Level-order traversal of trees
  • Connected components (run BFS from each unvisited node)
  • Bipartite check (two-color the graph)
  • Multi-source BFS (e.g., "rotten oranges" — start from multiple sources simultaneously)

Bipartite Check

Python:

python
def is_bipartite(graph: dict[int, list[int]], n: int) -> bool:
    color = [-1] * n

    for start in range(n):
        if color[start] != -1:
            continue
        queue = deque([start])
        color[start] = 0

        while queue:
            node = queue.popleft()
            for neighbor in graph.get(node, []):
                if color[neighbor] == -1:
                    color[neighbor] = 1 - color[node]
                    queue.append(neighbor)
                elif color[neighbor] == color[node]:
                    return False

    return True

Depth-First Search (DFS)

DFS explores as deep as possible before backtracking. It is the foundation of many graph algorithms.

TypeScript:

typescript
function dfs(graph: AdjList, start: number): Set<number> {
  const visited = new Set<number>();

  function explore(node: number): void {
    visited.add(node);
    for (const neighbor of graph.get(node) || []) {
      if (!visited.has(neighbor)) {
        explore(neighbor);
      }
    }
  }

  explore(start);
  return visited;
}

// Iterative DFS
function dfsIterative(graph: AdjList, start: number): Set<number> {
  const visited = new Set<number>();
  const stack = [start];

  while (stack.length > 0) {
    const node = stack.pop()!;
    if (visited.has(node)) continue;
    visited.add(node);

    for (const neighbor of graph.get(node) || []) {
      if (!visited.has(neighbor)) {
        stack.push(neighbor);
      }
    }
  }

  return visited;
}

Python:

python
def dfs(graph: dict[int, list[int]], start: int) -> set[int]:
    visited = set()

    def explore(node: int) -> None:
        visited.add(node)
        for neighbor in graph.get(node, []):
            if neighbor not in visited:
                explore(neighbor)

    explore(start)
    return visited

def dfs_iterative(graph: dict[int, list[int]], start: int) -> set[int]:
    visited = set()
    stack = [start]

    while stack:
        node = stack.pop()
        if node in visited:
            continue
        visited.add(node)

        for neighbor in graph.get(node, []):
            if neighbor not in visited:
                stack.append(neighbor)

    return visited

Complexity: O(V+E) time, O(V) space.

DFS Applications

  • Cycle detection (back edges in directed graphs)
  • Topological sort (ordering dependencies)
  • Connected components / flood fill
  • Path finding (does a path exist?)
  • Strongly connected components (Tarjan's, Kosaraju's)

Cycle Detection in Directed Graph

Python:

python
def has_cycle_directed(graph: dict[int, list[int]], n: int) -> bool:
    WHITE, GRAY, BLACK = 0, 1, 2
    color = [WHITE] * n

    def dfs(node: int) -> bool:
        color[node] = GRAY  # currently in the call stack
        for neighbor in graph.get(node, []):
            if color[neighbor] == GRAY:  # back edge → cycle
                return True
            if color[neighbor] == WHITE and dfs(neighbor):
                return True
        color[node] = BLACK  # fully processed
        return False

    return any(color[i] == WHITE and dfs(i) for i in range(n))

TIP

The three-color technique (WHITE/GRAY/BLACK) distinguishes between unvisited nodes, nodes currently in the recursion stack, and fully processed nodes. A back edge (pointing to a GRAY node) indicates a cycle.

Shortest Path Algorithms

Dijkstra's Algorithm

Finds shortest paths from a source to all vertices in a graph with non-negative edge weights.

TypeScript:

typescript
function dijkstra(
  graph: Map<number, [number, number][]>,
  start: number
): Map<number, number> {
  const dist = new Map<number, number>();
  dist.set(start, 0);

  // MinHeap: [distance, node]
  // Using a simple sorted insertion for clarity
  // In production, use a proper priority queue
  const pq: [number, number][] = [[0, start]];

  while (pq.length > 0) {
    pq.sort((a, b) => a[0] - b[0]);
    const [d, u] = pq.shift()!;

    if (d > (dist.get(u) ?? Infinity)) continue;

    for (const [v, weight] of graph.get(u) || []) {
      const newDist = d + weight;
      if (newDist < (dist.get(v) ?? Infinity)) {
        dist.set(v, newDist);
        pq.push([newDist, v]);
      }
    }
  }

  return dist;
}

Python:

python
import heapq

def dijkstra(graph: dict[int, list[tuple[int, int]]], start: int) -> dict[int, float]:
    dist: dict[int, float] = {start: 0}
    pq = [(0, start)]  # (distance, node)

    while pq:
        d, u = heapq.heappop(pq)
        if d > dist.get(u, float('inf')):
            continue

        for v, weight in graph.get(u, []):
            new_dist = d + weight
            if new_dist < dist.get(v, float('inf')):
                dist[v] = new_dist
                heapq.heappush(pq, (new_dist, v))

    return dist

Complexity: O((V+E)logV) with a binary heap. O(V2) with a simple array (better for dense graphs).

DANGER

Dijkstra fails with negative edge weights. A negative edge can create a shorter path through a node that was already finalized. Use Bellman-Ford instead.

Bellman-Ford Algorithm

Handles graphs with negative edge weights. Detects negative-weight cycles.

Python:

python
def bellman_ford(n: int, edges: list[tuple[int, int, int]], start: int) -> dict[int, float]:
    """edges: [(u, v, weight), ...]"""
    dist: dict[int, float] = {i: float('inf') for i in range(n)}
    dist[start] = 0

    # Relax all edges V-1 times
    for _ in range(n - 1):
        for u, v, w in edges:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w

    # Check for negative-weight cycles
    for u, v, w in edges:
        if dist[u] + w < dist[v]:
            raise ValueError("Graph contains a negative-weight cycle")

    return dist

Complexity: O(VE) time, O(V) space.

Floyd-Warshall Algorithm

Finds shortest paths between all pairs of vertices.

Python:

python
def floyd_warshall(n: int, edges: list[tuple[int, int, int]]) -> list[list[float]]:
    INF = float('inf')
    dist = [[INF] * n for _ in range(n)]

    for i in range(n):
        dist[i][i] = 0

    for u, v, w in edges:
        dist[u][v] = w

    for k in range(n):
        for i in range(n):
            for j in range(n):
                if dist[i][k] + dist[k][j] < dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]

    return dist

Complexity: O(V3) time, O(V2) space.

Algorithm Comparison

AlgorithmWeightsNegative?All-pairs?Complexity
BFSUnweightedN/ANoO(V+E)
DijkstraNon-negativeNoNoO((V+E)logV)
Bellman-FordAnyYes (detects cycles)NoO(VE)
Floyd-WarshallAnyYesYesO(V3)

Topological Sort

A linear ordering of vertices in a DAG (Directed Acyclic Graph) such that for every edge (u,v), u comes before v. Used for dependency resolution, build systems, course scheduling.

Topological order: A, B, C, D, E (or A, B, D, C, E — not unique).

Kahn's Algorithm (BFS-based)

TypeScript:

typescript
function topologicalSort(graph: AdjList, numNodes: number): number[] {
  const inDegree = new Array(numNodes).fill(0);

  // Calculate in-degrees
  for (const [_, neighbors] of graph) {
    for (const neighbor of neighbors) {
      inDegree[neighbor]++;
    }
  }

  // Start with all nodes that have no incoming edges
  const queue: number[] = [];
  for (let i = 0; i < numNodes; i++) {
    if (inDegree[i] === 0) queue.push(i);
  }

  const result: number[] = [];

  while (queue.length > 0) {
    const node = queue.shift()!;
    result.push(node);

    for (const neighbor of graph.get(node) || []) {
      inDegree[neighbor]--;
      if (inDegree[neighbor] === 0) {
        queue.push(neighbor);
      }
    }
  }

  if (result.length !== numNodes) {
    throw new Error("Graph has a cycle — topological sort impossible");
  }

  return result;
}

Python:

python
from collections import deque

def topological_sort(graph: dict[int, list[int]], num_nodes: int) -> list[int]:
    in_degree = [0] * num_nodes
    for neighbors in graph.values():
        for neighbor in neighbors:
            in_degree[neighbor] += 1

    queue = deque(i for i in range(num_nodes) if in_degree[i] == 0)
    result = []

    while queue:
        node = queue.popleft()
        result.append(node)
        for neighbor in graph.get(node, []):
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)

    if len(result) != num_nodes:
        raise ValueError("Graph has a cycle")

    return result

Complexity: O(V+E) time, O(V) space.

DFS-based Topological Sort

Python:

python
def topological_sort_dfs(graph: dict[int, list[int]], num_nodes: int) -> list[int]:
    visited = set()
    result = []

    def dfs(node: int) -> None:
        visited.add(node)
        for neighbor in graph.get(node, []):
            if neighbor not in visited:
                dfs(neighbor)
        result.append(node)  # add after all descendants are processed

    for i in range(num_nodes):
        if i not in visited:
            dfs(i)

    return result[::-1]  # reverse the postorder

Strongly Connected Components (SCCs)

In a directed graph, an SCC is a maximal set of vertices where every vertex is reachable from every other vertex.

Kosaraju's Algorithm

  1. Run DFS on original graph, push nodes to stack in finish order
  2. Transpose the graph (reverse all edges)
  3. Pop from stack, run DFS on transposed graph — each DFS gives one SCC

Python:

python
def kosaraju(graph: dict[int, list[int]], n: int) -> list[list[int]]:
    # Step 1: DFS and push to stack in finish order
    visited = set()
    stack = []

    def dfs1(node: int) -> None:
        visited.add(node)
        for neighbor in graph.get(node, []):
            if neighbor not in visited:
                dfs1(neighbor)
        stack.append(node)

    for i in range(n):
        if i not in visited:
            dfs1(i)

    # Step 2: Build transposed graph
    transposed: dict[int, list[int]] = defaultdict(list)
    for u in graph:
        for v in graph[u]:
            transposed[v].append(u)

    # Step 3: DFS on transposed graph in reverse finish order
    visited.clear()
    sccs = []

    def dfs2(node: int, component: list[int]) -> None:
        visited.add(node)
        component.append(node)
        for neighbor in transposed.get(node, []):
            if neighbor not in visited:
                dfs2(neighbor, component)

    while stack:
        node = stack.pop()
        if node not in visited:
            component: list[int] = []
            dfs2(node, component)
            sccs.append(component)

    return sccs

Complexity: O(V+E) time, O(V) space.

Union-Find (Disjoint Set Union)

Union-Find tracks a set of elements partitioned into disjoint subsets. It supports two operations efficiently: find (which set does an element belong to?) and union (merge two sets).

With Path Compression + Union by Rank

TypeScript:

typescript
class UnionFind {
  parent: number[];
  rank: number[];

  constructor(n: number) {
    this.parent = Array.from({ length: n }, (_, i) => i);
    this.rank = new Array(n).fill(0);
  }

  find(x: number): number {
    if (this.parent[x] !== x) {
      this.parent[x] = this.find(this.parent[x]); // path compression
    }
    return this.parent[x];
  }

  union(x: number, y: number): boolean {
    const rootX = this.find(x);
    const rootY = this.find(y);
    if (rootX === rootY) return false; // already connected

    // Union by rank
    if (this.rank[rootX] < this.rank[rootY]) {
      this.parent[rootX] = rootY;
    } else if (this.rank[rootX] > this.rank[rootY]) {
      this.parent[rootY] = rootX;
    } else {
      this.parent[rootY] = rootX;
      this.rank[rootX]++;
    }

    return true;
  }

  connected(x: number, y: number): boolean {
    return this.find(x) === this.find(y);
  }
}

Python:

python
class UnionFind:
    def __init__(self, n: int):
        self.parent = list(range(n))
        self.rank = [0] * n

    def find(self, x: int) -> int:
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])  # path compression
        return self.parent[x]

    def union(self, x: int, y: int) -> bool:
        root_x, root_y = self.find(x), self.find(y)
        if root_x == root_y:
            return False

        if self.rank[root_x] < self.rank[root_y]:
            self.parent[root_x] = root_y
        elif self.rank[root_x] > self.rank[root_y]:
            self.parent[root_y] = root_x
        else:
            self.parent[root_y] = root_x
            self.rank[root_x] += 1

        return True

    def connected(self, x: int, y: int) -> bool:
        return self.find(x) == self.find(y)

With both path compression and union by rank, operations are nearly O(1) — formally O(α(n)) where α is the inverse Ackermann function, which is effectively constant (4) for all practical input sizes.

Union-Find Applications

  • Connected components: Run union on all edges, count distinct roots
  • Cycle detection in undirected graphs: If find(u) == find(v) before unioning edge (u,v), there is a cycle
  • Kruskal's MST: Sort edges by weight, union them greedily
  • Consistent Hashing: Track which nodes own which partitions

Minimum Spanning Tree (MST)

Kruskal's Algorithm

Sort all edges by weight, then greedily add each edge if it doesn't create a cycle (using Union-Find).

Python:

python
def kruskal(n: int, edges: list[tuple[int, int, int]]) -> list[tuple[int, int, int]]:
    """Returns MST edges. edges: [(u, v, weight), ...]"""
    edges.sort(key=lambda e: e[2])
    uf = UnionFind(n)
    mst = []

    for u, v, w in edges:
        if uf.union(u, v):
            mst.append((u, v, w))
            if len(mst) == n - 1:
                break

    return mst

Complexity: O(ElogE) time (dominated by sorting), O(V) space.

Practice Problems

ProblemAlgorithmDifficulty
Number of IslandsBFS/DFS flood fillMedium
Clone GraphBFS/DFS + hash mapMedium
Course ScheduleTopological sort (cycle detection)Medium
Course Schedule IITopological sort (ordering)Medium
Network Delay TimeDijkstraMedium
Cheapest Flights Within K StopsModified Bellman-FordMedium
Redundant ConnectionUnion-FindMedium
Word LadderBFSHard
Alien DictionaryTopological sortHard
Critical Connections in a NetworkTarjan's bridgesHard

Further Reading

Try It Yourself

Problem 1: Given a graph with edges [(0,1), (0,2), (1,2), (2,3)] and 4 nodes, find the shortest path from node 0 to node 3 (unweighted).

Solution

Use BFS starting from node 0:

  • Level 0: visit 0, enqueue neighbors 1, 2
  • Level 1: visit 1 (neighbor 2 already visited), visit 2 (enqueue 3)
  • Level 2: visit 3 Shortest path distance: 2 (path: 0 -> 2 -> 3)

Problem 2: Determine if the directed graph with edges [(0,1), (1,2), (2,0)] and 3 nodes contains a cycle.

Solution

Use DFS with three-color marking (WHITE, GRAY, BLACK):

  • Start DFS at 0: color 0 GRAY
  • Visit 1: color 1 GRAY
  • Visit 2: color 2 GRAY
  • From 2, neighbor 0 is GRAY (still in the call stack) → back edge detected Answer: Yes, the graph contains a cycle (0 -> 1 -> 2 -> 0).

Problem 3: Perform topological sort on a DAG with edges: A->C, B->C, B->D, C->E, D->E.

Solution

Using Kahn's algorithm (BFS-based):

  • In-degrees: A=0, B=0, C=2, D=1, E=2
  • Start with queue: [A, B]
  • Process A: reduce C's in-degree to 1. Process B: reduce C to 0, D to 0. Queue: [C, D]
  • Process C: reduce E to 1. Process D: reduce E to 0. Queue: [E]
  • Process E. One valid topological order: [A, B, C, D, E] (or [B, A, C, D, E] or [A, B, D, C, E], etc.)

Problem 4: Given a weighted graph with edges (A,B,4), (A,C,1), (C,B,2), (B,D,1), (C,D,5), find the shortest path from A to D using Dijkstra.

Solution
  • Initialize: dist = {A:0, B:inf, C:inf, D:inf}, PQ = [(0,A)]
  • Process A: update B=4, C=1. PQ = [(1,C), (4,B)]
  • Process C: update B = min(4, 1+2) = 3, D = min(inf, 1+5) = 6. PQ = [(3,B), (4,B), (6,D)]
  • Process B (dist=3): update D = min(6, 3+1) = 4. PQ = [(4,B), (4,D), (6,D)]
  • Process D (dist=4): done. Shortest path A to D: 4 (path: A -> C -> B -> D)

Problem 5: Given 5 nodes and edges [(0,1), (0,2), (1,3), (2,3), (3,4)], use Union-Find to count the number of connected components.

Solution

Initialize: each node is its own component (5 components).

  • Union(0,1): merge → 4 components
  • Union(0,2): merge → 3 components
  • Union(1,3): merge → 2 components
  • Union(2,3): find(2)=0, find(3)=0 → already connected, skip
  • Union(3,4): merge → 1 component Answer: 1 connected component

Quick Quiz

1. What is the time complexity of BFS on a graph represented as an adjacency list?

  • a) O(V)
  • b) O(E)
  • c) O(V+E)
  • d) O(VE)
Answer

c) O(V+E) — BFS visits every vertex once (O(V)) and examines every edge once (O(E)) in an adjacency list representation.

2. Which shortest-path algorithm should you use if the graph has negative edge weights (but no negative cycles)?

  • a) BFS
  • b) Dijkstra
  • c) Bellman-Ford
  • d) Floyd-Warshall (if single-source only is needed)
Answer

c) Bellman-Ford — Dijkstra fails with negative weights because it assumes finalized distances cannot be improved. Bellman-Ford relaxes all edges V1 times and handles negative weights correctly.

3. What does topological sort require about the graph?

  • a) The graph must be undirected
  • b) The graph must be a DAG (Directed Acyclic Graph)
  • c) The graph must be weighted
  • d) The graph must be connected
Answer

b) The graph must be a DAG (Directed Acyclic Graph) — Topological sort produces a linear ordering where for every edge (u,v), u comes before v. This is only possible when there are no cycles.

4. What is the amortized time complexity of Union-Find operations with both path compression and union by rank?

  • a) O(1)
  • b) O(logn)
  • c) O(α(n)) (inverse Ackermann, effectively constant)
  • d) O(n)
Answer

c) O(α(n)) — The inverse Ackermann function grows so slowly that α(n)4 for all practical input sizes, making operations effectively constant time.

5. When is an adjacency matrix preferred over an adjacency list?

  • a) When the graph is sparse
  • b) When the graph is dense and you need O(1) edge lookup
  • c) When you need to iterate over neighbors quickly
  • d) When memory is limited
Answer

b) When the graph is dense and you need O(1) edge lookup — An adjacency matrix uses O(V2) space, which is efficient for dense graphs where EV2. It provides O(1) edge existence checks, whereas an adjacency list requires scanning the neighbor list.

"What I cannot create, I do not understand." — Richard Feynman