SwadhyAI Notes

All DSA notes

DSA Graphs

Graph Implementation

The query hits: is user 10⁷ connected to user 42? Your machine does not store circles and lines. It stores arrays, maps, and bits. The representation chooses which questions are cheap.

Pick an , , , or from the operations you run, then defend if edges are bidirectional.

Decision panel

1

Hot operations

Traverse neighbors? Test (u,v)? Mutate often?

2

Density

Sparse → list/CSR; dense pair tests → matrix.

3

Direction contract

Undirected needs two entries; directed does not.

4

Isolates

Allocate all V buckets before inserting edges.

What actually happens

Naive idea: always use a V×V matrix because edge tests are O(1). At V = 10⁵ that is impossible; at sparse social graphs most cells are empty forever.

Better idea: store only existing edges in lists (or CSR for static scans). Pay O(deg(u)) to test a specific neighbor unless you add a hash side index.

The improved design still fails on half-updated undirected edges, on dropping isolates when building from an edge list, and on hash iteration order that makes “the” BFS order nondeterministic.

Invariant

Every vertex in V has a storage slot (even if empty). For undirected graphs, (u,v) present implies (v,u) present with the same weight policy. Algorithms may assume that symmetry; a half-edge silently lies.

How to choose a representation

1. Traversal-heavy and sparse? Adjacency list (or CSR if static).

2. Constant-time arbitrary edge tests on dense V? Matrix.

3. Sorting edges by weight? Keep an edge list (Kruskal) and optionally build lists later.

Quantitative check

List space ≈ O(V + E); matrix ≈ O(V²). For V = 10⁴, E = 3×10⁴, lists win by orders of magnitude. Neighbor scan on a matrix still touches V entries per vertex, so BFS becomes O(V²) even when E is small.

1. Adjacency Matrix

A 2D array (grid) of size V x V. If matrix[i][j] == 1, there is an edge from i to j.

A
B
C
A
0
1
0
B
1
0
1
C
0
1
0
Pros:Fast lookups O(1) to check connection.
Cons:Uses lots of space O(V²). Bad for sparse graphs.

2. Adjacency List

A collection of lists (or a dictionary). Each vertex stores a list of its neighbors.

A
→
B
B
→
A
→
C
C
→
B
Pros:Saves space O(V + E). Great for sparse graphs.
Cons:Slower lookup O(V) to check connection.

Representation explorer

The graph below is identical in both views. Toggle directed/undirected to see why a symmetric matrix and two adjacency entries are implementation consequences, not extra logical edges.

Interactive invariant lab

Graph state, not just graph shape

Switch semantics and algorithms, then step through the frontier, discovery state, and edge-level proof obligation.

undirected graph for RepresentEach logical edge is listed in both endpoint buckets, but still counts once in |E|.42315261ABCDEFG

Semantic contract

Undirected weighted graph

Each logical edge is listed in both endpoint buckets, but still counts once in |E|.

Invariant

For every logical edge {u, v}, v appears in adj[u] and u appears in adj[v].

Same graph, different physical layout

Weights are stored beside the destination; ∞ means no edge.

A→B(4), C(2)
B→A(4), D(3)
C→A(2), D(1), E(5)
D→B(3), C(1), E(2), G(6)
E→C(5), D(2), F(1)
F→E(1)
G→D(6)
ActiveFrontierProcessed

Choose by operations, not habit

V vertices, E logical edges, and deg(u) neighbors of u.
RepresentationSpaceEdge queryScan neighborsBest fit
Adjacency list (array)O(V + E)O(deg(u))O(deg(u))Sparse traversal-heavy graphs
Adjacency sets/mapsO(V + E)Expected O(1)O(deg(u))Frequent insert/delete/contains
Adjacency matrixO(V²)O(1)O(V)Dense graph or constant-time pair queries
Edge listO(E)O(E)O(E)Bellman–Ford, Kruskal, interchange
CSRO(V + E)O(deg(u))O(deg(u))Static, cache-friendly massive graphs

A hash-backed neighbor map changes the edge-query bound, but adds hashing, object, and resizing overhead. State whether your complexity assumes a list, set, matrix, or compressed layout.

Weighted edges & lookup nuance

Storing weights

Adjacency lists usually store (neighbor, weight) pairs. Matrices store w[u][v] (or ∞ if absent). Multi-edges can be merged by keeping the minimum weight or kept explicitly depending on the problem.

“Is (u,v) an edge?”

Matrix: O(1). List: scan u's neighbors-O(deg(u)) without a side hash from neighbor → weight. For static graphs that query arbitrary edges often, an edge hash map or matrix slice can help.

CSR / COO (advanced)

For very large sparse graphs (scientific computing, PageRank prototypes), compressed sparse row (CSR) packs the adjacency list into parallel-friendly arrays with prefix sums over degrees.

Build safely from an edge list

Allocate all n buckets first so isolated vertices survive. Validate endpoints at the boundary, and add the reverse entry only when the input contract is undirected.

Python

python
def build_graph(n, edges, directed=False):
    adj = [[] for _ in range(n)]  # keeps isolated vertices
    for u, v, weight in edges:
        if not (0 <= u < n and 0 <= v < n):
            raise ValueError("edge endpoint out of range")
        adj[u].append((v, weight))
        if not directed:
            adj[v].append((u, weight))
    return adj

Java

java
import java.util.*;

final class GraphBuilder {
    record Edge(int to, long weight) {}

