2. Graph Representations
A graph is an abstract concept. To store one in a computer's memory, we must translate this idea of vertices and edges into a concrete data structure. The choice of representation is critical, as it dictates the performance and space efficiency of our graph algorithms.
A good representation must efficiently answer two fundamental questions:
- "Is vertex
uconnected to vertexv?" - "What are all the neighbors of vertex
u?"
The two most common methods to solve this are the Adjacency Matrix and the Adjacency List.
2.1. Adjacency Matrix
An Adjacency Matrix is a 2D array, typically of size V x V, where V is the number of vertices. Each cell adj[u][v] in the matrix stores information about the edge between vertex u and vertex v.
2.1.1. Concept
Imagine a flight chart. The rows represent the "source" vertex and the columns represent the "destination" vertex.
- For an unweighted graph, we store a
1(ortrue) atadj[u][v]if an edge exists, and0(orfalse) if it does not. - For a weighted graph, instead of storing
0or1, we store the weight of the edge. - For an undirected graph, the matrix will be symmetric about the diagonal (i.e.,
adj[u][v] == adj[v][u]). - For a directed graph, the matrix is not necessarily symmetric.
matrix[u][v]can be1whilematrix[v][u]is0
2.1.2. Example
Consider this simple undirected graph with 4 vertices:
(0) --- (1)
| \ |
| \ |
| \ |
| \ |
| \ |
| \ |
| \ |
(3) --- (2)
Edges: (0,1), (0,2), (0,3), (1,2), (2,3)
The Adjacency Matrix for this graph would be:
0 1 2 3
0[0,1,1,1]
1[1,0,1,0]
2[1,1,0,1]
3[1,0,1,0]
2.1.3 Implementation (C++)
In C++, you would implement this using a vector of vectors.
// Assume V is the number of vertices
int V = 4;
// 1. Initialize a V x V matrix with all zeros
std::vector<std::vector<int>> adjMatrix(
V,
std::vector<int>(V, 0)
);
// 2. Add an edge (e.g., between 0 and 2)
void addEdge(int u, int v) {
adjMatrix[u][v] = 1;
adjMatrix[v][u] = 1; // For undirected
}
// 3. To check for an edge (e.g., between 1 and 3):
bool hasEdge = (adjMatrix[1][3] == 1); // false
// 4. To find all neighbors of a vertex (e.g., vertex 0):
for (int j = 0; j < V; ++j) {
if (adjMatrix[0][j] == 1) {
// j is a neighbor of 0
// This will find j=1, j=2, and j=3
}
}
2.1.4. Pros and Cons
Pros:
- Fast Edge Lookup: Checking if an edge
(u, v)exists isO(1). You just access theadjMatrix[u][v]cell. - Simple to Implement: The logic is straightforward, as it's just a 2D array lookup.
Cons:
- Space Inefficient: This is the biggest drawback. The matrix always requires
O(V^2)space, regardless of how many edges the graph has. - Slow Neighbor Iteration: Finding all neighbors of a vertex requires iterating through its entire row, which is an
O(V)operation, even if the vertex only has one neighbor.
2.2. Adjacency List
An Adjacency List is an array (or std::vector) of lists. The main array has V entries, where each entry adj[u] corresponds to vertex u. The entry adj[u] then holds a list of all other vertices v that are neighbors of u.
2.2.1. Concept
Imagine a phone's contacts list. The main array (of size V) acts like your address book index. Each entry in this index (e.g., adj[u]) doesn't hold a single value, but rather points to a list of all that vertex's direct neighbors.
- For an unweighted graph, the list for
adj[u]simply stores the indices (likev1,v2) of all its neighbors. - For a weighted graph, the list for
adj[u]stores pairs, where each pair contains the neighbor's index and the edge's weight (e.g.,{v1, w1},{v2, w2}). - For an undirected graph, when you add an edge
(u, v), you must addvtoadj[u]'s list and addutoadj[v]'s list. - For a directed graph, When you add an edge
(u, v), you only addvtoadj[u]'s list.
2.2.2. Example
Using the same 4-vertex graph:
(0) --- (1)
| \ |
| \ |
| \ |
| \ |
| \ |
| \ |
| \ |
(3) --- (2)
The Adjacency List for this graph would be:
0: -> [1, 2, 3]
1: -> [0, 2]
2: -> [0, 1, 3]
3: -> [0, 2]
2.2.3. Implementation (C++)
In C++, you would implement this using a vector of lists (or a vector of vectors).
// Assume V is the number of vertices
int V = 4;
// 1. Initialize a vector of size V. Each element
// is an empty list.
std::vector<std::list<int>> adjList(V);
// 2. Add an edge (e.g., between 0 and 2)
void addEdge(int u, int v) {
adjList[u].push_back(v);
adjList[v].push_back(u); // For undirected
}
// 3. To check for an edge (e.g., between 1 and 3):
// This is slower than the matrix.
bool hasEdge = false;
for (auto& neighbor : adjList[1]) {
if (neighbor == 3) {
hasEdge = true;
break;
}
} // hasEdge is false
// 4. To find all neighbors of a vertex (e.g., vertex 0):
// This is extremely fast.
for (auto& neighbor : adjList[0]) {
// neighbor is a neighbor of 0
// This will find neighbor=1, neighbor=2, and neighbor=3
}
2.2.4. Handling Weighted Graphs
To store weights, the list cannot just be of ints. It must store pairs (or a custom struct).
std::vector<std::list<std::pair<int, int>>> adjList(V);- The
pair<int, int>would store{neighbor, weight}. addEdgewould look like:adjList[u].push_back({v, weight});
2.3.5. Pros and Cons
Pros:
- Space Efficient: This is the primary advantage. The total space required is
O(V + E). - Fast Neighbor Iteration: Finding all neighbors of a vertex u is
O(deg(u))(wheredeg(u)is the degree, or number of neighbors). This is optimally fast.
Cons:
- Slow Edge Lookup: Checking if a specific edge
(u, v)exists isO(deg(u))in the worst case, because we must iterate through the listadj[u]to see ifvis in it.
2.3. Comparison: Matrix vs. List
| Operation | Adjacency Matrix | Adjacency List |
|---|---|---|
| Space | O(V²) |
O(V + E) |
| Add Edge | O(1) |
O(1) |
| Check Edge (u, v) | O(1) (Fast) |
O(deg(u)) (Slow) |
| Find Neighbors of u | O(V) (Slow) |
O(deg(u)) (Fast) |
| Remove Edge (u, v) | O(1) |
O(deg(u)) |
| Best For | Dense Graphs (where E ≈ V²) |
Sparse Graphs (most common case) |
No comments to display
No comments to display