Adjacency lists are the workhorse graph data structure used widely across the industry due to their flexibility and performance. As a long-time C++ graph algorithm developer, I have utilized them across search, social networks, recommendation systems and more.
In this comprehensive guide, you will gain an in-depth understanding of adjacency list internals, optimizations and applications from a professional C++ perspective.
Graph Data Structure Overview
But before we dive further, let‘s do a quick primer on graph terminology and representations.
Graph Basics
A graph models a set of vertices and the connections between them. It is used to represent networks and relationships arising in domains like social media, transportation, biology and web links analysis.
Key graph terms are:
- Vertex: Fundamental unit containing data or metadata, like a person in social network
- Edge: An abstract connection between two vertices, indicating a relationship
- Weighted/Unweighted: Whether numeric weights are assigned to edges
- Directed/Undirected: Specifies if edges imply one-way or two-way relationship
Graph Representations
There are two primary ways to actually store the vertex and edge data on disk and in memory:
Adjacency Matrix: A |V| x |V| 2D array of vertices, where each cell denotes connectivity through 1/0 or actual edge weight.
Adjacency List: A vector of individual linked lists per vertex, listing the connected vertices.
There is also the Incidence Matrix with rows for vertices and columns for edges. But as it is less popular, we will focus our analysis on the first two.
Here is a sample undirected graph:

And its adjacency matrix representation:

While the corresponding adjacency list would be:
A -> B, C
B -> A, C, D
C -> A, B, D, E
D -> B, C, E
E -> C, D
Now that you have a high level sense of mathematical graph concepts, let‘s analyze the adjacency list data structure and its C++ implementation closely.
Adjacency List Design and Implementation
An adjacency list aims to represent an N vertex graph as a vector of N linked lists. Each index in the array corresponds to a vertex. Each linked list describes the set of neighbors of the vertex through connections to corresponding list elements.
Adjacency List Class Definition
Here is how an adjacency List class can be defined in C++:
class AdjacencyList {
int numVertices;
vector<list<int>> adjList;
public:
AdjacencyList(int V) {
// Constructor
numVertices = V;
adjList.resize(V);
}
void addEdge(int src, int dest);
void printGraph();
// Other functions
};
The key components are:
numVertices – Total number of vertices in the graph
adjList – Primary adjacency list vector of linked lists
Analysis
-
Space Complexity: O(V + E)
- Stores all vertices and edges
-
Vertex Addition: O(1)
- Just increments numVertices
This forms the generic foundation of our high performance adjacency list implementation.
Next let‘s analyze the key operations and optimizations possible.
Key Operations
The fundamental operations supported by adjacency list are:
Add Edge
Connecting two vertices via an edge simply involves appending connected vertex to the source vertex‘s linked list:
void addEdge(int src, int dest) {
adjList[src].push_back(dest); // Add dest to end
}
Time Complexity: O(1)
Remove Edge
Deleting an edge is slower as it requires searching through the linked list before removing:
void removeEdge(int src, int dest) {
adjList[src].remove(dest);
}
Time Complexity: O(E), with E being number of edges.
Print Graph
Printing adjacency list format involves iterating through all vertices:
void printGraph() {
for(int v = 0; v < numVertices; v++) {
cout << v << " -> ";
// Print v‘s adjacency list
}
}
Time Complexity: O(V + E)
These form the core operations to build a graph incrementally. Now let‘s analyze ways to optimize this further.
Optimizations
There are several optimizations possible on top of the base implementation:
Reversible Edges
For undirected graphs with bidirectional edges, explicitly handle reversibility:
void addEdge(int src, int dest) {
adjList[src].push_back(dest);
// Optimized for undirected graph
adjList[dest].push_back(src);
}
This handles both directions in one method call improving efficiency.
Alternative Containers
The STL unordered_set can be used instead of list for slight speedups:
vector<unordered_set<int>> adjList;
// Faster lookups with hash table
if(adjList[src].find(dest) != adjList.end()) {
// Vertex found
}
Tradeoff is extra space overhead of hashing.
There are also options like set or priority_queue for ordered traversals.
Weighted Edges
For weighted graphs, encapsulate edge weights with the connected vertex pointer:
struct Edge {
int dest Vertex;
int weight;
};
// Store vector of Edge
vector<list<Edge>> adjList;
This keeps everything together. Update methods to populate weight as well.
Thus with these optimizations, we can customize the adjacency list as per graph requirements. Now let‘s analyze the complexity in detail.
Time and Space Complexity Analysis
Let‘s formally express the time and storage analysis for key operations using big O asymptotic notation for our optimized adjacency list:
| Operation | Time Complexity | Space Complexity |
|---|---|---|
| Add Vertex | O(1) | O(1) |
| Remove Vertex | O(V + E) | O(1) |
| Add Edge | O(1) | O(1) |
| Remove Edge | O(E) | O(1) |
| Query Edge | O(V) | O(1) |
| Print Graph | O(V + E) | O(1) |
Observations:
- Add Vertex is O(1) constant time, by just updating count
- Add/Remove Edge is fast at O(1) and O(E) respectively
- Query Edge exists can take O(V) in worst case
- Spacewise – stores all vertices + edges, so additional memory is O(1) per operation
So in most cases, adjacency list operations are very fast. This, coupled with space savings compared to matrix, makes it the popular graph structure choice.
Comparison to Adjacency Matrix:
| Operation | Adj. List | Adj. Matrix |
|---|---|---|
| Space | O(V + E) | O(V^2) |
| Add Edge | O(1) | O(1) |
| Remove Edge | O(E) | O(1) |
| Query Edge | O(V) | O(1) |
| Print Graph | O(V + E) | O(V^2) |
We can clearly observe the time/space tradeoffs:
- Adjacency list occupies less total memory by not allocating an NxN matrix
- Edge deletion and lookup queries are faster with matrix
- But matrix slows down significantly for large/sparse graphs overall
So in most real-world cases, adjacency list wins out through faster additions and smaller memory footprint.
These asymptotic complexity results form the foundation of choosing the appropriate data structure for your graph algorithm needs.
Graph Traversal Algorithms
Now that we have a high performance adjacency list foundation, let‘s explore how to actually process graphs by traversing connected vertices. Graph traversals have many applications like peer recommendations, advertisement targeting and medical diagnosis through symptom tracking.
We will analyze two fundamental graph visit algorithms:
Depth First Search
DFS starts at a vertex, explores it as far as possible before backtracking. This goes very deep expanding one path fully before the next branch.
Pseudo-code is:
DFS(s):
visit s
mark s visited
for every edge (s, d):
if d not visited:
DFS(d)
Implementing DFS traversal on adjacency list:
void dfs(int start) {
visit(start); // Print or process vertex
visited[start] = true;
for(int v : adjList[start]) {
if(!visited[v]) {
dfs(v);
}
}
}
We leverage the adjacency list to directly access all neighboring vertices.
Sample output on our graph for DFS starting at vertex A:
A -> B -> D -> E -> C
It goes as deep as possible from each neighboring vertex before backtracking.
Time Complexity: O(V + E)
Breadth First Search
In contrast to the deep DFS, BFS explores vertices outwards level by level from the root.
Pseudocode:
BFS(s)
visit s
add s to queue q
while q not empty:
t = q.dequeue() // Remove next vertex
visit t
for edges (t, i):
if i not visited:
visit i
q.enqueue(i)
Implementation with adjacency list:
void bfs(int start) {
visit(start);
queue.enqueue(start);
while(!queue.empty()) {
int t = queue.dequeue();
for(int i : adjList[t]) {
if(!visited[i]) {
visit(i);
queue.enqueue(i);
}
}
}
}
Output for BFS beginning with A:
A -> B -> C -> D -> E
It progressively fans out crossing edge branches.
Time Complexity: O(V + E)
So both traversals produce the entire graph coverage through complementary strategies in linear time.
Applications
Some key applications leveraging adjacency list representation and traversals include:
1. Peer Recommendations
Graph algorithms help suggest friends, followers, groups etc. on social networks by connectivity.
2. Route Planning
Maps and GPS navigation rely on graph models to calculate shortest or optimal routes.
3. Web Search
Crawling link structure of web pages is done through directed web graphs.
There are also a wide variety of real-world connections modeled efficiently via adjacency lists like financial transactions, neural networks and phylogenetics hierarchies.
Conclusion
We have explored the design, optimizations and usage of high performance adjacency lists for graph computing in C++.
Key highlights are:
- Linear vectors and linked hash sets provide flexibility and speed
- Intelligent handling of edge direction leads to big efficiency gains
- Graph traversals enable search, recommendations and related applications
- Significant performance over other representations like matrices
Adjacency lists pack quite a punch. With the right design choices, they are often the first data structure of choice when representing readable graphs in memory. Graph frameworks like Boost and NetworKit have utilized such programming best practices around adjacency lists to create fast applications.
I hope you enjoyed this advanced dive into optimizing adjacency list internals! Let me know if you have any other questions.


