Elementary Graph Algorithms: Complete Guide to Graph Representations

Elementary Graph Algorithms: Representations of Graphs

Before I can run any graph algorithm, I have to decide how the graph itself is going to live in memory. That choice sounds trivial, but it quietly determines the performance ceiling of everything built on top of it. This guide is my rundown of the elementary ways to represent graphs — adjacency matrices, adjacency lists, and edge lists — along with the reasoning I use to pick between them and the basic operations built on top of each.

History and Background

Graph theory itself dates back to Leonhard Euler’s 1736 solution to the Seven Bridges of Königsberg problem, which is generally credited as the founding moment of the field. But representing graphs computationally is a much newer concern, tied to the rise of digital computing in the mid-20th century. Adjacency matrices arose naturally from linear algebra’s treatment of graphs (a graph’s matrix representation makes eigenvalue-based analysis possible), while adjacency lists became popular as computer scientists needed more memory-efficient structures for the large, sparse graphs that show up in real-world networks. Both representations were standardized in early algorithms texts and are foundational material in CLRS.

Problem Statement

Given a set of vertices and edges, I need a data structure that supports the operations graph algorithms actually need: checking if an edge exists, iterating over a vertex’s neighbors, adding/removing edges, and doing all of this efficiently for the graph’s actual density (sparse vs. dense).

Core Concepts

How It Works

Graphs are typically represented one of three ways:

  1. Adjacency Matrix: A V × V matrix where cell [i][j] holds 1 (or the weight) if an edge exists from i to j, and 0/infinity otherwise.
  2. Adjacency List: An array of V lists, where list[i] contains all vertices adjacent to vertex i.
  3. Edge List: A simple list of all edges as (u, v) or (u, v, weight) tuples, with no per-vertex indexing.

Choosing between them is really a tradeoff between the speed of edge lookup, the speed of neighbor iteration, and the memory used, which strongly depends on how sparse or dense the graph is.

Working Principle

The adjacency matrix leans entirely on direct indexing: checking or setting an edge is a single array access, which is why it’s O(1) for edge queries. Its downside is that it always allocates V² space no matter how many actual edges exist, and iterating over a vertex’s neighbors always costs O(V) since I have to scan the whole row.

The adjacency list instead stores only the edges that actually exist, using a per-vertex list (often a linked list, dynamic array, or even a hash set). This makes it far more memory-efficient for sparse graphs and makes neighbor iteration proportional to the vertex’s actual degree rather than the total vertex count — but checking whether a specific edge exists now requires scanning a list rather than a direct lookup (unless I use a hash-based list).

The edge list is the simplest and is mostly useful when I need to process all edges globally rather than per-vertex — for example, sorting edges by weight for Kruskal’s algorithm.

Mathematical Foundation

For an adjacency matrix, space usage is:

$$S_{matrix} = O(V^2)$$

For an adjacency list, space usage is:

$$S_{list} = O(V + E)$$

Since a simple graph has at most:

$$E_{max} = \binom{V}{2} = \frac{V(V-1)}{2}$$

edges, the adjacency list only becomes worse than the matrix when the graph approaches this maximum density, i.e., when E is on the order of V². This is the formal justification for the common rule of thumb: use adjacency lists for sparse graphs, matrices for dense ones.

Diagrams

flowchart TD
    A[Choose Graph Representation] --> B{Is the graph dense E close to V squared?}
    B -- Yes --> C[Use Adjacency Matrix: O1 edge lookup]
    B -- No --> D{Do I need fast edge existence checks?}
    D -- Yes --> E[Use Adjacency List with hash set per vertex]
    D -- No --> F[Use plain Adjacency List]
    C --> G[Proceed with graph algorithm]
    E --> G
    F --> G

Pseudocode

// Adjacency Matrix
CREATE-MATRIX(V):
    matrix = V x V array initialized to 0
    return matrix

ADD-EDGE-MATRIX(matrix, u, v, weight):
    matrix[u][v] = weight
    matrix[v][u] = weight   // omit for directed graphs

// Adjacency List
CREATE-LIST(V):
    adj = array of V empty lists
    return adj

ADD-EDGE-LIST(adj, u, v, weight):
    adj[u].append((v, weight))
    adj[v].append((u, weight))   // omit for directed graphs

Step-by-Step Example

Consider an undirected graph with vertices {0,1,2,3} and edges (0,1), (0,2), (1,3).

