Data Structure and Algorithm
Data Structure
Graph
Adjacency List Representation

Graph Representation (Adjacency List)

An Adjacency List is the most common way to represent graphs in practice, especially for sparse graphs (where the number of edges is much less than $V^2$).

In this representation, we maintain an array (or map) of lists. The size of the array is equal to the number of vertices $V$. For each vertex $i$, the list at index $i$ contains all the vertices that are adjacent to $i$.


Adjacency List Concept

For the undirected graph shown below:

The corresponding Adjacency List representation is:

Index [0]  -->  1  -->  4  -->  /
Index [1]  -->  0  -->  4  -->  2  -->  3  -->  /
Index [2]  -->  1  -->  3  -->  /
Index [3]  -->  1  -->  4  -->  2  -->  /
Index [4]  -->  3  -->  0  -->  1  -->  /

(where / represents the end of the list/null)

Cache Friendliness: While traditional computer science textbooks define adjacency lists using linked lists, in modern languages like JavaScript, C++ (vector), and Java (ArrayList), they are implemented using dynamic arrays. This offers much better cache locality/friendliness than pointers of a linked list.


JavaScript Implementation

Here is a complete JavaScript implementation of a graph using an Adjacency List:

class Graph {
    constructor() {
        this.list = {};
    }
    
    // Add a vertex/node to the graph
    addVertex(vertex) {
        if (!this.list[vertex]) {
            this.list[vertex] = [];
        }
    }
    
    // Add an undirected edge between vertex1 and vertex2
    addEdge(vertex1, vertex2) {
        // Ensure both vertices exist
        this.addVertex(vertex1);
        this.addVertex(vertex2);
        
        this.list[vertex1].push(vertex2);
        this.list[vertex2].push(vertex1);
    }
    
    // Display the adjacency list connections
    showConnections() {
        Object.keys(this.list).forEach(vertex => {
            console.log(`${vertex} --> ${this.list[vertex].join(", ")}`);
        });
    }
}
 
// Example usage:
const g = new Graph();
g.addEdge("A", "B");
g.addEdge("A", "C");
g.addEdge("B", "D");
g.addEdge("C", "E");
 
g.showConnections();
/*
Output:
A --> B, C
B --> A, D
C --> A, E
D --> B
E --> C
*/

Complexity Analysis

  • Space Complexity: $O(V + E)$ (or $O(V + 2E)$ for undirected graphs)
  • Adding a vertex: $O(1)$
  • Adding an edge: $O(1)$
  • Removing a vertex: $O(V + E)$ (must remove the vertex and search all lists to clean up edges pointing to it)
  • Removing an edge: $O(V)$ (requires searching the adjacency list of the vertex to remove the edge)
  • Querying edge existence (u -> v): $O(V)$ (worst case, if a vertex is connected to all other vertices)

Advantages and Disadvantages

Advantages

  • Highly Space-Efficient: Saves a massive amount of memory for sparse graphs compared to an Adjacency Matrix.
  • Fast Vertex Insertion: Adding a new vertex is a constant time operation $O(1)$.
  • Iterating Neighbors: Finding all neighbors of a vertex is extremely fast and takes optimal time proportional to the degree of the vertex.

Disadvantages

  • Slower Edge Queries: Checking if there is an edge between vertex u and v takes $O(V)$ in the worst case (compared to $O(1)$ in an Adjacency Matrix), as we must traverse the list of neighbors for u.