Skip to main content

Graph

A non-linear data structure consisting of vertices (nodes) connected by edges. Graphs model relationships and networks, making them essential for solving real-world problems.

Overview

Graphs represent pairwise relationships between objects. This implementation uses an adjacency list representation, which is space-efficient for sparse graphs.

Graph Types

  • Directed Graph: Edges have direction (A → B doesn’t imply B → A)
  • Undirected Graph: Edges are bidirectional (A — B means both A → B and B → A)

Implementation

Vertex Class

class Vertex<T = number> {
  value: T;
  edges: Vertex<T>[] = [];

  constructor(value: T) {
    this.value = value;
  }
}
Each vertex stores its value and a list of adjacent vertices (edges). Reference: vertex.ts:1-9

Graph Class

class Graph<T = number> {
  directed: boolean;
  vertices: Vertex<T>[] = [];

  constructor(directed = false) {
    this.directed = directed;
  }
  
  // ... methods
}
Reference: graph.ts:3-10
Adjacency List: Each vertex maintains an array of its adjacent vertices. This representation is efficient for sparse graphs (few edges relative to vertices).

API Reference

constructor(directed: boolean = false)

Creates a new graph.
// Undirected graph
const graph = new Graph<number>();

// Directed graph
const directedGraph = new Graph<number>(true);
Parameters:
  • directed: Whether edges are directional (default: false)

addVertex(value: T): Vertex<T>

Adds a vertex to the graph.
const graph = new Graph<number>();
const v1 = graph.addVertex(1);
const v2 = graph.addVertex(2);
const v3 = graph.addVertex(3);
Returns: The newly created vertex Complexity: O(1) Reference: graph.ts:12-17

addEdge(value1: T, value2: T): void

Adds an edge between two vertices. Creates vertices if they don’t exist.
const graph = new Graph<number>();
graph.addEdge(1, 2);
graph.addEdge(1, 3);
graph.addEdge(2, 3);

// Undirected graph:
// 1 -- 2, 3
// 2 -- 1, 3  
// 3 -- 1, 2
Parameters:
  • value1: First vertex value
  • value2: Second vertex value
Behavior:
  • For undirected graphs: Creates bidirectional edge (A ↔ B)
  • For directed graphs: Creates unidirectional edge (A → B)
Complexity: O(V) - needs to find vertices Reference: graph.ts:23-40

toString(): string

Returns a string representation of the graph.
const graph = new Graph<number>();
graph.addEdge(1, 2);
graph.addEdge(1, 3);
graph.addEdge(2, 3);

console.log(graph.toString());
// Output:
// 1 -- 2 3
// 2 -- 1 3
// 3 -- 1 2
For directed graphs, uses -> instead of --:
const directedGraph = new Graph<number>(true);
directedGraph.addEdge(1, 2);
directedGraph.addEdge(2, 3);

console.log(directedGraph.toString());
// Output:
// 1 -> 2
// 2 -> 3
// 3 ->
Complexity: O(V + E) Reference: graph.ts:42-56

Usage Examples

Undirected Graph (Social Network)

const socialNetwork = new Graph<string>();

// Add friendships (bidirectional)
socialNetwork.addEdge('Alice', 'Bob');
socialNetwork.addEdge('Alice', 'Charlie');
socialNetwork.addEdge('Bob', 'David');
socialNetwork.addEdge('Charlie', 'David');

console.log(socialNetwork.toString());
// Alice -- Bob Charlie
// Bob -- Alice David
// Charlie -- Alice David
// David -- Bob Charlie

Directed Graph (Web Pages)

const webGraph = new Graph<string>(true);

// Add hyperlinks (unidirectional)
webGraph.addEdge('home.html', 'about.html');
webGraph.addEdge('home.html', 'contact.html');
webGraph.addEdge('about.html', 'contact.html');
webGraph.addEdge('contact.html', 'home.html');

console.log(webGraph.toString());
// home.html -> about.html contact.html
// about.html -> contact.html
// contact.html -> home.html

Weighted Graph (Cities and Distances)

interface WeightedEdge {
  to: string;
  weight: number;
}

class WeightedGraph {
  private adjacencyList = new Map<string, WeightedEdge[]>();
  
  addEdge(from: string, to: string, weight: number) {
    if (!this.adjacencyList.has(from)) {
      this.adjacencyList.set(from, []);
    }
    this.adjacencyList.get(from)!.push({ to, weight });
  }
}

const cityGraph = new WeightedGraph();
cityGraph.addEdge('NYC', 'Boston', 215);
cityGraph.addEdge('NYC', 'Philadelphia', 95);
cityGraph.addEdge('Boston', 'Philadelphia', 310);

Graph Traversal

Depth-First Search (DFS)

Explores as far as possible along each branch before backtracking.
function dfs<T>(graph: Graph<T>, startValue: T): T[] {
  const visited = new Set<T>();
  const result: T[] = [];
  
  const vertex = graph.vertices.find(v => v.value === startValue);
  if (!vertex) return result;
  
  function traverse(vertex: Vertex<T>) {
    visited.add(vertex.value);
    result.push(vertex.value);
    
    for (const neighbor of vertex.edges) {
      if (!visited.has(neighbor.value)) {
        traverse(neighbor);
      }
    }
  }
  
  traverse(vertex);
  return result;
}

// Example:
const graph = new Graph<number>();
graph.addEdge(1, 2);
graph.addEdge(1, 3);
graph.addEdge(2, 4);
graph.addEdge(3, 4);

console.log(dfs(graph, 1)); // [1, 2, 4, 3]
Complexity: O(V + E) Use cases: Cycle detection, topological sorting, pathfinding

Breadth-First Search (BFS)

Explores all neighbors at the current depth before moving to the next level.
function bfs<T>(graph: Graph<T>, startValue: T): T[] {
  const visited = new Set<T>();
  const result: T[] = [];
  const queue: Vertex<T>[] = [];
  
  const vertex = graph.vertices.find(v => v.value === startValue);
  if (!vertex) return result;
  
  queue.push(vertex);
  visited.add(vertex.value);
  
  while (queue.length > 0) {
    const current = queue.shift()!;
    result.push(current.value);
    
    for (const neighbor of current.edges) {
      if (!visited.has(neighbor.value)) {
        visited.add(neighbor.value);
        queue.push(neighbor);
      }
    }
  }
  
  return result;
}

// Example:
console.log(bfs(graph, 1)); // [1, 2, 3, 4]
Complexity: O(V + E) Use cases: Shortest path in unweighted graphs, level-order traversal

Common Graph Algorithms

Has Path

Check if a path exists between two vertices.
function hasPath<T>(
  graph: Graph<T>,
  start: T,
  end: T,
  visited = new Set<T>()
): boolean {
  if (start === end) return true;
  if (visited.has(start)) return false;
  
  visited.add(start);
  
  const vertex = graph.vertices.find(v => v.value === start);
  if (!vertex) return false;
  
  for (const neighbor of vertex.edges) {
    if (hasPath(graph, neighbor.value, end, visited)) {
      return true;
    }
  }
  
  return false;
}

Find Connected Components

Find all connected components in an undirected graph.
function findConnectedComponents<T>(graph: Graph<T>): T[][] {
  const visited = new Set<T>();
  const components: T[][] = [];
  
  function dfs(vertex: Vertex<T>, component: T[]) {
    visited.add(vertex.value);
    component.push(vertex.value);
    
    for (const neighbor of vertex.edges) {
      if (!visited.has(neighbor.value)) {
        dfs(neighbor, component);
      }
    }
  }
  
  for (const vertex of graph.vertices) {
    if (!visited.has(vertex.value)) {
      const component: T[] = [];
      dfs(vertex, component);
      components.push(component);
    }
  }
  
  return components;
}

Detect Cycle (Undirected Graph)

function hasCycle<T>(graph: Graph<T>): boolean {
  const visited = new Set<T>();
  
  function dfs(vertex: Vertex<T>, parent: T | null): boolean {
    visited.add(vertex.value);
    
    for (const neighbor of vertex.edges) {
      if (!visited.has(neighbor.value)) {
        if (dfs(neighbor, vertex.value)) return true;
      } else if (neighbor.value !== parent) {
        return true; // Back edge found (cycle)
      }
    }
    
    return false;
  }
  
  for (const vertex of graph.vertices) {
    if (!visited.has(vertex.value)) {
      if (dfs(vertex, null)) return true;
    }
  }
  
  return false;
}

