DSAGraph Representations

Graph Representations

Once you understand what a graph is, the next question is: how do you store it in memory? The choice of representation affects the time and space complexity of every graph algorithm you run on it. There are three primary representations: adjacency matrix, adjacency list, and edge list.

Note
For a graph with V vertices and E edges: use an adjacency matrix for dense graphs where fast edge lookup matters, use an adjacency list for sparse graphs and most standard algorithms, and use an edge list when you need to sort or iterate over edges directly.
The Sample Graph

Throughout this page we will use the same example graph so you can compare all three representations side by side.

Undirected, unweighted graph — 5 vertices, 6 edges:

     (0)
    /   \
  (1)   (2)
  / \     \
(3) (4)---(2)

  Edges: 0-1, 0-2, 1-3, 1-4, 2-4, 3-4
  Vertices: 0, 1, 2, 3, 4
Adjacency Matrix

An adjacency matrix is a V×V 2D array where matrix[i][j] = 1 (or the edge weight) if there is an edge from vertex i to vertex j, and 0 otherwise. For an undirected graph the matrix is symmetric: matrix[i][j] === matrix[j][i].

Adjacency matrix for our sample graph:

       0  1  2  3  4
  0  [ 0, 1, 1, 0, 0 ]
  1  [ 1, 0, 0, 1, 1 ]
  2  [ 1, 0, 0, 0, 1 ]
  3  [ 0, 1, 0, 0, 1 ]
  4  [ 0, 1, 1, 1, 0 ]

  matrix[0][1] = 1 → edge 0-1 exists ✓
  matrix[0][3] = 0 → edge 0-3 does NOT exist ✓
  Symmetric because the graph is undirected

JS
// Adjacency Matrix — JavaScript implementation

class AdjacencyMatrix {
  constructor(numVertices) {
    this.V = numVertices;
    // Create V×V matrix filled with zeros
    this.matrix = Array.from({ length: numVertices }, () =>
      new Array(numVertices).fill(0)
    );
  }

  // Add an undirected edge between u and v
  addEdge(u, v, weight = 1) {
    this.matrix[u][v] = weight;
    this.matrix[v][u] = weight; // remove for directed graphs
  }

  // Remove an edge
  removeEdge(u, v) {
    this.matrix[u][v] = 0;
    this.matrix[v][u] = 0;
  }

  // O(1) edge check — the key advantage of a matrix
  hasEdge(u, v) {
    return this.matrix[u][v] !== 0;
  }

  // Get all neighbors of vertex u — O(V) even if few neighbors
  getNeighbors(u) {
    const neighbors = [];
    for (let v = 0; v < this.V; v++) {
      if (this.matrix[u][v] !== 0) {
        neighbors.push(v);
      }
    }
    return neighbors;
  }

  print() {
    console.log("Adjacency Matrix:");
    for (let i = 0; i < this.V; i++) {
      console.log(`${i}: [${this.matrix[i].join(", ")}]`);
    }
  }
}

// Build our sample graph
const g = new AdjacencyMatrix(5);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(1, 4);
g.addEdge(2, 4);
g.addEdge(3, 4);

g.print();
// 0: [0, 1, 1, 0, 0]
// 1: [1, 0, 0, 1, 1]
// 2: [1, 0, 0, 0, 1]
// 3: [0, 1, 0, 0, 1]
// 4: [0, 1, 1, 1, 0]

console.log(g.hasEdge(0, 1)); // true  — O(1)
console.log(g.hasEdge(0, 3)); // false — O(1)
console.log(g.getNeighbors(1)); // [0, 3, 4]

Property

Value

Space complexity

O(V²) — always allocates V×V cells

Add edge

O(1)

Remove edge

O(1)

Check if edge exists

O(1)

Iterate neighbors of vertex u

O(V) — must scan entire row

Iterate all edges

O(V²)

Tip
Use an adjacency matrix when: your graph is dense (E ≈ V²), you need constant-time edge lookups, or you are implementing Floyd-Warshall (all-pairs shortest paths).
Warning
The O(V²) space cost is painful. A graph with 1,000,000 vertices would need a 1,000,000 × 1,000,000 matrix — one trillion cells. Most real graphs are sparse, making the adjacency list the default choice.
Adjacency List

An adjacency list stores, for each vertex, only the list of vertices it is directly connected to. The total space used is O(V + E) — proportional to the actual number of edges, not V². This is the most commonly used representation in practice and in interviews.

Adjacency list for our sample graph:

  0 → [1, 2]
  1 → [0, 3, 4]
  2 → [0, 4]
  3 → [1, 4]
  4 → [1, 2, 3]

  Only stores edges that actually exist — no wasted space
  Total entries = 2 × E = 2 × 6 = 12 (each undirected edge stored twice)

JS
// Adjacency List — JavaScript implementation (Map of arrays)

class AdjacencyList {
  constructor() {
    this.list = new Map(); // vertex → array of neighbors
  }

  // Ensure vertex exists in the list
  addVertex(v) {
    if (!this.list.has(v)) {
      this.list.set(v, []);
    }
  }

  // Add undirected edge
  addEdge(u, v) {
    this.addVertex(u);
    this.addVertex(v);
    this.list.get(u).push(v);
    this.list.get(v).push(u); // remove for directed graphs
  }

  // Remove an edge — O(degree) to find and splice
  removeEdge(u, v) {
    this.list.set(u, this.list.get(u).filter(n => n !== v));
    this.list.set(v, this.list.get(v).filter(n => n !== u));
  }

  // Check if edge exists — O(degree of u)
  hasEdge(u, v) {
    return this.list.has(u) && this.list.get(u).includes(v);
  }

  // Get neighbors — O(1) to return the array
  getNeighbors(v) {
    return this.list.get(v) || [];
  }

  print() {
    for (const [vertex, neighbors] of this.list) {
      console.log(`${vertex} → [${neighbors.join(", ")}]`);
    }
  }
}

// Build our sample graph
const g = new AdjacencyList();
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(1, 4);
g.addEdge(2, 4);
g.addEdge(3, 4);

g.print();
// 0 → [1, 2]
// 1 → [0, 3, 4]
// 2 → [0, 4]
// 3 → [1, 4]
// 4 → [1, 2, 3]

console.log(g.hasEdge(0, 1)); // true
console.log(g.hasEdge(0, 3)); // false
console.log(g.getNeighbors(1)); // [0, 3, 4]

Property

Value

Space complexity

O(V + E) — only stores existing edges

Add edge

O(1) amortized

Remove edge

O(degree) to find and remove

Check if edge exists

O(degree) — scan neighbor list

Iterate neighbors of vertex u

O(degree of u) — very fast

Iterate all edges

O(V + E)

Tip
The adjacency list is the go-to representation for BFS, DFS, Dijkstra's, and topological sort. Its O(V + E) traversal matches the natural complexity of these algorithms perfectly.
Edge List

An edge list is the simplest representation: a plain array of edges, where each edge is a pair (or triple for weighted graphs) [u, v] or [u, v, weight]. There is no per-vertex structure — just a flat list of all connections.

Edge list for our sample graph:

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

  Simple, compact, easy to sort by weight
  Perfect for Kruskal's minimum spanning tree algorithm

JS
// Edge List — JavaScript implementation

class EdgeList {
  constructor() {
    this.edges = []; // array of [u, v] or [u, v, weight]
    this.vertices = new Set();
  }

  addEdge(u, v, weight = 1) {
    this.vertices.add(u);
    this.vertices.add(v);
    this.edges.push([u, v, weight]);
    // For undirected graphs some algorithms expect both directions:
    // this.edges.push([v, u, weight]);
  }

