Steven's Knowledge

Graphs

Representations, traversals, shortest paths, topological sort, and union-find

Graphs

Graphs generalize trees: nodes can have any number of connections, cycles are allowed, and edges can be directed or weighted. Many real-world problems -- social networks, routing, dependency resolution -- are graph problems in disguise.

Representations

Adjacency List (Preferred for Most Problems)

// Unweighted graph
type Graph = Map<number, number[]>;

function buildGraph(edges: [number, number][]): Graph {
  const graph: Graph = 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);
    graph.get(v)!.push(u); // remove for directed graph
  }
  return graph;
}

// Weighted graph
type WeightedGraph = Map<number, [number, number][]>; // node -> [neighbor, weight]

Adjacency Matrix

// matrix[i][j] = 1 means edge from i to j
const matrix: number[][] = Array.from({ length: n }, () => new Array(n).fill(0));
Adjacency ListAdjacency Matrix
SpaceO(V + E)O(V^2)
Check edge existsO(degree)O(1)
Get all neighborsO(degree)O(V)
Best forSparse graphsDense graphs, quick edge lookup

Default to adjacency list. Most interview graphs are sparse.

BFS on Graphs

function bfs(graph: Graph, start: number): number[] {
  const visited = new Set<number>([start]);
  const queue: number[] = [start];
  const order: number[] = [];

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

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

  return order;
}

BFS gives shortest path in unweighted graphs. To track distance:

function shortestPath(graph: Graph, start: number, end: number): number {
  const visited = new Set<number>([start]);
  const queue: [number, number][] = [[start, 0]]; // [node, distance]

  while (queue.length > 0) {
    const [node, dist] = queue.shift()!;
    if (node === end) return dist;

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

  return -1; // unreachable
}

DFS on Graphs

function dfs(graph: Graph, start: number): number[] {
  const visited = new Set<number>();
  const order: number[] = [];

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

  explore(start);
  return order;
}

Cycle Detection

Undirected Graph

function hasCycleUndirected(graph: Graph): boolean {
  const visited = new Set<number>();

  function dfs(node: number, parent: number): boolean {
    visited.add(node);
    for (const neighbor of graph.get(node) ?? []) {
      if (!visited.has(neighbor)) {
        if (dfs(neighbor, node)) return true;
      } else if (neighbor !== parent) {
        return true; // back edge found
      }
    }
    return false;
  }

  for (const node of graph.keys()) {
    if (!visited.has(node)) {
      if (dfs(node, -1)) return true;
    }
  }
  return false;
}

Directed Graph (Three-Color DFS)

function hasCycleDirected(graph: Graph): boolean {
  const WHITE = 0, GRAY = 1, BLACK = 2;
  const color = new Map<number, number>();

  for (const node of graph.keys()) color.set(node, WHITE);

  function dfs(node: number): boolean {
    color.set(node, GRAY);
    for (const neighbor of graph.get(node) ?? []) {
      if (color.get(neighbor) === GRAY) return true;  // back edge = cycle
      if (color.get(neighbor) === WHITE && dfs(neighbor)) return true;
    }
    color.set(node, BLACK);
    return false;
  }

  for (const node of graph.keys()) {
    if (color.get(node) === WHITE && dfs(node)) return true;
  }
  return false;
}

Topological Sort

Order nodes so that for every directed edge u -> v, u comes before v. Only possible in DAGs (directed acyclic graphs).

Kahn's Algorithm (BFS-based)

function topologicalSort(graph: Graph, numNodes: number): number[] {
  const inDegree = new Array(numNodes).fill(0);
  for (const [, neighbors] of graph) {
    for (const n of neighbors) inDegree[n]++;
  }

  const queue: number[] = [];
  for (let i = 0; i < numNodes; i++) {
    if (inDegree[i] === 0) queue.push(i);
  }

  const order: number[] = [];
  while (queue.length > 0) {
    const node = queue.shift()!;
    order.push(node);
    for (const neighbor of graph.get(node) ?? []) {
      inDegree[neighbor]--;
      if (inDegree[neighbor] === 0) queue.push(neighbor);
    }
  }

  // If order.length < numNodes, there is a cycle
  return order.length === numNodes ? order : [];
}

Use cases: Build systems, course scheduling, dependency resolution.

Shortest Path Algorithms

Dijkstra's Algorithm

For graphs with non-negative edge weights.

function dijkstra(graph: WeightedGraph, start: number, n: number): number[] {
  const dist = new Array(n).fill(Infinity);
  dist[start] = 0;

  // Simple priority queue using sorted array (use a real heap in production)
  const pq: [number, number][] = [[0, start]]; // [distance, node]

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

    if (d > dist[u]) continue; // stale entry

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

  return dist;
}
// Time: O((V + E) log V) with proper heap

Bellman-Ford Algorithm

Handles negative edge weights. Detects negative cycles.

function bellmanFord(
  edges: [number, number, number][], // [from, to, weight]
  n: number,
  start: number
): number[] | null {
  const dist = new Array(n).fill(Infinity);
  dist[start] = 0;

  // Relax all edges V-1 times
  for (let i = 0; i < n - 1; i++) {
    for (const [u, v, w] of edges) {
      if (dist[u] !== Infinity && dist[u] + w < dist[v]) {
        dist[v] = dist[u] + w;
      }
    }
  }

  // Check for negative cycles
  for (const [u, v, w] of edges) {
    if (dist[u] !== Infinity && dist[u] + w < dist[v]) {
      return null; // negative cycle exists
    }
  }

  return dist;
}
// Time: O(V * E)

Union-Find (Disjoint Set Union)

Efficiently tracks connected components. Two operations: find (which set?) and union (merge sets).

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 px = this.find(x);
    const py = this.find(y);
    if (px === py) return false; // already connected

    // union by rank
    if (this.rank[px] < this.rank[py]) this.parent[px] = py;
    else if (this.rank[px] > this.rank[py]) this.parent[py] = px;
    else {
      this.parent[py] = px;
      this.rank[px]++;
    }
    return true;
  }

  connected(x: number, y: number): boolean {
    return this.find(x) === this.find(y);
  }
}
// Both operations: amortized O(α(n)) ≈ O(1)

Use cases: Number of connected components, redundant connections, Kruskal's MST, accounts merge.

Key Takeaways

  1. Default to adjacency list. Build it from edge list at the start of your solution.
  2. BFS for shortest path in unweighted graphs. DFS for exhaustive exploration.
  3. Topological sort whenever you see dependencies or ordering constraints on a DAG.
  4. Dijkstra for weighted shortest path (non-negative). Bellman-Ford when negative weights exist.
  5. Union-Find for connectivity. Faster than BFS/DFS when you need repeated connectivity queries.
  6. Always track visited nodes. Forgetting this is the most common graph bug.

On this page