Graph Algorithms - Complete Guide

Graphs are one of the most versatile data structures in computer science. This guide covers essential graph algorithms and their implementations.

What is a Graph? #

A graph is a data structure consisting of:

  • Vertices (nodes) - The entities
  • Edges - Connections between vertices

Types of Graphs #

Directed vs Undirected #

Directed (Digraph): Edges have direction (one-way streets) Undirected: Edges have no direction (two-way streets)

Weighted vs Unweighted #

Weighted: Edges have values (distance, cost) Unweighted: All edges are equal

Cyclic vs Acyclic #

Cyclic: Contains cycles (paths that loop back) Acyclic: No cycles (DAG - Directed Acyclic Graph)

Graph Representations #

Adjacency List #

Most common representation:

// Undirected graph
const graph = {
  'A': ['B', 'C'],
  'B': ['A', 'D', 'E'],
  'C': ['A', 'F'],
  'D': ['B'],
  'E': ['B', 'F'],
  'F': ['C', 'E']
};

// Using Map
const graph2 = new Map();
graph2.set('A', ['B', 'C']);
graph2.set('B', ['A', 'D']);

// Directed graph with weights
const weightedGraph = {
  'A': [['B', 4], ['C', 2]],
  'B': [['D', 3]],
  'C': [['D', 1], ['E', 5]]
};

Adjacency Matrix #

2D array representation:

// 0 = no edge, 1 = edge exists
const graph = [
  [0, 1, 1, 0],  // A connects to B, C
  [1, 0, 0, 1],  // B connects to A, D
  [1, 0, 0, 1],  // C connects to A, D
  [0, 1, 1, 0]   // D connects to B, C
];

// Weighted graph
const weightedGraph = [
  [0, 4, 2, 0],
  [4, 0, 0, 3],
  [2, 0, 0, 1],
  [0, 3, 1, 0]
];

Edge List #

List of all edges:

const edges = [
  ['A', 'B'],
  ['A', 'C'],
  ['B', 'D'],
  ['C', 'D']
];

// Weighted
const weightedEdges = [
  ['A', 'B', 4],
  ['A', 'C', 2],
  ['B', 'D', 3]
];

Graph Traversal #

Breadth-First Search (BFS) #

Explores level by level:

