Adjacency List in Graph Theory
In graph theory, an Adjacency List is another way to represent a graph, which is more space-efficient compared to the Adjacency Matrix for sparse graphs. Instead of using a 2D array, an adjacency list uses an array (or a list) of lists (or linked lists) to represent the connections between vertices. Each vertex in the graph has a list of adjacent vertices to which it is connected. This structure is particularly useful when the graph is sparse, meaning that the number of edges is much less than the square of the number of vertices.
An Adjacency List is a data structure used to represent a graph. It consists of an array of lists, where each index of the array represents a vertex in the graph. The list at each index contains the vertices that are adjacent (connected) to the corresponding vertex.
In simpler terms, for a graph with vertices, we create an array of elements. Each element is a list (or linked list) containing the vertices that are directly connected by an edge.
In an undirected graph, if there is an edge between two vertices and , both will include in its adjacency list, and will include in its list.
Example:
Consider the undirected graph:
A - B
| |
C - D
The adjacency list for this graph will look like this:
A: [B, C]
B: [A, D]
C: [A, D]
D: [B, C]
Here, vertex is connected to vertices and , so the adjacency list of contains and . Similarly, the other vertices have their respective connections.
In a directed graph, the adjacency list contains the vertices that are reachable by a directed edge from a given vertex. If there is an edge from vertex to vertex , then ’s adjacency list will contain , but ’s adjacency list will not contain .
Example:
Consider the directed graph:
A → B
↓ ↑
C → D
The adjacency list for this directed graph would look like this:
A: [B, C]
B: [D]
C: [D]
D: []
Here, vertex has directed edges to and , so its adjacency list contains and . Vertex has a directed edge to , so its list contains , and so on.
Space Efficiency: For sparse graphs, an adjacency list uses less space compared to an adjacency matrix. The space complexity is , where is the number of vertices and is the number of edges, which is much more efficient when the graph is sparse.
Faster Edge Enumeration: Enumerating the neighbors of a vertex is faster, requiring time, where is the number of neighbors. This is more efficient than the adjacency matrix, where you have to scan an entire row.
Dynamic Graph Representation: It is easier to add or remove edges dynamically, as only the corresponding adjacency lists need to be updated.
Efficient for Sparse Graphs: The adjacency list is ideal for sparse graphs where the number of edges is much smaller than , as it avoids using unnecessary space to store non-existent edges.
Slower Edge Lookup: To check if an edge exists between two vertices, you may need to scan through the adjacency list of the vertex, which takes time, where is the number of neighbors of the vertex.
More Complex for Matrix Operations: Operations like matrix multiplication are less straightforward with adjacency lists compared to adjacency matrices.
Check if an Edge Exists: To check if there is an edge between vertex and vertex , you need to scan the adjacency list of vertex to see if is present. This operation takes time, where is the number of neighbors of vertex .
Add an Edge: To add an edge from vertex to vertex , simply add to the adjacency list of . This operation takes time.
Remove an Edge: To remove an edge, you need to remove from the adjacency list of vertex . This can take time in the worst case if is at the end of the list.
Get All Neighbors of a Vertex: To get all neighbors of vertex , you simply access the adjacency list of , which takes time.
Here's an example implementation of an Adjacency List in Python for both directed and undirected graphs:
class Graph:
def __init__(self, vertices, directed=True):
self.V = vertices # Number of vertices
self.graph = [[] for _ in range(vertices)] # Initialize the adjacency list
self.directed = directed
def add_edge(self, u, v):
# Add edge from u to v
self.graph[u].append(v)
if not self.directed:
# For undirected graph, add edge from v to u as well
self.graph[v].append(u)
def remove_edge(self, u, v):
# Remove edge from u to v
if v in self.graph[u]:
self.graph[u].remove(v)
if not self.directed and u in self.graph[v]:
# For undirected graph, remove edge from v to u as well
self.graph[v].remove(u)
def display(self):
for i in range(self.V):
print(f"Vertex {i}: {self.graph[i]}")
# Example usage for Directed Graph
g = Graph(4, directed=True)
g.add_edge(0, 1)
g.add_edge(0, 2)
g.add_edge(1, 3)
g.add_edge(2, 3)
print("Adjacency List for Directed Graph:")
g.display()
# Example usage for Undirected Graph
g2 = Graph(4, directed=False)
g2.add_edge(0, 1)
g2.add_edge(0, 2)
g2.add_edge(1, 3)
g2.add_edge(2, 3)
print("\nAdjacency List for Undirected Graph:")
g2.display()
Output:
Adjacency List for Directed Graph:
Vertex 0: [1, 2]
Vertex 1: [3]
Vertex 2: [3]
Vertex 3: []
Adjacency List for Undirected Graph:
Vertex 0: [1, 2]
Vertex 1: [0, 3]
Vertex 2: [0, 3]
Vertex 3: [1, 2]