    static List<List<Edge>> buildGraph(
            int n, long[][] edges, boolean directed) {
        List<List<Edge>> adj = new ArrayList<>(n);
        for (int i = 0; i < n; i++) adj.add(new ArrayList<>());

        for (long[] edge : edges) {
            int u = Math.toIntExact(edge[0]);
            int v = Math.toIntExact(edge[1]);
            long weight = edge[2];
            if (u < 0 || u >= n || v < 0 || v >= n) {
                throw new IllegalArgumentException("edge endpoint out of range");
            }
            adj.get(u).add(new Edge(v, weight));
            if (!directed) adj.get(v).add(new Edge(u, weight));
        }
        return adj;
    }
}

Implementation (adjacency list)

We usually prefer the Adjacency List because most real-world graphs are sparse (not everything is connected to everything). Here's how to implement a Graph class in Python and Java using a dictionary/map.

Python

python
class Graph:
    def __init__(self):
        # Dictionary to store the graph: {vertex: [neighbors]}
        self.adj_list = {}

    def add_vertex(self, vertex):
        if vertex not in self.adj_list:
            self.adj_list[vertex] = []

    def add_edge(self, v1, v2):
        # Add v1 and v2 if they don't exist
        if v1 not in self.adj_list:
            self.add_vertex(v1)
        if v2 not in self.adj_list:
            self.add_vertex(v2)
        
        # Add connection (Undirected Graph)
        self.adj_list[v1].append(v2)
        self.adj_list[v2].append(v1)

    def print_graph(self):
        for vertex in self.adj_list:
            print(f"{vertex} -> {self.adj_list[vertex]}")

# Usage
my_graph = Graph()
my_graph.add_edge('A', 'B')
my_graph.add_edge('B', 'C')
my_graph.add_edge('C', 'A')

my_graph.print_graph()

# Output:
# A -> ['B', 'C']
# B -> ['A', 'C']
# C -> ['B', 'A']

Java

java
import java.util.*;

class Graph {
    // Map to store graph: Vertex -> List of Neighbors
    private Map<String, List<String>> adjList;

    public Graph() {
        adjList = new HashMap<>();
    }

    public void addVertex(String vertex) {
        adjList.putIfAbsent(vertex, new ArrayList<>());
    }

    public void addEdge(String v1, String v2) {
        addVertex(v1);
        addVertex(v2);

        // Add connection (Undirected Graph)
        adjList.get(v1).add(v2);
        adjList.get(v2).add(v1);
    }

    public void printGraph() {
        for (String vertex : adjList.keySet()) {
            System.out.println(vertex + " -> " + adjList.get(vertex));
        }
    }

    public static void main(String[] args) {
        Graph myGraph = new Graph();
        myGraph.addEdge("A", "B");
        myGraph.addEdge("B", "C");
        myGraph.addEdge("C", "A");

        myGraph.printGraph();
        
        // Output:
        // A -> [B, C]
        // B -> [A, C]
        // C -> [B, A]
    }
}

Mutation invariants

Undirected symmetry

Adding or deleting {u,v} must update both buckets atomically. A half-edge silently corrupts degree counts, traversals, and cycle detection.

Duplicate policy

A list permits parallel edges; a set deduplicates them; a neighbor→weight map overwrites unless you combine weights. Choose intentionally.

Vertex deletion

Removing a vertex also requires removing every incident edge. An incoming-edge index can avoid scanning every bucket in a directed graph.

Deterministic order

Hash iteration order should not define externally visible traversal output. Sort once for static graphs or use ordered collections when reproducibility matters.

CSR: why static graphs can be faster

Compressed sparse row stores one offsets array of length V + 1 and one contiguous neighbors array of length E (or 2E for an undirected graph). Neighbors of u occupy neighbors[offsets[u]..offsets[u+1]). It avoids one object per list/edge and improves cache locality.

BuildO(V + E) via degree prefix sums
TraverseSequential, allocation-light scans
MutateExpensive; rebuild or use deltas

Implementation review checklist

  • ✓ Are vertex IDs dense, sparse, or strings, and is remapping required?
  • ✓ Are self-loops and parallel edges legal, ignored, or rejected?
  • ✓ Can weights exceed int, and what represents “no edge” without colliding with a real weight?
  • ✓ Does E count logical undirected edges once even though storage contains two entries?
  • ✓ Do empty buckets preserve isolated vertices and disconnected components?
  • ✓ Is the graph immutable during algorithms, or do readers need locking/versioned snapshots?

Simple rule of thumb

Use an adjacency list for most sparse graphs (social, web, road networks).
Use an adjacency matrix when the graph is dense or you need extremely fast arbitrary edge lookups.

Exercises

  1. Build from edges [(0,1),(1,2)] with n = 5. How many empty adjacency buckets must exist, and what goes wrong if you only create keys that appear in E?
  2. An undirected add_edge updates only adj[u]. Which algorithms silently misbehave, and how would a degree-sum check catch it?
  3. V = 5000, E ≈ V²/2. Would you still prefer lists for BFS? Estimate neighbor-scan work for list vs matrix.

Primary sources

  • Cormen et al., Introduction to Algorithms (CLRS): adjacency-list and adjacency-matrix representations.
  • Sedgewick & Wayne: Graph ADT implementations in practice.
  • Scientific computing notes on CSR/CSC sparse formats (e.g. SciPy sparse documentation).

Takeaway

Representation is an operations budget. Keep V complete, keep undirected edges symmetric, and state whether complexity assumes lists, sets, or a matrix.