Topological Sort (Directed Acyclic Graph)

function topologicalSort<T>(graph: Graph<T>): T[] {
  const visited = new Set<T>();
  const stack: T[] = [];
  
  function dfs(vertex: Vertex<T>) {
    visited.add(vertex.value);
    
    for (const neighbor of vertex.edges) {
      if (!visited.has(neighbor.value)) {
        dfs(neighbor);
      }
    }
    
    stack.unshift(vertex.value); // Add to front
  }
  
  for (const vertex of graph.vertices) {
    if (!visited.has(vertex.value)) {
      dfs(vertex);
    }
  }
  
  return stack;
}

Graph Representations

Adjacency List (This Implementation)

// Graph: 1--2, 1--3, 2--3
{
  1: [2, 3],
  2: [1, 3],
  3: [1, 2]
}
Pros: Space-efficient O(V + E), fast neighbor iteration Cons: Slower edge existence check O(V)

Adjacency Matrix

// Graph: 1--2, 1--3, 2--3
[
  [0, 1, 1],  // Vertex 1
  [1, 0, 1],  // Vertex 2
  [1, 1, 0]   // Vertex 3
]
Pros: O(1) edge existence check, simple representation Cons: O(V²) space, slow to iterate neighbors

Edge List

// Graph: 1--2, 1--3, 2--3
[
  { from: 1, to: 2 },
  { from: 1, to: 3 },
  { from: 2, to: 3 }
]
Pros: Simple, compact for sparse graphs Cons: Slow for most operations

Complexity Summary

OperationAdjacency ListAdjacency Matrix
Add vertexO(1)O(V²)
Add edgeO(1)O(1)
Remove vertexO(E)O(V²)
Remove edgeO(E)O(1)
Check if edge existsO(V)O(1)
Find neighborsO(1)O(V)
SpaceO(V + E)O(V²)
This implementation uses adjacency lists, which are optimal for sparse graphs (E << V²).

Real-World Applications

Users are vertices, friendships are edges. Find mutual friends, suggest connections, detect communities.
Web pages are vertices, hyperlinks are directed edges. PageRank algorithm, sitemap generation.
Packages/tasks are vertices, dependencies are directed edges. Topological sort for build order.
Users/items are vertices, interactions are edges. Collaborative filtering, similar items.
Nodes are vertices, connections are weighted edges. Max flow, min cut problems.

Graph Properties

Degree

function getDegree<T>(graph: Graph<T>, value: T): number {
  const vertex = graph.vertices.find(v => v.value === value);
  return vertex ? vertex.edges.length : 0;
}
In-degree (directed): Number of incoming edges Out-degree (directed): Number of outgoing edges Degree (undirected): Number of edges

Density

function getDensity<T>(graph: Graph<T>): number {
  const V = graph.vertices.length;
  let E = 0;
  
  for (const vertex of graph.vertices) {
    E += vertex.edges.length;
  }
  
  // For undirected graphs, each edge is counted twice
  if (!graph.directed) {
    E = E / 2;
  }
  
  const maxEdges = graph.directed ? V * (V - 1) : (V * (V - 1)) / 2;
  return E / maxEdges;
}
Sparse graph: E ≈ V (density ≈ 0) Dense graph: E ≈ V² (density ≈ 1)

Key Characteristics

Advantages:
  • Models complex relationships naturally
  • Flexible structure (add/remove vertices and edges)
  • Powerful algorithms for analysis
  • Adjacency list is space-efficient
Limitations:
  • Can be complex to implement and reason about
  • Some operations are expensive (finding edge, removing vertex)
  • Memory overhead for storing edges
  • No random access to elements

When to Use Graphs

Use graphs when:
  • Modeling relationships between entities
  • Finding paths or connections
  • Analyzing networks
  • Solving optimization problems
  • Working with hierarchies that can have cycles
Graph Choice Guide:
  • Sparse graph (few edges): Use adjacency list
  • Dense graph (many edges): Use adjacency matrix
  • Simple queries: Use edge list
  • Weighted edges: Add weight field to edge objects

Tree

Special case: connected acyclic graph

Binary Heap

Used in graph algorithms (Dijkstra, Prim)

Build docs developers (and LLMs) love