Home DSA Graph Representation Explained: Adjacency List vs Matrix (With Code)

Graph Representation Explained: Adjacency List vs Matrix (With Code)

In Plain English 🔥
Imagine a city map. Every city is a dot, and every road connecting two cities is a line. A graph is exactly that — dots (called nodes) connected by lines (called edges). Now, how do you store that map inside a computer's memory? That's the whole game of graph representation. You're essentially choosing the best filing system for your map so that lookups, additions, and traversals are as fast as possible.
⚡ Quick Answer
Imagine a city map. Every city is a dot, and every road connecting two cities is a line. A graph is exactly that — dots (called nodes) connected by lines (called edges). Now, how do you store that map inside a computer's memory? That's the whole game of graph representation. You're essentially choosing the best filing system for your map so that lookups, additions, and traversals are as fast as possible.

Graphs power more of your daily software than you'd guess. Google Maps finds your fastest route using graph traversal. Facebook's friend suggestions use graph connectivity. LinkedIn's 'second connection' feature? Graph theory. Every time you see a network of relationships — people, cities, servers, web pages — there's a graph underneath. Understanding how to store that graph isn't a minor detail; it's the decision that determines whether your algorithm runs in milliseconds or minutes.

The core problem is this: a graph is an abstract concept, but your computer only understands memory — arrays, pointers, contiguous blocks of data. You need a concrete data structure that faithfully represents which nodes exist and which nodes are connected, without wasting memory or making lookups unnecessarily slow. Two approaches dominate: the adjacency matrix and the adjacency list. Each is a completely different philosophy about the trade-off between memory usage and access speed, and choosing the wrong one for your problem is a classic, costly mistake.

By the end of this article you'll be able to build both representations from scratch in Java, explain in plain English why each one exists, look at a problem description and immediately know which structure to reach for, and walk into an interview and answer the follow-up questions that trip most candidates up.

Adjacency Matrix — The 'Spreadsheet' Approach to Storing Edges

An adjacency matrix is a 2D grid where both the rows and columns represent nodes. If there's an edge between node A and node B, you put a 1 (or the edge weight) at position [A][B]. If there's no edge, you store 0. That's it.

Why would you choose a grid? Because checking whether two specific nodes are connected is O(1) — instantaneous. You just index directly into the array like matrix[3][7]. There's no searching, no traversal. That property is invaluable when your core operation is 'does this edge exist?'

The cost you pay is memory. If your graph has N nodes, you always allocate N × N space — even if your graph only has a handful of edges. A social network with 100 million users storing an adjacency matrix would need 10 quadrillion cells. That's why adjacency matrices are reserved for dense graphs — graphs where most possible edges actually exist — and for smaller graphs where the O(1) edge lookup is worth the memory price.

For weighted graphs (like road distances), you simply store the weight instead of 1. For unweighted undirected graphs, the matrix is always symmetric — matrix[i][j] always equals matrix[j][i].

AdjacencyMatrix.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
public class AdjacencyMatrix {

    private final int[][] matrix;   // The actual grid storing edge data
    private final int nodeCount;    // Total number of nodes in the graph

    public AdjacencyMatrix(int nodeCount) {
        this.nodeCount = nodeCount;
        // Allocate a nodeCount x nodeCount grid, all initialised to 0 (no edges)
        this.matrix = new int[nodeCount][nodeCount];
    }

    // Add an undirected edge between two nodes (both directions matter)
    public void addEdge(int fromNode, int toNode) {
        // Validate to avoid silent index-out-of-bounds bugs in production
        if (fromNode < 0 || toNode < 0 || fromNode >= nodeCount || toNode >= nodeCount) {
            throw new IllegalArgumentException(
                "Node index out of range: " + fromNode + ", " + toNode
            );
        }
        matrix[fromNode][toNode] = 1;  // Mark edge in one direction
        matrix[toNode][fromNode] = 1;  // Mark edge in the other (undirected)
    }

    // O(1) check — this is the matrix's superpower
    public boolean hasEdge(int fromNode, int toNode) {
        return matrix[fromNode][toNode] == 1;
    }