  // O(E) — must scan all edges
  hasEdge(u, v) {
    return this.edges.some(([a, b]) => a === u && b === v);
  }

  // O(E) — must scan all edges
  getNeighbors(u) {
    return this.edges
      .filter(([a]) => a === u)
      .map(([, b]) => b);
  }

  // Sort edges by weight — key operation for Kruskal's MST
  sortByWeight() {
    this.edges.sort((a, b) => a[2] - b[2]);
  }

  print() {
    console.log("Edge List:");
    for (const [u, v, w] of this.edges) {
      console.log(`  ${u} --(${w})-- ${v}`);
    }
  }
}

// Build sample graph
const g = new EdgeList();
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(1, 4);
g.addEdge(2, 4);
g.addEdge(3, 4);

g.print();
// 0 --(1)-- 1
// 0 --(1)-- 2
// 1 --(1)-- 3
// 1 --(1)-- 4
// 2 --(1)-- 4
// 3 --(1)-- 4

Property

Value

Space complexity

O(E) — one entry per edge

Add edge

O(1)

Remove edge

O(E) — search then remove

Check if edge exists

O(E) — linear scan

Iterate neighbors of vertex u

O(E) — scan all edges

Iterate all edges

O(E)

Sort by weight

O(E log E) — just sort the array

Comparison Table

Operation

Adjacency Matrix

Adjacency List

Edge List

Space

O(V²)

O(V + E)

O(E)

Add edge

O(1)

O(1)

O(1)

Remove edge

O(1)

O(degree)

O(E)

Check edge exists

O(1)

O(degree)

O(E)

Iterate neighbors

O(V)

O(degree)

O(E)

Iterate all edges

O(V²)

O(V + E)

O(E)

Best for

Dense graphs, Floyd-Warshall

BFS, DFS, Dijkstra

Kruskal's, edge sorting

When to Use Each Representation

Representation

Use When

Adjacency Matrix

Dense graph (E ≈ V²), constant-time edge lookup needed, graph is small (few thousand vertices), Floyd-Warshall algorithm

Adjacency List

Sparse graph (most real-world graphs), BFS/DFS traversal, Dijkstra's shortest path, Prim's MST, topological sort

Edge List

Kruskal's MST (needs edges sorted by weight), processing edges independently, simple serialization/import, small utility scripts

Weighted Graph Representations

Adding weights requires only small changes to each representation.

Weighted directed graph example:

     4       2
  (0)───►(1)───►(3)
   │      │
  7│     3│
   ▼      ▼
  (2)◄───(4)
       5

  Edges: 0→1 (weight 4), 0→2 (weight 7),
         1→3 (weight 2), 1→4 (weight 3),
         4→2 (weight 5)

JS
// === Weighted Adjacency Matrix ===
// Use the weight value instead of 1; use Infinity for no edge

const V = 5;
const INF = Infinity;
const weightedMatrix = [
  //  0    1    2    3    4
  [   0,   4,   7, INF, INF],  // 0
  [ INF,   0, INF,   2,   3],  // 1
  [ INF, INF,   0, INF, INF],  // 2
  [ INF, INF, INF,   0, INF],  // 3
  [ INF, INF,   5, INF,   0],  // 4
];

console.log(weightedMatrix[0][1]); // 4  — weight of edge 0→1
console.log(weightedMatrix[0][3]); // Infinity — no direct edge


// === Weighted Adjacency List ===
// Store objects { node, weight } instead of plain node numbers

class WeightedAdjList {
  constructor() {
    this.list = new Map();
  }

  addVertex(v) {
    if (!this.list.has(v)) this.list.set(v, []);
  }

  // Directed weighted edge
  addEdge(u, v, weight) {
    this.addVertex(u);
    this.addVertex(v);
    this.list.get(u).push({ node: v, weight });
    // For undirected: also push { node: u, weight } to v's list
  }

  getNeighbors(v) {
    return this.list.get(v) || [];
  }
}

