Graphs are commonly used in computing to model networks. An adjacency list is a compact and flexible way to represent the structure of a graph.
What is an adjacency list?
An adjacency list is a way of storing a graph where each vertex (or node) is associated with a list of all the vertices to which it is directly connected. These connected vertices are known as its adjacent vertices or neighbours. This representation captures the structure of a graph efficiently, especially when not all possible edges are present.
The adjacency list approach works well for sparse graphs, which are graphs where the number of edges is significantly fewer than the maximum possible. It avoids wasting memory by storing only actual connections, unlike other methods that might store every possible relationship regardless of whether it exists.
In most programming languages, adjacency lists can be implemented using data structures like:
Dictionaries (or maps), where the keys are the vertices and the values are lists of adjacent vertices.
Arrays of lists, where each index represents a vertex and contains a list of neighbours.
Structure of an adjacency list
Undirected graph
In an undirected graph, edges have no direction. That is, an edge from A to B is the same as an edge from B to A.
Suppose we have an undirected graph with the following vertices and connections:
Practice Questions
FAQ
Adjacency lists are highly effective for graphs that change frequently because they allow for efficient edge insertion and deletion. When adding an edge, the operation is simply a matter of appending a vertex to another’s adjacency list, which is typically an O(1) operation if the list is implemented as a dynamic array or linked list. Deletion involves searching for the vertex in the list and removing it, which is usually O(k), where k is the number of adjacent vertices. This flexibility makes adjacency lists well-suited for dynamic graphs, such as social networks or simulation systems, where connections between nodes may form or dissolve regularly. Unlike adjacency matrices, which require changing specific entries in a 2D array and potentially reallocating memory for size adjustments, adjacency lists can expand and shrink efficiently with minimal memory overhead. This adaptability ensures that performance remains stable even as the graph’s structure evolves significantly over time.
Yes, adjacency lists are very useful for detecting cycles, especially when combined with graph traversal algorithms like depth-first search (DFS). During DFS, you maintain a set or array to track the recursion stack of visited vertices. As you visit each node, you check whether any of its neighbours are already in the current recursion stack. If a neighbour is found there, a cycle exists. This method works effectively with an adjacency list because accessing the neighbours of any vertex is fast and straightforward. In directed graphs, this technique can help detect back edges that form cycles, which is essential in tasks like validating dependency structures. In undirected graphs, extra care is needed to avoid detecting the immediate parent node as a cycle. Adjacency lists keep traversal efficient by focusing only on existing edges, which speeds up cycle detection in large, sparse graphs and avoids unnecessary processing of non-existent connections, unlike adjacency matrices.
The internal structure used for each vertex’s adjacency list significantly impacts the performance of common operations. If a dynamic array (like a Python list) is used, adding edges is fast (amortised O(1)), and iteration is efficient, but removing a specific vertex or checking existence can take O(n) time. A linked list allows for quick insertion and deletion once the element is found, but searching is still linear, and memory overhead is higher due to pointer storage. Using a set (like Python’s set) offers faster existence checks (O(1) average time), which is useful for algorithms that frequently check whether a connection exists. However, sets typically use more memory and do not preserve the order of neighbours, which may matter in certain traversal algorithms. The choice depends on what operations are most common: for frequent additions and iterations, arrays are ideal; for fast membership checks, sets are preferred; for frequent deletions, linked lists may be best.
When representing large-scale real-world networks—such as the web, transport systems, or social media graphs—using adjacency lists, several challenges can arise. First, although adjacency lists are memory-efficient, the sheer number of vertices and edges can still result in significant storage requirements, especially if the graph has millions or billions of nodes. Second, such networks are often heterogeneous, with some nodes having very high degrees (connected to thousands of others), which can lead to unbalanced data structures and inefficiencies in processing. Third, operations like searching for specific edge weights, shortest paths, or centrality measures may require augmenting the list structure with extra metadata, increasing complexity. Additionally, large graphs are often distributed across systems, and adjacency lists stored in one machine may not scale well. They may also need special indexing and compression strategies to reduce storage and retrieval time. Finally, concurrency and updates in real-time systems add further challenges in maintaining consistent and efficient adjacency list representations.
Adjacency lists can represent multigraphs (graphs with multiple edges between the same pair of vertices) and self-loops (edges that connect a vertex to itself), but this requires slight adjustments. In standard implementations, each adjacency list contains unique neighbours. To support multigraphs, the list must allow duplicate entries, meaning a vertex like A might have B listed more than once if multiple edges exist from A to B. This is often implemented using lists or arrays that permit repetition. For weighted multigraphs, each edge can be represented as a separate tuple with its own weight, e.g. [(B, 3), (B, 5)]. Handling self-loops is more straightforward: the vertex simply includes itself in its own adjacency list, e.g. A: [A]. While this is logically valid, it can affect algorithms like DFS or BFS if they don’t account for revisiting the same node immediately. Care must be taken in graph traversal and visualisation to correctly interpret and process these types of edges.