    // Print the full grid so we can visualise the structure
    public void printMatrix() {
        System.out.print("    ");
        for (int col = 0; col < nodeCount; col++) {
            System.out.printf("%3d", col);  // Column headers
        }
        System.out.println();

        for (int row = 0; row < nodeCount; row++) {
            System.out.printf("%3d |", row);  // Row header
            for (int col = 0; col < nodeCount; col++) {
                System.out.printf("%3d", matrix[row][col]);
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        // Modelling a small city road network with 5 intersections (nodes 0-4)
        AdjacencyMatrix cityRoads = new AdjacencyMatrix(5);

        cityRoads.addEdge(0, 1);  // Road between intersection 0 and 1
        cityRoads.addEdge(0, 4);  // Road between intersection 0 and 4
        cityRoads.addEdge(1, 2);  // Road between intersection 1 and 2
        cityRoads.addEdge(1, 3);  // Road between intersection 1 and 3
        cityRoads.addEdge(3, 4);  // Road between intersection 3 and 4

        cityRoads.printMatrix();

        System.out.println();
        // O(1) lookup — is there a direct road from 0 to 3?
        System.out.println("Direct road from 0 to 3? " + cityRoads.hasEdge(0, 3));
        System.out.println("Direct road from 1 to 3? " + cityRoads.hasEdge(1, 3));
    }
}
▶ Output
0 1 2 3 4
0 | 0 1 0 0 1
1 | 1 0 1 1 0
2 | 0 1 0 0 0
3 | 0 1 0 0 1
4 | 1 0 0 1 0

Direct road from 0 to 3? false
Direct road from 1 to 3? true
⚠️
Memory Math You Should Know:An adjacency matrix for N nodes always costs O(N²) space — regardless of edge count. For a graph with 1,000 nodes that's 1,000,000 cells. For 10,000 nodes it's 100,000,000 cells. Run this calculation mentally before choosing the matrix approach on large, sparse graphs.

Adjacency List — The 'Contacts App' Approach That Scales

An adjacency list stores a graph differently: instead of a giant grid, each node gets its own personal list of the nodes it's directly connected to. If node 3 connects to nodes 1, 5, and 7, then node 3's list contains [1, 5, 7]. Nothing more.

Think of it like a contacts app. Each person (node) has a list of their direct friends (neighbours). You don't store a slot for every person on the planet that says 'not friends'. You only store the actual connections.

This is why adjacency lists dominate in the real world: most graphs are sparse — they have far fewer edges than the theoretical maximum. A social network with a billion users might have each user averaging 200 friends. An adjacency matrix would waste an astronomical amount of memory on zeros. An adjacency list stores only the real edges, giving you O(V + E) space where V is vertices and E is edges.

The trade-off: checking if a specific edge exists is now O(degree of node) — you have to scan the neighbour list. But for most graph algorithms like BFS and DFS, you're iterating over neighbours anyway, which is exactly what adjacency lists are optimised for.

AdjacencyList.java · JAVA
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class AdjacencyList {

    // Each node maps to a list of its direct neighbours
    // Using a Map allows nodes to have any label (String, Integer, etc.)
    private final Map<Integer, List<Integer>> adjacencyList;

    public AdjacencyList() {
        this.adjacencyList = new HashMap<>();
    }

    // Register a node so it exists in the graph even with no edges
    public void addNode(int node) {
        adjacencyList.putIfAbsent(node, new ArrayList<>());
    }

    // Add an undirected edge — each side adds the other to its neighbour list
    public void addEdge(int fromNode, int toNode) {
        addNode(fromNode);  // Ensure both nodes exist before adding edges
        addNode(toNode);

        adjacencyList.get(fromNode).add(toNode);  // fromNode now knows about toNode
        adjacencyList.get(toNode).add(fromNode);  // toNode now knows about fromNode
    }

    // O(degree) check — scan the neighbour list of fromNode
    public boolean hasEdge(int fromNode, int toNode) {
        List<Integer> neighbours = adjacencyList.getOrDefault(fromNode, new ArrayList<>());
        return neighbours.contains(toNode);
    }

    // Get all direct neighbours — this is the operation BFS/DFS uses constantly
    public List<Integer> getNeighbours(int node) {
        return adjacencyList.getOrDefault(node, new ArrayList<>());
    }

    // Print each node alongside its neighbour list
    public void printList() {
        for (Map.Entry<Integer, List<Integer>> entry : adjacencyList.entrySet()) {
            System.out.println("Node " + entry.getKey() + " --> " + entry.getValue());
        }
    }

    public static void main(String[] args) {
        // Modelling a simplified version of a flight route network
        AdjacencyList flightRoutes = new AdjacencyList();

        flightRoutes.addEdge(0, 1);  // London -- Paris
        flightRoutes.addEdge(0, 4);  // London -- Dubai
        flightRoutes.addEdge(1, 2);  // Paris -- New York
        flightRoutes.addEdge(1, 3);  // Paris -- Tokyo
        flightRoutes.addEdge(3, 4);  // Tokyo -- Dubai

        System.out.println("=== Flight Route Adjacency List ===");
        flightRoutes.printList();

        System.out.println();
        System.out.println("Neighbours of node 1 (Paris): " + flightRoutes.getNeighbours(1));
        System.out.println("Direct flight from 0 to 3? " + flightRoutes.hasEdge(0, 3));
        System.out.println("Direct flight from 1 to 4? " + flightRoutes.hasEdge(1, 4));
    }
}
▶ Output
=== Flight Route Adjacency List ===
Node 0 --> [1, 4]
Node 1 --> [0, 2, 3]
Node 2 --> [1]
Node 3 --> [1, 4]
Node 4 --> [0, 3]

Neighbours of node 1 (Paris): [0, 2, 3]
Direct flight from 0 to 3? false
Direct flight from 1 to 4? false
🔥
Why BFS and DFS Love Adjacency Lists:Both BFS and DFS need to repeatedly ask 'give me all neighbours of this node'. With an adjacency list, that operation is instant — you just return the pre-built list. With a matrix, you'd scan an entire row of N cells just to find the non-zero entries. For traversal-heavy algorithms, the list wins every time.

Directed and Weighted Graphs — When Edges Have Direction and Cost

So far the edges in our examples have been undirected — a road from A to B means you can also go from B to A. But real-world graphs often have direction. Twitter follows are directed: you following someone doesn't mean they follow you back. One-way streets are directed. Dependencies in a build system are directed.

A directed graph (digraph) changes one thing in the adjacency list: when you add an edge from A to B, you only add B to A's neighbour list — not the reverse. In the matrix, you only set matrix[A][B] = 1, not matrix[B][A].

Weighted graphs add a cost to each edge. Google Maps doesn't just care that roads exist — it cares how long each road takes. For an adjacency list, you swap the simple integer neighbour for an Edge object that holds both the destination and the weight. For a matrix, you store the weight value instead of 1.

The code below combines both concepts — a directed, weighted graph — which is the most realistic structure you'll implement in real interview problems and production systems.

DirectedWeightedGraph.java · JAVA
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class DirectedWeightedGraph {

    // Inner class representing a directed edge with a weight (travel cost)
    static class Edge {
        int destination;  // Where this edge points TO
        int weight;       // The cost to traverse this edge (e.g. distance in km)

        Edge(int destination, int weight) {
            this.destination = destination;
            this.weight = weight;
        }

        @Override
        public String toString() {
            return "->" + destination + "(cost:" + weight + ")";
        }
    }

    // Each node has a list of outgoing weighted edges
    private final Map<Integer, List<Edge>> graph;

    public DirectedWeightedGraph() {
        this.graph = new HashMap<>();
    }

    public void addNode(int node) {
        graph.putIfAbsent(node, new ArrayList<>());
    }

    // Directed: ONLY adds toNode to fromNode's list, not the reverse
    public void addDirectedEdge(int fromNode, int toNode, int weight) {
        addNode(fromNode);
        addNode(toNode);
        graph.get(fromNode).add(new Edge(toNode, weight));
        // Notice: we do NOT add fromNode to toNode's list here
    }

    public List<Edge> getOutgoingEdges(int node) {
        return graph.getOrDefault(node, new ArrayList<>());
    }

    public void printGraph() {
        for (Map.Entry<Integer, List<Edge>> entry : graph.entrySet()) {
            System.out.print("Node " + entry.getKey() + " : ");
            for (Edge edge : entry.getValue()) {
                System.out.print(edge + "  ");
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        // Modelling a package delivery network where roads are one-directional
        // and weight represents delivery time in minutes
        DirectedWeightedGraph deliveryNetwork = new DirectedWeightedGraph();

        deliveryNetwork.addDirectedEdge(0, 1, 10);  // Warehouse -> Depot A (10 mins)
        deliveryNetwork.addDirectedEdge(0, 2, 30);  // Warehouse -> Depot B (30 mins)
        deliveryNetwork.addDirectedEdge(1, 3, 15);  // Depot A   -> Customer X (15 mins)
        deliveryNetwork.addDirectedEdge(2, 3, 5);   // Depot B   -> Customer X (5 mins)
        deliveryNetwork.addDirectedEdge(1, 2, 8);   // Depot A   -> Depot B (8 mins)

        System.out.println("=== Delivery Network (Directed + Weighted) ===");
        deliveryNetwork.printGraph();

        System.out.println();
        System.out.println("Routes out of Depot A (node 1): "
            + deliveryNetwork.getOutgoingEdges(1));

        // Customer X (node 3) has no outgoing edges — it's a terminal node
        System.out.println("Routes out of Customer X (node 3): "
            + deliveryNetwork.getOutgoingEdges(3));
    }
}
▶ Output
=== Delivery Network (Directed + Weighted) ===
Node 0 : ->1(cost:10) ->2(cost:30)
Node 1 : ->3(cost:15) ->2(cost:8)
Node 2 : ->3(cost:5)
Node 3 :

Routes out of Depot A (node 1): [->3(cost:15), ->2(cost:8)]
Routes out of Customer X (node 3): []
⚠️
Watch Out — Directed vs Undirected Bugs Are Silent:If you accidentally add both directions when implementing a directed graph, your algorithm won't crash — it'll just give wrong answers. Dijkstra's on a directed graph with accidental reverse edges will find paths that don't actually exist. Always verify directionality before running any shortest-path algorithm.
Feature / AspectAdjacency MatrixAdjacency List
Space complexityO(V²) — always allocates V×VO(V + E) — only stores real edges
Check if edge existsO(1) — direct index lookupO(degree of node) — scan neighbour list
Find all neighbours of a nodeO(V) — must scan full rowO(degree of node) — instant list access
Add a new edgeO(1)O(1) amortised
Add a new nodeO(V²) — must resize entire matrixO(1) — just add a new list entry
Best forDense graphs, edge-lookup-heavy problemsSparse graphs, BFS/DFS traversals
Typical use casesFloyd-Warshall, small grid problemsBFS, DFS, Dijkstra, real-world networks
Memory for 1,000 nodes, 2,000 edges~4 MB (1M integer cells)~16 KB (only 2K edges stored)
Weighted graph supportStore weight instead of 1Store Edge object with weight field

🎯 Key Takeaways

  • Adjacency matrix = O(1) edge lookup but O(V²) space — only justifiable for dense graphs or algorithms like Floyd-Warshall that access every pair of nodes anyway.
  • Adjacency list = O(V + E) space and O(degree) neighbour access — the default choice for virtually all real-world sparse graph problems and traversal algorithms.
  • Directed graphs break the symmetry assumption: addEdge(A, B) must NOT automatically add addEdge(B, A). Getting this wrong produces silent logical bugs that are extremely hard to trace.
  • The most important decision in graph programming happens before you write a single traversal algorithm: choosing your representation. Get this wrong and no amount of algorithmic cleverness will save your performance.

⚠ Common Mistakes to Avoid

  • Mistake 1: Using an adjacency matrix for a sparse graph — If your graph has 10,000 nodes but only 15,000 edges, allocating a 10,000×10,000 matrix wastes 99.985% of its memory on zeros. The symptom is your program running out of heap space or being extremely slow even before any algorithm runs. Fix: Count your nodes and edges first. If E << V², use an adjacency list. The rule of thumb: if edge count is closer to V than V², go with the list.
  • Mistake 2: Forgetting to add both directions for undirected graphs — You call addEdge(2, 5) and only add 5 to node 2's list. Now BFS from node 5 can't reach node 2, even though there should be a connection. The symptom is traversals that mysteriously miss nodes that are 'one step behind'. Fix: Always add both directions in the addEdge method itself, not at the call site — make the API safe by default, not the caller responsible for remembering.
  • Mistake 3: Using node indices that don't start at 0 for a matrix — You have nodes labelled 1 through N and allocate a matrix of size N×N, then access matrix[nodeLabel] directly. Node N tries to access index N in an array of size N, throwing an ArrayIndexOutOfBoundsException. Fix: Either allocate size N+1 and accept the wasted row/column at index 0, or consistently subtract 1 from every node label before indexing. Pick one approach and document it clearly.

Interview Questions on This Topic

  • QWhen would you choose an adjacency matrix over an adjacency list, and what's the concrete cost of making the wrong choice? Walk me through the memory calculation for a sparse graph with 50,000 nodes and 100,000 edges using both representations.
  • QHow would you modify an adjacency list to support a weighted directed graph? What changes to the data structure and the addEdge method would you need, and how does it affect the space complexity?
  • QIf I give you an adjacency matrix, can you convert it to an adjacency list in place? What's the time and space complexity of that conversion, and why might someone need to do it at runtime?

Frequently Asked Questions

What is the difference between adjacency list and adjacency matrix in graph representation?

An adjacency matrix is a V×V grid where matrix[i][j]=1 means an edge exists between nodes i and j — it uses O(V²) space but gives O(1) edge lookups. An adjacency list gives each node its own list of direct neighbours, using only O(V+E) space but requiring O(degree) time to check if a specific edge exists. Use the matrix for dense graphs or when edge lookups dominate; use the list for sparse graphs or traversal-heavy algorithms.

How do you represent a weighted graph using an adjacency list?

Instead of storing a plain integer neighbour in the list, you store an Edge object that contains both the destination node and the weight. For example: each node maps to a List where Edge holds int destination and int weight. This way you can retrieve all outgoing edges from a node and their costs in a single list traversal, exactly what Dijkstra's algorithm needs.

Why does the choice of graph representation affect algorithm performance?

Because graph algorithms like BFS and DFS repeatedly ask 'what are the neighbours of this node?' — and the cost of answering that question differs between representations. With an adjacency list it's instant (return the pre-built list). With a matrix you scan a full row of V cells to find non-zero entries. For a graph with 10,000 nodes, that's 10,000 wasted comparisons per node visit. Across millions of visits this difference is the gap between an algorithm that finishes in seconds and one that runs for hours.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousSerialize and Deserialize Binary TreeNext →BFS — Breadth First Search
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged