Graphs

Reading time
2 min read
Word count
303 words
Diagram count
0 diagrams

Source: Victor Bona's Obsidian Compendium snapshot, Knowledge base/Data Structures/Graphs.md.

A Graph is a non-linear data structure consisting of vertices (nodes) and edges that connect pairs of vertices. Graphs are used to represent relationships, networks, and connections between entities. They are fundamental to modeling real-world problems in computer science.

  • Intent: Model relationships and connections between entities, enabling analysis of networks, paths, and dependencies.
  • Use Cases: Social networks, web page linking, road networks, dependency resolution, circuit design, recommendation systems, network routing.
  • Key Properties:
    • Vertices (V) and Edges (E)
    • Can be directed or undirected
    • Can be weighted or unweighted
    • Can have cycles or be acyclic

Graph Types

Undirected Graph:          Directed Graph (Digraph):
    A --- B                     A -- B
    |     |                          |
    |     |                     |     
    C --- D                     C -- D

Weighted Graph:            Directed Acyclic Graph (DAG):
    A -5- B                     A -- B
    |     |                     |     |
   3|    2|                          
    C -4- D                     C -- D

Graph Representations

Adjacency List

Best for sparse graphs (E << V²)

class Graph<T> {
  private adjacencyList: Map<T, Set<T>> = new Map();
  private directed: boolean;

  constructor(directed: boolean = false) {
    this.directed = directed;
  }

  // O(1) - Add vertex
  addVertex(vertex: T): void {
    if (!this.adjacencyList.has(vertex)) {
      this.adjacencyList.set(vertex, new Set());
    }
  }

  // O(1) - Add edge
  addEdge(v1: T, v2: T): void {
    this.addVertex(v1);
    this.addVertex(v2);
    this.adjacencyList.get(v1)!.add(v2);
    if (!this.directed) {
      this.adjacencyList.get(v2)!.add(v1);
    }
  }

  // O(1) - Remove edge
  removeEdge(v1: T, v2: T): void {
    this.adjacencyList.get(v1)?.delete(v2);
    if (!this.directed) {
      this.adjacencyList.get(v2)?.delete(v1);
    }
  }

  // O(V + E) - Remove vertex
  removeVertex(vertex: T): void {
    if (!this.adjacencyList.has(vertex)) return;

    // Remove all edges to this vertex
    for (const [_, neighbors] of this.adjacencyList) {
      neighbors.delete(vertex);
    }

    this.adjacencyList.delete(vertex);
  }

  // O(1) - Get neighbors
  getNeighbors(vertex: T): T[] {
    return [...(this.adjacencyList.get(vertex) || [])];
  }

  // O(1) - Check if edge exists
  hasEdge(v1: T, v2: T): boolean {
    return this.adjacencyList.get(v1)?.has(v2) || false;
  }

  getVertices(): T[] {
    return [...this.adjacencyList.keys()];
  }

  getEdgeCount(): number {
    let count = 0;
    for (const neighbors of this.adjacencyList.values()) {
      count += neighbors.size;
    }
    return this.directed ? count : count / 2;
  }
}

Weighted Graph

class WeightedGraph<T> {
  private adjacencyList: Map<T, Map<T, number>> = new Map();
  private directed: boolean;

  constructor(directed: boolean = false) {
    this.directed = directed;
  }

  addVertex(vertex: T): void {
    if (!this.adjacencyList.has(vertex)) {
      this.adjacencyList.set(vertex, new Map());
    }
  }

  addEdge(v1: T, v2: T, weight: number): void {
    this.addVertex(v1);
    this.addVertex(v2);
    this.adjacencyList.get(v1)!.set(v2, weight);
    if (!this.directed) {
      this.adjacencyList.get(v2)!.set(v1, weight);
    }
  }

  getWeight(v1: T, v2: T): number | undefined {
    return this.adjacencyList.get(v1)?.get(v2);
  }

  getNeighborsWithWeights(vertex: T): [T, number][] {
    const neighbors = this.adjacencyList.get(vertex);
    return neighbors ? [...neighbors.entries()] : [];
  }
}

Adjacency Matrix

Best for dense graphs or when O(1) edge lookup is needed

class GraphMatrix {
  private matrix: number[][];
  private vertices: number;

  constructor(vertices: number) {
    this.vertices = vertices;
    this.matrix = Array.from({ length: vertices }, () =>
      new Array(vertices).fill(0)
    );
  }

  // O(1) - Add edge (weighted)
  addEdge(v1: number, v2: number, weight: number = 1): void {
    this.matrix[v1][v2] = weight;
    // For undirected: this.matrix[v2][v1] = weight;
  }

  // O(1) - Check edge
  hasEdge(v1: number, v2: number): boolean {
    return this.matrix[v1][v2] !== 0;
  }

  // O(V) - Get neighbors
  getNeighbors(vertex: number): number[] {
    const neighbors: number[] = [];
    for (let i = 0; i < this.vertices; i++) {
      if (this.matrix[vertex][i] !== 0) {
        neighbors.push(i);
      }
    }
    return neighbors;
  }
}

Space/Time Complexity

OperationAdjacency ListAdjacency Matrix
SpaceO(V + E)O(V²)
Add VertexO(1)O(V²)
Add EdgeO(1)O(1)
Remove EdgeO(E)O(1)
Check EdgeO(V)O(1)
Get NeighborsO(degree)O(V)
Iterate EdgesO(V + E)O(V²)

Graph Traversal

Breadth-First Search (BFS)

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

  visited.add(start);

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

    for (const neighbor of graph.getNeighbors(vertex)) {
      if (!visited.has(neighbor)) {
        visited.add(neighbor);
        queue.push(neighbor);
      }
    }
  }

  return result;
}

Depth-First Search (DFS)

// Recursive
function dfsRecursive<T>(graph: Graph<T>, start: T): T[] {
  const visited = new Set<T>();
  const result: T[] = [];

  function dfs(vertex: T): void {
    visited.add(vertex);
    result.push(vertex);

    for (const neighbor of graph.getNeighbors(vertex)) {
      if (!visited.has(neighbor)) {
        dfs(neighbor);
      }
    }
  }

  dfs(start);
  return result;
}

// Iterative
function dfsIterative<T>(graph: Graph<T>, start: T): T[] {
  const visited = new Set<T>();
  const result: T[] = [];
  const stack: T[] = [start];

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

    if (visited.has(vertex)) continue;
    visited.add(vertex);
    result.push(vertex);

    for (const neighbor of graph.getNeighbors(vertex)) {
      if (!visited.has(neighbor)) {
        stack.push(neighbor);
      }
    }
  }

  return result;
}

Classic Graph Algorithms

Dijkstra's Shortest Path

function dijkstra<T>(
  graph: WeightedGraph<T>,
  start: T
): Map<T, { distance: number; previous: T | null }> {
  const distances = new Map<T, { distance: number; previous: T | null }>();
  const visited = new Set<T>();
  const pq: [T, number][] = [[start, 0]];

  // Initialize
  for (const vertex of graph.getVertices()) {
    distances.set(vertex, { distance: Infinity, previous: null });
  }
  distances.get(start)!.distance = 0;

  while (pq.length > 0) {
    // Get minimum (should use proper min-heap)
    pq.sort((a, b) => a[1] - b[1]);
    const [current, currentDist] = pq.shift()!;

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

    for (const [neighbor, weight] of graph.getNeighborsWithWeights(current)) {
      const newDist = currentDist + weight;
      if (newDist < distances.get(neighbor)!.distance) {
        distances.get(neighbor)!.distance= newDist;
        distances.get(neighbor)!.previous= current;
        pq.push([neighbor, newDist]);
      }
    }
  }

  return distances;
}

Topological Sort (DAG only)

function topologicalSort<T>(graph: Graph<T>): T[] {
  const visited = new Set<T>();
  const result: T[] = [];

  function dfs(vertex: T): void {
    visited.add(vertex);
    for (const neighbor of graph.getNeighbors(vertex)) {
      if (!visited.has(neighbor)) {
        dfs(neighbor);
      }
    }
    result.unshift(vertex); // Add to front
  }

  for (const vertex of graph.getVertices()) {
    if (!visited.has(vertex)) {
      dfs(vertex);
    }
  }

  return result;
}

// Kahn's Algorithm (BFS-based)
function topologicalSortKahn<T>(graph: Graph<T>): T[] {
  const inDegree = new Map<T, number>();
  const result: T[] = [];

  // Calculate in-degrees
  for (const vertex of graph.getVertices()) {
    inDegree.set(vertex, 0);
  }
  for (const vertex of graph.getVertices()) {
    for (const neighbor of graph.getNeighbors(vertex)) {
      inDegree.set(neighbor, (inDegree.get(neighbor) || 0) + 1);
    }
  }

  // Start with zero in-degree vertices
  const queue: T[] = [];
  for (const [vertex, degree] of inDegree) {
    if (degree === 0) queue.push(vertex);
  }

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

    for (const neighbor of graph.getNeighbors(vertex)) {
      inDegree.set(neighbor, inDegree.get(neighbor)! - 1);
      if (inDegree.get(neighbor) === 0) {
        queue.push(neighbor);
      }
    }
  }

  // Check for cycle
  if (result.length !== graph.getVertices().length) {
    throw new Error('Graph has a cycle');
  }

  return result;
}