As an adjacency matrix:

    0  1  2  3
0 [ 0  1  1  0 ]
1 [ 1  0  0  1 ]
2 [ 1  0  0  0 ]
3 [ 0  1  0  0 ]

As an adjacency list:

0: [1, 2]
1: [0, 3]
2: [0]
3: [1]

As an edge list:

(0,1), (0,2), (1,3)

Each representation stores exactly the same graph, but querying “are 0 and 3 connected?” is O(1) with the matrix (a single lookup at [0][3]) versus O(degree(0)) with the list (I’d scan 0’s list, which has 2 entries).

Time Complexity

OperationAdjacency MatrixAdjacency List
Check edge (u,v) existsO(1)O(degree(u))
Iterate all neighbors of uO(V)O(degree(u))
Add edgeO(1)O(1)
Remove edgeO(1)O(degree(u))
Iterate all edgesO(V²)O(V + E)

Space Complexity

Correctness Analysis

Correctness here isn’t really about a single algorithm but about whether the representation faithfully preserves the graph’s structure: every representation must guarantee that for every edge (u,v) in E, a query for that edge returns true (or the correct weight), and that iterating a vertex’s neighbor set returns exactly its adjacent vertices, no more and no less. All three standard representations satisfy this trivially by construction, assuming edges are inserted correctly and, for undirected graphs, inserted symmetrically.

Advantages

Adjacency Matrix:

Adjacency List:

Disadvantages

Adjacency Matrix:

Adjacency List:

Applications

Implementation in C

#include <stdio.h>
#include <stdlib.h>

#define MAX_V 100

/* Adjacency Matrix representation */
typedef struct {
    int matrix[MAX_V][MAX_V];
    int V;
} GraphMatrix;

void initMatrix(GraphMatrix *g, int v) {
    g->V = v;
    for (int i = 0; i < v; i++)
        for (int j = 0; j < v; j++)
            g->matrix[i][j] = 0;
}

void addEdgeMatrix(GraphMatrix *g, int u, int v) {
    g->matrix[u][v] = 1;
    g->matrix[v][u] = 1;
}

/* Adjacency List representation using linked lists */
typedef struct Node {
    int vertex;
    struct Node *next;
} Node;

typedef struct {
    Node *head[MAX_V];
    int V;
} GraphList;

void initList(GraphList *g, int v) {
    g->V = v;
    for (int i = 0; i < v; i++)
        g->head[i] = NULL;
}

void addEdgeList(GraphList *g, int u, int v) {
    Node *newNode = (Node *)malloc(sizeof(Node));
    newNode->vertex = v;
    newNode->next = g->head[u];
    g->head[u] = newNode;

    newNode = (Node *)malloc(sizeof(Node));
    newNode->vertex = u;
    newNode->next = g->head[v];
    g->head[v] = newNode;
}

void printList(GraphList *g) {
    for (int i = 0; i < g->V; i++) {
        printf("%d: ", i);
        Node *temp = g->head[i];
        while (temp != NULL) {
            printf("%d -> ", temp->vertex);
            temp = temp->next;
        }
        printf("NULL\n");
    }
}

int main(void) {
    GraphMatrix gm;
    initMatrix(&gm, 4);
    addEdgeMatrix(&gm, 0, 1);
    addEdgeMatrix(&gm, 0, 2);
    addEdgeMatrix(&gm, 1, 3);

    printf("Adjacency Matrix:\n");
    for (int i = 0; i < gm.V; i++) {
        for (int j = 0; j < gm.V; j++)
            printf("%d ", gm.matrix[i][j]);
        printf("\n");
    }

    GraphList gl;
    initList(&gl, 4);
    addEdgeList(&gl, 0, 1);
    addEdgeList(&gl, 0, 2);
    addEdgeList(&gl, 1, 3);

    printf("\nAdjacency List:\n");
    printList(&gl);

    return 0;
}

Sample Input and Output

Input: Vertices {0,1,2,3}, edges (0,1), (0,2), (1,3).

Output:

Adjacency Matrix:
0 1 1 0
1 0 0 1
1 0 0 0
0 1 0 0

Adjacency List:
0: 2 -> 1 -> NULL
1: 3 -> 0 -> NULL
2: 0 -> NULL
3: 1 -> NULL

Optimization Techniques

Common Mistakes

Further Reading

Exit mobile version