function bfs(graph, start) {
  const visited = new Set();
  const queue = [start];
  visited.add(start);

  while (queue.length > 0) {
    const node = queue.shift();
    console.log(node);

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

const graph = {
  'A': ['B', 'C'],
  'B': ['A', 'D', 'E'],
  'C': ['A', 'F'],
  'D': ['B'],
  'E': ['B', 'F'],
  'F': ['C', 'E']
};

bfs(graph, 'A');  // A B C D E F

Time: O(V + E), Space: O(V)

Depth-First Search (DFS) #

Explores as far as possible before backtracking:

// Recursive
function dfs(graph, node, visited = new Set()) {
  visited.add(node);
  console.log(node);

  for (const neighbor of graph[node]) {
    if (!visited.has(neighbor)) {
      dfs(graph, neighbor, visited);
    }
  }
}

// Iterative
function dfsIterative(graph, start) {
  const visited = new Set();
  const stack = [start];

  while (stack.length > 0) {
    const node = stack.pop();

    if (!visited.has(node)) {
      visited.add(node);
      console.log(node);

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

dfs(graph, 'A');  // A B D E F C

Time: O(V + E), Space: O(V)

Shortest Path Algorithms #

Dijkstra’s Algorithm #

Finds shortest path in weighted graph:

function dijkstra(graph, start) {
  const distances = {};
  const visited = new Set();
  const pq = [[0, start]];  // [distance, node]

  // Initialize distances
  for (const node in graph) {
    distances[node] = Infinity;
  }
  distances[start] = 0;

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

    if (visited.has(node)) continue;
    visited.add(node);

    for (const [neighbor, weight] of graph[node]) {
      const newDist = dist + weight;

      if (newDist < distances[neighbor]) {
        distances[neighbor] = newDist;
        pq.push([newDist, neighbor]);
      }
    }
  }

  return distances;
}

const graph = {
  'A': [['B', 4], ['C', 2]],
  'B': [['C', 1], ['D', 5]],
  'C': [['D', 8], ['E', 10]],
  'D': [['E', 2]],
  'E': []
};

console.log(dijkstra(graph, 'A'));
// { A: 0, B: 4, C: 2, D: 9, E: 11 }

Time: O((V + E) log V), Space: O(V)

Bellman-Ford Algorithm #

Handles negative weights:

function bellmanFord(vertices, edges, start) {
  const distances = {};

  // Initialize
  vertices.forEach(v => distances[v] = Infinity);
  distances[start] = 0;

  // Relax edges V-1 times
  for (let i = 0; i < vertices.length - 1; i++) {
    for (const [u, v, weight] of edges) {
      if (distances[u] + weight < distances[v]) {
        distances[v] = distances[u] + weight;
      }
    }
  }

  // Check for negative cycles
  for (const [u, v, weight] of edges) {
    if (distances[u] + weight < distances[v]) {
      return null;  // Negative cycle detected
    }
  }

  return distances;
}

const vertices = ['A', 'B', 'C', 'D'];
const edges = [
  ['A', 'B', 4],
  ['A', 'C', 2],
  ['B', 'D', 3],
  ['C', 'D', 1]
];

console.log(bellmanFord(vertices, edges, 'A'));

Time: O(V × E), Space: O(V)

Cycle Detection #

Detect Cycle in Undirected Graph #

function hasCycleUndirected(graph) {
  const visited = new Set();

  function dfs(node, parent) {
    visited.add(node);

    for (const neighbor of graph[node]) {
      if (!visited.has(neighbor)) {
        if (dfs(neighbor, node)) return true;
      } else if (neighbor !== parent) {
        return true;  // Cycle found
      }
    }

    return false;
  }

  for (const node in graph) {
    if (!visited.has(node)) {
      if (dfs(node, null)) return true;
    }
  }

  return false;
}

Detect Cycle in Directed Graph #

function hasCycleDirected(graph) {
  const visited = new Set();
  const recStack = new Set();

  function dfs(node) {
    visited.add(node);
    recStack.add(node);

    for (const neighbor of graph[node] || []) {
      if (!visited.has(neighbor)) {
        if (dfs(neighbor)) return true;
      } else if (recStack.has(neighbor)) {
        return true;  // Cycle found
      }
    }

    recStack.delete(node);
    return false;
  }

  for (const node in graph) {
    if (!visited.has(node)) {
      if (dfs(node)) return true;
    }
  }

  return false;
}

Topological Sort #

Order vertices in DAG:

function topologicalSort(graph) {
  const visited = new Set();
  const stack = [];

  function dfs(node) {
    visited.add(node);

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

    stack.push(node);
  }

  for (const node in graph) {
    if (!visited.has(node)) {
      dfs(node);
    }
  }

  return stack.reverse();
}

const graph = {
  'A': ['C'],
  'B': ['C', 'D'],
  'C': ['E'],
  'D': ['F'],
  'E': ['F'],
  'F': []
};

console.log(topologicalSort(graph));  // ['B', 'A', 'D', 'C', 'E', 'F']

Time: O(V + E), Space: O(V)

Connected Components #

function countComponents(n, edges) {
  const graph = Array.from({ length: n }, () => []);

  // Build graph
  for (const [u, v] of edges) {
    graph[u].push(v);
    graph[v].push(u);
  }

  const visited = new Set();
  let count = 0;

  function dfs(node) {
    visited.add(node);
    for (const neighbor of graph[node]) {
      if (!visited.has(neighbor)) {
        dfs(neighbor);
      }
    }
  }

  for (let i = 0; i < n; i++) {
    if (!visited.has(i)) {
      dfs(i);
      count++;
    }
  }

  return count;
}

console.log(countComponents(5, [[0,1], [1,2], [3,4]]));  // 2

Minimum Spanning Tree #

Prim’s Algorithm #

function prim(graph) {
  const visited = new Set();
  const mst = [];
  const start = Object.keys(graph)[0];

  visited.add(start);
  const edges = [...graph[start].map(([n, w]) => [w, start, n])];

  while (visited.size < Object.keys(graph).length) {
    edges.sort((a, b) => a[0] - b[0]);
    const [weight, from, to] = edges.shift();

    if (visited.has(to)) continue;

    visited.add(to);
    mst.push([from, to, weight]);

    for (const [neighbor, w] of graph[to]) {
      if (!visited.has(neighbor)) {
        edges.push([w, to, neighbor]);
      }
    }
  }

  return mst;
}

Kruskal’s Algorithm #

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

  find(x) {
    if (this.parent[x] !== x) {
      this.parent[x] = this.find(this.parent[x]);
    }
    return this.parent[x];
  }

  union(x, y) {
    const rootX = this.find(x);
    const rootY = this.find(y);

    if (rootX !== rootY) {
      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;
    }
    return false;
  }
}

function kruskal(n, edges) {
  edges.sort((a, b) => a[2] - b[2]);
  const uf = new UnionFind(n);
  const mst = [];

  for (const [u, v, weight] of edges) {
    if (uf.union(u, v)) {
      mst.push([u, v, weight]);
    }
  }

  return mst;
}

const edges = [
  [0, 1, 4],
  [0, 2, 3],
  [1, 2, 1],
  [1, 3, 2],
  [2, 3, 4]
];

console.log(kruskal(4, edges));

Union-Find (Disjoint Set) #

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

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

  union(x, y) {
    const rootX = this.find(x);
    const rootY = this.find(y);

    if (rootX === rootY) return false;

    // 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, y) {
    return this.find(x) === this.find(y);
  }
}

Common Graph Problems #

Number of Islands #

function numIslands(grid) {
  if (!grid.length) return 0;

  const rows = grid.length;
  const cols = grid[0].length;
  let count = 0;

  function dfs(r, c) {
    if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] === '0') {
      return;
    }

    grid[r][c] = '0';  // Mark as visited

    dfs(r + 1, c);
    dfs(r - 1, c);
    dfs(r, c + 1);
    dfs(r, c - 1);
  }

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (grid[r][c] === '1') {
        dfs(r, c);
        count++;
      }
    }
  }

  return count;
}

