Graph Adjacency List Explained

Learn Graph Adjacency List Explained with practical examples, explanations, best practices, and real-world coding techniques.

What Is an Adjacency List?

An adjacency list is a graph representation that stores the neighbors of each vertex. Instead of allocating space for every possible pair of vertices, an adjacency list records only the edges that actually exist.

This representation is particularly useful for sparse graphs, where the number of edges is much smaller than the maximum possible number of edges.

Basic Example

Consider a graph containing four vertices with the following connections:

A --- B
|     |
C --- D

Its adjacency list can be written as:

A: [B, C]
B: [A, D]
C: [A, D]
D: [B, C]

Each vertex has a list containing the vertices directly connected to it.

Why Use an Adjacency List?

A graph with V vertices can potentially contain many more possible vertex-to-vertex relationships than the graph actually uses. An adjacency matrix allocates space for all possible pairs, while an adjacency list stores the existing relationships.

For a graph with V vertices and E edges, an adjacency list typically requires O(V + E) space.

Adjacency List for an Undirected Graph

In an undirected graph, an edge connects two vertices in both directions. Therefore, each edge is normally stored in the adjacency list of both vertices.

A --- B
|
C

The adjacency list is:

A: [B, C]
B: [A]
C: [A]

The edge A-B appears under both A and B because the connection can be traversed in either direction.

Adjacency List for a Directed Graph

In a directed graph, edges have a source and destination. The destination is normally stored in the source vertex's adjacency list.

A ---> B
B ---> C
C ---> A

The adjacency list is:

A: [B]
B: [C]
C: [A]

Unlike an undirected graph, A → B does not automatically create B → A.

Creating an Adjacency List in JavaScript

JavaScript objects and Map objects can both be used to implement adjacency lists. A Map is often convenient when vertex identifiers are not limited to simple object property keys.

const graph = new Map();

graph.set('A', ['B', 'C']);
graph.set('B', ['A', 'D']);
graph.set('C', ['A']);
graph.set('D', ['B']);

console.log(graph.get('A'));
console.log(graph.get('B'));

Building an Adjacency List Dynamically

Instead of manually creating every neighbor list, an edge function can construct the graph as edges are added.

const graph = {};

function addEdge(graph, a, b) {
  if (!graph[a]) {
    graph[a] = [];
  }

  if (!graph[b]) {
    graph[b] = [];
  }

  graph[a].push(b);
  graph[b].push(a);
}

addEdge(graph, 'A', 'B');
addEdge(graph, 'A', 'C');
addEdge(graph, 'B', 'D');

console.log(graph);

This implementation creates an undirected graph because each edge is stored in both directions.

Adding a Directed Edge

For a directed graph, only the source vertex needs to receive the destination in its adjacency list.

const graph = {};

function addDirectedEdge(graph, from, to) {
  if (!graph[from]) {
    graph[from] = [];
  }

  if (!graph[to]) {
    graph[to] = [];
  }

  graph[from].push(to);
}

addDirectedEdge(graph, 'A', 'B');
addDirectedEdge(graph, 'B', 'C');

console.log(graph);

Weighted Adjacency List

An adjacency list can also represent a weighted graph. Instead of storing only the neighboring vertex, each entry can contain the destination and edge weight.

const graph = {
  A: [
    { node: 'B', weight: 4 },
    { node: 'C', weight: 10 }
  ],
  B: [
    { node: 'A', weight: 4 },
    { node: 'C', weight: 3 }
  ],
  C: [
    { node: 'A', weight: 10 },
    { node: 'B', weight: 3 }
  ]
};

This representation preserves both pieces of information required by a weighted graph: the neighboring vertex and the cost of reaching it.

Adding Weighted Edges

For an undirected weighted graph, the same weighted connection is normally stored for both vertices.

const graph = {};

function addWeightedEdge(graph, a, b, weight) {
  if (!graph[a]) {
    graph[a] = [];
  }

  if (!graph[b]) {
    graph[b] = [];
  }

  graph[a].push({ node: b, weight });
  graph[b].push({ node: a, weight });
}

addWeightedEdge(graph, 'A', 'B', 5);
addWeightedEdge(graph, 'B', 'C', 3);

console.log(graph);

Finding Neighbors

One of the main advantages of an adjacency list is that a vertex's neighbors can be accessed directly.

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

console.log(graph.A);
// ['B', 'C']

If vertex A has degree d, iterating through its neighbors takes O(d) time.

Checking Whether an Edge Exists

To check whether an edge exists using a basic adjacency list, you normally search the neighbor list of the source vertex.

function hasEdge(graph, from, to) {
  return graph[from]?.includes(to) ?? false;
}

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

console.log(hasEdge(graph, 'A', 'B')); // true
console.log(hasEdge(graph, 'A', 'D')); // false

With a normal array-based neighbor list, the lookup is generally O(degree) rather than O(1). A different structure, such as a Set, can provide faster average membership checks.

BFS with an Adjacency List

Breadth-First Search is particularly natural with adjacency lists because the algorithm needs to visit the neighbors of each vertex.

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

  visited.add(start);

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

    console.log(current);

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

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

bfs(graph, 'A');

With a suitable queue implementation, BFS using an adjacency list runs in O(V + E) time because each vertex and edge is processed a bounded number of times.

DFS with an Adjacency List

Depth-First Search also works efficiently with adjacency lists.

function dfs(graph, vertex, visited = new Set()) {
  if (visited.has(vertex)) {
    return;
  }

  visited.add(vertex);
  console.log(vertex);

  for (const neighbor of graph[vertex]) {
    dfs(graph, neighbor, visited);
  }
}

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

dfs(graph, 'A');