const wg = new WeightedAdjList();
wg.addEdge(0, 1, 4);
wg.addEdge(0, 2, 7);
wg.addEdge(1, 3, 2);
wg.addEdge(1, 4, 3);
wg.addEdge(4, 2, 5);

console.log(wg.getNeighbors(1));
// [{ node: 3, weight: 2 }, { node: 4, weight: 3 }]


// === Weighted Edge List ===
// Each entry is a [from, to, weight] triple

const weightedEdges = [
  [0, 1, 4],
  [0, 2, 7],
  [1, 3, 2],
  [1, 4, 3],
  [4, 2, 5],
];

// Sort by weight (e.g. for Kruskal's)
weightedEdges.sort((a, b) => a[2] - b[2]);
// [[1,3,2], [1,4,3], [0,1,4], [4,2,5], [0,2,7]]
Full Graph Class with All Operations

Here is a production-style graph class using an adjacency list — the representation you will reach for in 90% of interview problems and real projects.

JS
class Graph {
  constructor(directed = false) {
    this.directed = directed;
    this.adjacencyList = new Map();
  }

  addVertex(v) {
    if (!this.adjacencyList.has(v)) {
      this.adjacencyList.set(v, new Map()); // neighbor → weight
    }
    return this;
  }

  addEdge(u, v, weight = 1) {
    this.addVertex(u);
    this.addVertex(v);
    this.adjacencyList.get(u).set(v, weight);
    if (!this.directed) {
      this.adjacencyList.get(v).set(u, weight);
    }
    return this;
  }

  removeEdge(u, v) {
    if (this.adjacencyList.has(u)) {
      this.adjacencyList.get(u).delete(v);
    }
    if (!this.directed && this.adjacencyList.has(v)) {
      this.adjacencyList.get(v).delete(u);
    }
    return this;
  }

  removeVertex(v) {
    if (!this.adjacencyList.has(v)) return this;
    // Remove all edges pointing to v
    for (const [, neighbors] of this.adjacencyList) {
      neighbors.delete(v);
    }
    this.adjacencyList.delete(v);
    return this;
  }

  hasEdge(u, v) {
    return (
      this.adjacencyList.has(u) &&
      this.adjacencyList.get(u).has(v)
    );
  }

  getNeighbors(v) {
    if (!this.adjacencyList.has(v)) return [];
    return [...this.adjacencyList.get(v).keys()];
  }

  getWeight(u, v) {
    if (!this.hasEdge(u, v)) return Infinity;
    return this.adjacencyList.get(u).get(v);
  }

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

  getEdges() {
    const edges = [];
    for (const [u, neighbors] of this.adjacencyList) {
      for (const [v, weight] of neighbors) {
        if (this.directed || u <= v) { // avoid duplicates for undirected
          edges.push([u, v, weight]);
        }
      }
    }
    return edges;
  }

  // BFS — returns visit order
  bfs(start) {
    if (!this.adjacencyList.has(start)) return [];
    const visited = new Set([start]);
    const queue = [start];
    const order = [];

    while (queue.length > 0) {
      const node = queue.shift();
      order.push(node);
      for (const neighbor of this.getNeighbors(node)) {
        if (!visited.has(neighbor)) {
          visited.add(neighbor);
          queue.push(neighbor);
        }
      }
    }
    return order;
  }

  // DFS — returns visit order
  dfs(start, visited = new Set(), order = []) {
    if (!this.adjacencyList.has(start)) return order;
    visited.add(start);
    order.push(start);
    for (const neighbor of this.getNeighbors(start)) {
      if (!visited.has(neighbor)) {
        this.dfs(neighbor, visited, order);
      }
    }
    return order;
  }

  print() {
    for (const [v, neighbors] of this.adjacencyList) {
      const edges = [...neighbors.entries()]
        .map(([n, w]) => `${n}(w:${w})`)
        .join(", ");
      console.log(`${v} → ${edges || "(no neighbors)"}`);
    }
  }
}