Course Schedule (Cycle Detection) #

function canFinish(numCourses, prerequisites) {
  const graph = Array.from({ length: numCourses }, () => []);

  for (const [course, prereq] of prerequisites) {
    graph[prereq].push(course);
  }

  const visited = new Set();
  const recStack = new Set();

  function hasCycle(node) {
    visited.add(node);
    recStack.add(node);

    for (const neighbor of graph[node]) {
      if (!visited.has(neighbor)) {
        if (hasCycle(neighbor)) return true;
      } else if (recStack.has(neighbor)) {
        return true;
      }
    }

    recStack.delete(node);
    return false;
  }

  for (let i = 0; i < numCourses; i++) {
    if (!visited.has(i)) {
      if (hasCycle(i)) return false;
    }
  }

  return true;
}

Clone Graph #

function cloneGraph(node) {
  if (!node) return null;

  const clones = new Map();

  function dfs(node) {
    if (clones.has(node)) {
      return clones.get(node);
    }

    const clone = { val: node.val, neighbors: [] };
    clones.set(node, clone);

    for (const neighbor of node.neighbors) {
      clone.neighbors.push(dfs(neighbor));
    }

    return clone;
  }

  return dfs(node);
}

Graph Algorithm Complexity #

AlgorithmTimeSpace
BFSO(V + E)O(V)
DFSO(V + E)O(V)
DijkstraO((V+E) log V)O(V)
Bellman-FordO(V × E)O(V)
Topological SortO(V + E)O(V)
Prim’s MSTO(E log V)O(V)
Kruskal’s MSTO(E log E)O(V)

When to Use Each Algorithm #

BFS: Shortest path in unweighted graph, level-order traversal DFS: Cycle detection, topological sort, pathfinding Dijkstra: Shortest path with positive weights Bellman-Ford: Shortest path with negative weights Union-Find: Connected components, cycle detection in undirected graphs Topological Sort: Task scheduling, dependency resolution

Graph algorithms are essential for solving complex connectivity and path problems. Master these fundamentals to tackle any graph challenge.