DFS also takes O(V + E) time for an adjacency-list representation when traversing the reachable portion of the graph.

Space Complexity

An adjacency list stores the vertices and the edges that actually exist. Therefore, its typical space complexity is:

Space: O(V + E)

For an undirected graph, each edge is commonly stored twice, once for each endpoint. This changes the constant amount of storage but not the asymptotic complexity.

Time Complexity of Common Operations

Operation Typical Complexity Explanation
Access neighbors O(1) to access the list, O(degree) to scan it Depends on the number of neighbors
Add edge O(1) amortized Append to the relevant list
Remove edge O(degree) The neighbor may need to be searched
Check edge O(degree) Array-based lists are searched for the neighbor
BFS O(V + E) Vertices and edges are traversed
DFS O(V + E) Vertices and edges are traversed
Space O(V + E) Stores vertices and existing edges

Adjacency List vs Adjacency Matrix

Both representations are widely used, but they make different trade-offs.

Feature Adjacency List Adjacency Matrix
Space O(V + E) O(V²)
Find neighbors O(degree) O(V)
Check direct edge Typically O(degree) O(1)
Sparse graphs Space efficient Can waste space
Dense graphs Still usable Often practical

Using Sets for Neighbor Lists

If an application frequently checks whether a particular neighbor exists, a Set can be used instead of an array for each adjacency list.

const graph = new Map();

graph.set('A', new Set(['B', 'C']));
graph.set('B', new Set(['A']));
graph.set('C', new Set(['A']));

console.log(graph.get('A').has('B')); // true
console.log(graph.get('A').has('D')); // false

This can provide average-case constant-time membership checks, while arrays are often simpler when traversal order and direct iteration are the primary concerns.

Representing Weighted Graphs with Objects

For algorithms such as Dijkstra's algorithm, each adjacency-list entry commonly stores both a neighbor and its edge weight.

const graph = {
  A: [
    { node: 'B', weight: 2 },
    { node: 'C', weight: 7 }
  ],
  B: [
    { node: 'C', weight: 3 }
  ],
  C: []
};

for (const edge of graph.A) {
  console.log(
    'Neighbor:',
    edge.node,
    'Weight:',
    edge.weight
  );
}

Practical Example: Network Connections

Imagine a network where computers are vertices and network connections are edges. An adjacency list can store only the connections that actually exist.

const network = {
  ServerA: ['ServerB', 'ServerC'],
  ServerB: ['ServerA', 'ServerD'],
  ServerC: ['ServerA'],
  ServerD: ['ServerB']
};

If ServerA has connections to ServerB and ServerC, only those relationships need to be stored under ServerA. There is no need to allocate entries for every other possible server pair.

When Should You Use an Adjacency List?

  • The graph is sparse.
  • You frequently need to iterate through the neighbors of a vertex.
  • The graph contains a large number of vertices but relatively few edges.
  • You are implementing BFS, DFS, connected components, or many graph algorithms.
  • Memory efficiency is important.
  • You need to represent weighted edges without allocating space for nonexistent edges.

Common Mistakes

  • Forgetting to add both directions when representing an undirected edge.
  • Accidentally adding reverse edges to a directed graph.
  • Assuming an array-based adjacency list provides O(1) edge lookup.
  • Forgetting to initialize a vertex's neighbor list before adding edges.
  • Ignoring isolated vertices that have no neighbors.
  • Storing weighted edges without preserving their associated weights.
  • Using an adjacency matrix when the graph is extremely sparse and memory is limited.

Key Takeaways

  • An adjacency list stores the neighbors of each vertex.
  • It normally requires O(V + E) space.
  • Undirected edges are generally stored in both vertices' lists.
  • Directed edges are normally stored from source to destination.
  • Weighted graphs can store objects containing both a neighbor and its weight.
  • BFS and DFS can run in O(V + E) time using an adjacency list.
  • Finding neighbors is efficient because only actual connections are stored.
  • Basic array-based edge lookup takes O(degree) time rather than O(1).
  • Adjacency lists are especially useful for sparse graphs and traversal-based algorithms.

Code Examples

const graph = {};

function addEdge(graph, a, b) {
  if (!graph[a]) {
    graph[a] = [];
  }

  if (!graph[b]) {
    graph[b] = [];
  }

  graph[a].push(b);
  graph[b].push(a);
}

addEdge(graph, 'A', 'B');
addEdge(graph, 'A', 'C');
addEdge(graph, 'B', 'D');

console.log(graph);
    
const graph = {};

function addDirectedEdge(graph, from, to) {
  if (!graph[from]) {
    graph[from] = [];
  }

  if (!graph[to]) {
    graph[to] = [];
  }

  graph[from].push(to);
}

addDirectedEdge(graph, 'A', 'B');
addDirectedEdge(graph, 'B', 'C');
addDirectedEdge(graph, 'C', 'D');

console.log(graph);
    
const graph = {};

function addWeightedEdge(graph, a, b, weight) {
  if (!graph[a]) {
    graph[a] = [];
  }

  if (!graph[b]) {
    graph[b] = [];
  }

  graph[a].push({ node: b, weight });
  graph[b].push({ node: a, weight });
}

addWeightedEdge(graph, 'A', 'B', 4);
addWeightedEdge(graph, 'B', 'C', 3);
addWeightedEdge(graph, 'A', 'C', 10);

console.log(graph);
    

Pro Tip

For most large, sparse graph problems, start by thinking about an adjacency list. Store only the edges that exist, and remember that an undirected edge normally appears in both endpoints while a directed edge follows only its defined direction.

🎨 IgKnight Docs

Master HTML, CSS, JS, Python, .NET and more with easy-to-follow lessons and practical code examples.

© 2025 IgKnight Learning Platform. All rights reserved.