// Usage
const g = new Graph(false); // undirected
g.addEdge("A", "B", 4)
 .addEdge("A", "C", 2)
 .addEdge("B", "C", 1)
 .addEdge("B", "D", 5)
 .addEdge("C", "D", 8)
 .addEdge("D", "E", 2);

g.print();
// A → B(w:4), C(w:2)
// B → A(w:4), C(w:1), D(w:5)
// C → A(w:2), B(w:1), D(w:8)
// D → B(w:5), C(w:8), E(w:2)
// E → D(w:2)

console.log(g.bfs("A"));  // ["A", "B", "C", "D", "E"]
console.log(g.dfs("A"));  // ["A", "B", "C", "D", "E"]
console.log(g.hasEdge("A", "D")); // false
console.log(g.getWeight("A", "B")); // 4
console.log(g.getEdges());
// [["A","B",4], ["A","C",2], ["B","C",1], ["B","D",5], ["C","D",8], ["D","E",2]]
Converting Between Representations

JS
// Convert adjacency list → adjacency matrix
function listToMatrix(adjList, numVertices) {
  const matrix = Array.from({ length: numVertices }, () =>
    new Array(numVertices).fill(0)
  );
  for (const [u, neighbors] of adjList) {
    for (const [v, weight] of neighbors) {
      matrix[u][v] = weight;
    }
  }
  return matrix;
}

// Convert adjacency matrix → adjacency list
function matrixToList(matrix) {
  const list = new Map();
  for (let u = 0; u < matrix.length; u++) {
    list.set(u, new Map());
    for (let v = 0; v < matrix[u].length; v++) {
      if (matrix[u][v] !== 0 && matrix[u][v] !== Infinity) {
        list.get(u).set(v, matrix[u][v]);
      }
    }
  }
  return list;
}

// Convert edge list → adjacency list
function edgesToList(edges, directed = false) {
  const list = new Map();
  for (const [u, v, weight = 1] of edges) {
    if (!list.has(u)) list.set(u, new Map());
    if (!list.has(v)) list.set(v, new Map());
    list.get(u).set(v, weight);
    if (!directed) list.get(v).set(u, weight);
  }
  return list;
}

// Convert adjacency list → edge list
function listToEdges(adjList, directed = false) {
  const edges = [];
  for (const [u, neighbors] of adjList) {
    for (const [v, weight] of neighbors) {
      if (directed || u <= v) { // skip duplicates for undirected
        edges.push([u, v, weight]);
      }
    }
  }
  return edges;
}
Choosing the Right Representation
  1. Start with adjacency list — it works for 90% of graph problems and interviews.

  2. Switch to adjacency matrix only when you need O(1) edge lookup or the graph is provably dense.

  3. Use edge list when the algorithm itself operates on edges (Kruskal's MST, offline sorting).

  4. For graphs with string or non-integer vertex names, use a Map<string, Map<string, number>> adjacency list.

  5. In competitive programming with integer vertices 0..V-1, a plain array of arrays (array[V]) is the fastest adjacency list.

Note
Most LeetCode and interview graph problems give you edges as an input array like `[[0,1],[1,2],[0,2]]`. Your first step is almost always to convert this into an adjacency list before running BFS, DFS, or Dijkstra.
Practice Problems
  • LeetCode 133 — Clone Graph: build adjacency list, then BFS/DFS to clone

  • LeetCode 207 — Course Schedule: build directed graph, detect cycle

  • LeetCode 743 — Network Delay Time: weighted adjacency list + Dijkstra

  • LeetCode 1584 — Min Cost to Connect All Points: edge list + Kruskal's

  • LeetCode 997 — Find the Town Judge: in-degree / out-degree tracking

  • LeetCode 310 — Minimum Height Trees: adjacency list + iterative pruning