Cycle Detection

// Undirected graph - using DFS
function hasCycleUndirected<T>(graph: Graph<T>): boolean {
  const visited = new Set<T>();

  function dfs(vertex: T, parent: T | null): boolean {
    visited.add(vertex);

    for (const neighbor of graph.getNeighbors(vertex)) {
      if (!visited.has(neighbor)) {
        if (dfs(neighbor, vertex)) return true;
      } else if (neighbor !== parent) {
        return true; // Back edge found
      }
    }

    return false;
  }

  for (const vertex of graph.getVertices()) {
    if (!visited.has(vertex)) {
      if (dfs(vertex, null)) return true;
    }
  }

  return false;
}

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

  for (const vertex of graph.getVertices()) {
    color.set(vertex, WHITE);
  }

  function dfs(vertex: T): boolean {
    color.set(vertex, GRAY);

    for (const neighbor of graph.getNeighbors(vertex)) {
      if (color.get(neighbor) === GRAY) {
        return true; // Back edge
      }
      if (color.get(neighbor) === WHITE && dfs(neighbor)) {
        return true;
      }
    }

    color.set(vertex, BLACK);
    return false;
  }

  for (const vertex of graph.getVertices()) {
    if (color.get(vertex) === WHITE && dfs(vertex)) {
      return true;
    }
  }

  return false;
}

Connected Components

function findConnectedComponents<T>(graph: Graph<T>): T[][] {
  const visited = new Set<T>();
  const components: T[][] = [];

  function dfs(vertex: T, component: T[]): void {
    visited.add(vertex);
    component.push(vertex);

    for (const neighbor of graph.getNeighbors(vertex)) {
      if (!visited.has(neighbor)) {
        dfs(neighbor, component);
      }
    }
  }

  for (const vertex of graph.getVertices()) {
    if (!visited.has(vertex)) {
      const component: T[] = [];
      dfs(vertex, component);
      components.push(component);
    }
  }

  return components;
}

Shortest Path (Unweighted)

function shortestPath<T>(graph: Graph<T>, start: T, end: T): T[] | null {
  const visited = new Set<T>();
  const parent = new Map<T, T | null>();
  const queue: T[] = [start];

  visited.add(start);
  parent.set(start, null);

  while (queue.length > 0) {
    const vertex = queue.shift()!;

    if (vertex === end) {
      // Reconstruct path
      const path: T[] = [];
      let current: T | null = end;
      while (current !== null) {
        path.unshift(current);
        current = parent.get(current) || null;
      }
      return path;
    }

    for (const neighbor of graph.getNeighbors(vertex)) {
      if (!visited.has(neighbor)) {
        visited.add(neighbor);
        parent.set(neighbor, vertex);
        queue.push(neighbor);
      }
    }
  }

  return null; // No path found
}

Minimum Spanning Tree

Prim's Algorithm

function primMST<T>(graph: WeightedGraph<T>): [T, T, number][] {
  const vertices = graph.getVertices();
  if (vertices.length === 0) return [];

  const mst: [T, T, number][] = [];
  const visited = new Set<T>();
  const pq: [T, T, number][] = []; // [from, to, weight]

  const start = vertices[0];
  visited.add(start);

  // Add edges from start
  for (const [neighbor, weight] of graph.getNeighborsWithWeights(start)) {
    pq.push([start, neighbor, weight]);
  }

  while (pq.length > 0 && visited.size < vertices.length) {
    pq.sort((a, b)=> a[2] - b[2]);
    const [from, to, weight]= pq.shift()!;

    if (visited.has(to)) continue;

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

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

  return mst;
}

Algorithm Complexity Summary

AlgorithmTimeSpace
BFSO(V + E)O(V)
DFSO(V + E)O(V)
DijkstraO((V + E) log V)O(V)
Bellman-FordO(V × E)O(V)
Floyd-WarshallO(V³)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 Different Representations

Adjacency List:

  • Sparse graphs (E << V²)
  • Need to iterate over neighbors
  • Memory is constrained

Adjacency Matrix:

  • Dense graphs (E ≈ V²)
  • Need O(1) edge lookup
  • Graph is small