Skip to main content

Graph Representation (Java)

Description

A graph is a non-linear data structure consisting of two fundamental components:

  • Vertices (Nodes): Individual entities, typically labeled with integers starting from 0.
  • Edges: Connections between pairs of vertices.

Graphs appear everywhere in computer science — modeling social networks (users connected by friendships), transportation networks (cities connected by roads), dependency systems (tasks connected by prerequisites), and much more.

There are two primary types of graphs:

  1. Undirected Graph: Every edge is bidirectional. If vertex A connects to vertex B, then B also connects to A. Example: a friendship — if Alice is friends with Bob, Bob is friends with Alice.
  2. Directed Graph (Digraph): Each edge has a specific direction. An edge from A to B does not imply an edge from B to A. Example: following someone on social media — you can follow someone without them following you back.

Edges can optionally carry weights — numeric values that represent cost, distance, capacity, or any other quantitative attribute.

The core challenge addressed here is: How do we store a graph in a Java program so that we can efficiently query and traverse it? The two standard representations are:

  1. Adjacency Matrix — a 2D array int[][] adj where adj[i][j] indicates whether an edge exists from vertex i to vertex j.
  2. Adjacency List — an ArrayList<ArrayList<Integer>> where each inner list holds the neighbors of the corresponding vertex.

Given V vertices (numbered 0 to V−1) and E edges, implement both representations in Java and understand when to use each one.

Side-by-side comparison of an undirected graph and a directed graph, each with 4 vertices and several edges
Side-by-side comparison of an undirected graph and a directed graph, each with 4 vertices and several edges

Examples

Example 1

Input: V = 3, E = 3, edges = [(0,1), (0,2), (1,2)], undirected

Output (Adjacency Matrix):

0 1 1
1 0 1
1 1 0

Output (Adjacency List):

0 → [1, 2]
1 → [0, 2]
2 → [0, 1]

Explanation: This graph forms a triangle where every vertex is connected to the other two. In the adjacency matrix, adj[0][1] = 1 and adj[1][0] = 1 both record the same undirected edge (0,1). The matrix is symmetric because every edge is bidirectional. In the adjacency list (using Java's ArrayList<ArrayList<Integer>>), adj.get(0) contains [1, 2] — the two neighbors of vertex 0.

Example 2

Input: V = 4, E = 4, edges = [(0,1), (1,2), (2,3), (3,0)], directed

Output (Adjacency Matrix):

0 1 0 0
0 0 1 0
0 0 0 1
1 0 0 0

Output (Adjacency List):

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

Explanation: This forms a directed cycle: 0→1→2→3→0. The adjacency matrix is not symmetric — adj[0][1] = 1 but adj[1][0] = 0, because the edge only goes from 0 to 1, not back. In the adjacency list, adj.get(0) only contains [1], not [1, 3] — even though vertex 3 points to vertex 0, that edge belongs in vertex 3's list, not vertex 0's.

Example 3

Input: V = 3, E = 3, edges = [(0,1,4), (0,2,7), (1,2,3)], undirected, weighted

Output (Adjacency Matrix):

0 4 7
4 0 3
7 3 0

Output (Adjacency List):

0 → [(1,4), (2,7)]
1 → [(0,4), (2,3)]
2 → [(0,7), (1,3)]

Explanation: In a weighted graph, the adjacency matrix stores the weight rather than 1. Cell adj[0][1] = 4 means the edge from vertex 0 to vertex 1 has weight 4. In the adjacency list using Java, we store int[] pairs or use a helper class: each entry holds both the neighbor and the weight. For example, vertex 0's list [(1,4), (2,7)] means vertex 0 connects to vertex 1 with weight 4 and to vertex 2 with weight 7.

Constraints

  • 1 ≤ V ≤ 10^5 (number of vertices)
  • 0 ≤ E ≤ min(V × (V − 1) / 2, 10^5) for undirected graphs
  • 0 ≤ E ≤ min(V × (V − 1), 10^5) for directed graphs
  • 0 ≤ u, v < V (vertex indices are 0-based)
  • Edge weights (if present): −10^9 ≤ w ≤ 10^9
  • No self-loops unless explicitly stated
  • No duplicate edges in the input

Editorial

Adjacency Matrix

Intuition

The simplest way to represent a graph in code is with a two-dimensional grid. Picture a spreadsheet where both the row headers and column headers are vertex numbers (0, 1, 2, … V−1). Each cell answers one binary question: "Is there an edge from vertex i to vertex j?"

If the answer is yes, we write 1 (or the edge weight) in that cell. If not, the cell stays 0.

Think of a seating arrangement at a dinner party. You make a chart with all guest names along the top and down the side. If two guests know each other, you check the box where their row and column meet. This chart is your adjacency matrix.

For a graph with V vertices, we create a V × V two-dimensional integer array:

int[][] adj = new int[V][V];

In Java, new int[V][V] automatically initializes every cell to 0 — no manual initialization needed (unlike some other languages). For each edge (u, v):

  • Undirected: Set adj[u][v] = 1 AND adj[v][u] = 1
  • Directed: Set only adj[u][v] = 1
  • Weighted: Store the weight: adj[u][v] = weight

This representation makes checking if a specific edge exists extremely fast — it is a direct array access at adj[u][v], which takes constant O(1) time.

Step-by-Step Explanation

Let's build the adjacency matrix for Example 1: V = 3, edges = [(0,1), (0,2), (1,2)], undirected.

Step 1: Declare int[][] adj = new int[3][3];. Java initializes all cells to 0.

adj = [[0, 0, 0],
       [0, 0, 0],
       [0, 0, 0]]

Step 2: Process edge (0,1). Set adj[0][1] = 1.

adj = [[0, 1, 0],
       [0, 0, 0],
       [0, 0, 0]]

Step 3: Undirected mirror: set adj[1][0] = 1.

adj = [[0, 1, 0],
       [1, 0, 0],
       [0, 0, 0]]

Step 4: Process edge (0,2). Set adj[0][2] = 1.

adj = [[0, 1, 1],
       [1, 0, 0],
       [0, 0, 0]]

Step 5: Mirror: set adj[2][0] = 1.

adj = [[0, 1, 1],
       [1, 0, 0],
       [1, 0, 0]]

Step 6: Process edge (1,2). Set adj[1][2] = 1.

adj = [[0, 1, 1],
       [1, 0, 1],
       [1, 0, 0]]

Step 7: Mirror: set adj[2][1] = 1.

adj = [[0, 1, 1],
       [1, 0, 1],
       [1, 1, 0]]

Step 8: All 3 edges have been processed. The adjacency matrix is complete. The matrix is symmetric along the main diagonal (because the graph is undirected), and the diagonal is all zeros (no self-loops).

Building an Adjacency Matrix — Undirected Graph (Java) — Watch how each undirected edge fills two symmetric cells in the V×V integer array, gradually building the complete adjacency matrix.

Algorithm

  1. Read the number of vertices V and edges E.
  2. Create a V × V integer array: int[][] adj = new int[V][V]; (auto-initialized to 0 in Java).
  3. For each edge (u, v) read from input:
    • Set adj[u][v] = 1 (or the weight for weighted graphs).
    • If the graph is undirected, also set adj[v][u] = 1.
    • If directed, set only adj[u][v].
  4. To check if an edge exists from i to j: read adj[i][j] — returns 1 if edge exists, 0 otherwise.
  5. To find all neighbors of vertex i: loop through column index j from 0 to V−1, collecting every j where adj[i][j] != 0.

Code

import java.util.Scanner;

public class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int V = sc.nextInt();
        int E = sc.nextInt();

        // Java auto-initializes int arrays to 0
        int[][] adj = new int[V][V];

        for (int i = 0; i < E; i++) {
            int u = sc.nextInt();
            int v = sc.nextInt();

            adj[u][v] = 1;
            adj[v][u] = 1; // Remove this line for directed graph
        }

        // Print the adjacency matrix
        for (int i = 0; i < V; i++) {
            for (int j = 0; j < V; j++) {
                System.out.print(adj[i][j] + " ");
            }
            System.out.println();
        }
    }
}

// --- Weighted graph variant ---
// Store the weight instead of 1:
//
// int u = sc.nextInt(), v = sc.nextInt(), w = sc.nextInt();
// adj[u][v] = w;
// adj[v][u] = w; // Remove for directed
#include <bits/stdc++.h>
using namespace std;

int main() {
    int V, E;
    cin >> V >> E;

    // Create V x V matrix initialized to 0
    vector<vector<int>> adj(V, vector<int>(V, 0));

    for (int i = 0; i < E; i++) {
        int u, v;
        cin >> u >> v;

        adj[u][v] = 1;
        adj[v][u] = 1; // Remove for directed graph
    }

    // Print the adjacency matrix
    for (int i = 0; i < V; i++) {
        for (int j = 0; j < V; j++) {
            cout << adj[i][j] << " ";
        }
        cout << endl;
    }

    return 0;
}
def build_adjacency_matrix(V, edges, directed=False):
    # Create V x V matrix initialized to 0
    adj = [[0] * V for _ in range(V)]

    for u, v in edges:
        adj[u][v] = 1
        if not directed:
            adj[v][u] = 1  # Mirror for undirected

    return adj


V, E = map(int, input().split())
edges = []
for _ in range(E):
    u, v = map(int, input().split())
    edges.append((u, v))

adj = build_adjacency_matrix(V, edges)
for row in adj:
    print(" ".join(map(str, row)))

Complexity Analysis

Time Complexity:

  • Building the matrix: O(V² + E)

    • new int[V][V] in Java allocates and zero-fills V² cells → O(V²).
    • Processing each of the E edges takes O(1) per edge → O(E).
    • Total: O(V²) dominates.
  • Checking if edge (u, v) exists: O(1) — direct array access adj[u][v].

  • Finding all neighbors of vertex u: O(V) — must scan all V cells in row u to find non-zero entries.

Space Complexity: O(V²)

The 2D array always uses V² integer cells regardless of edge count. For a graph with 10,000 vertices and only 100 edges, we allocate 100,000,000 cells — 99.999% are wasted zeros. In Java, each int is 4 bytes, so a 10,000 × 10,000 matrix uses ~400 MB of memory.

Why This Approach Is Not Efficient

The adjacency matrix has a fundamental limitation: it always consumes O(V²) space, regardless of how sparse the graph is.

In practice, most graphs are sparse — they have far fewer edges than the maximum possible V × (V−1) / 2. Consider these numbers:

  • V = 10,000, E = 20,000 (a typical sparse graph)
  • Adjacency matrix: V² = 100,000,000 integer cells = ~400 MB in Java (4 bytes per int)
  • Actual useful data: only 40,000 cells are non-zero (2 per undirected edge)
  • 99.96% of the allocated memory is wasted

Besides wasted space, enumerating all neighbors of a vertex requires iterating over the full row of V cells, even if the vertex has just 2 neighbors. This O(V) neighbor scan is the bottleneck in graph traversal algorithms like BFS and DFS, which call it repeatedly.

The key observation: if we could store only the edges that exist instead of every possible edge, we would reduce space from O(V²) to O(V + E) and make neighbor iteration proportional to the actual number of neighbors. In Java, ArrayList<ArrayList<Integer>> gives us exactly this — dynamically-sized lists that only hold actual neighbors.

Bar chart comparing memory usage of adjacency matrix (400 MB) versus adjacency list (~0.16 MB) for V=10,000 and E=20,000
Bar chart comparing memory usage of adjacency matrix (400 MB) versus adjacency list (~0.16 MB) for V=10,000 and E=20,000

Optimal Approach - Adjacency List

Intuition

Instead of pre-allocating space for every possible connection, what if each vertex only stored the neighbors it actually has?

Imagine you maintain a contact list on your phone. You do not have an entry for every human on Earth — you only store the numbers of people you actually know. If you have 50 contacts, your list has 50 entries. If your friend has 200 contacts, their list has 200 entries.

The adjacency list works exactly like this. We create an array of V lists — one list per vertex. Each list holds only the actual neighbors of that vertex:

ArrayList<ArrayList<Integer>> adj = new ArrayList<>();
for (int i = 0; i < V; i++) {
    adj.add(new ArrayList<>());
}

For each edge (u, v):

  • adj.get(u).add(v); — add v to u's neighbor list
  • For undirected: adj.get(v).add(u); — add u to v's list as well

For weighted graphs, we store pairs of (neighbor, weight). In Java, a clean approach is:

ArrayList<ArrayList<int[]>> adj = new ArrayList<>();
// Each int[] is {neighbor, weight}
adj.get(u).add(new int[]{v, weight});

Or you can create a Pair class or use ArrayList<ArrayList<int[]>>.

This is the standard representation used in virtually all graph algorithm implementations in Java — BFS, DFS, Dijkstra, Kruskal, topological sort, and more all expect adjacency lists.

Step-by-Step Explanation

Let's build the adjacency list for Example 1: V = 3, edges = [(0,1), (0,2), (1,2)], undirected.

Step 1: Create 3 empty ArrayLists, one per vertex.

adj.get(0) = []
adj.get(1) = []
adj.get(2) = []

Step 2: Process edge (0,1). Call adj.get(0).add(1) — add vertex 1 to vertex 0's list.

adj.get(0) = [1]
adj.get(1) = []
adj.get(2) = []

Step 3: Undirected mirror: call adj.get(1).add(0) — add vertex 0 to vertex 1's list.

adj.get(0) = [1]
adj.get(1) = [0]
adj.get(2) = []

Step 4: Process edge (0,2). Call adj.get(0).add(2).

adj.get(0) = [1, 2]
adj.get(1) = [0]
adj.get(2) = []

Step 5: Mirror: call adj.get(2).add(0).

adj.get(0) = [1, 2]
adj.get(1) = [0]
adj.get(2) = [0]

Step 6: Process edge (1,2). Call adj.get(1).add(2).

adj.get(0) = [1, 2]
adj.get(1) = [0, 2]
adj.get(2) = [0]

Step 7: Mirror: call adj.get(2).add(1).

adj.get(0) = [1, 2]
adj.get(1) = [0, 2]
adj.get(2) = [0, 1]

Step 8: All edges processed. The adjacency list is complete. Each vertex stores only its actual neighbors. Total entries: 6 (two per undirected edge). To iterate over vertex 0's neighbors, just loop over adj.get(0) — instant access to [1, 2] without scanning any zeros.

Building an Adjacency List — Undirected Graph (Java) — Watch how each edge is processed and added to both vertices' ArrayLists, progressively building the graph's adjacency list representation.

Algorithm

  1. Read the number of vertices V and edges E.
  2. Create an ArrayList<ArrayList<Integer>> with V empty inner lists:
    ArrayList<ArrayList<Integer>> adj = new ArrayList<>();
    for (int i = 0; i < V; i++) adj.add(new ArrayList<>());
    
  3. For each edge (u, v) from input:
    • adj.get(u).add(v); — add v to u's neighbor list.
    • If undirected, also adj.get(v).add(u);.
    • If directed, only add to adj.get(u).
    • For weighted edges with weight w: adj.get(u).add(new int[]{v, w}); (using ArrayList<ArrayList<int[]>>).
  4. To iterate over all neighbors of vertex i: for (int neighbor : adj.get(i)) { ... }.
  5. To check if edge (u, v) exists: scan adj.get(u) for value v — takes O(degree(u)) time.

Code

import java.util.*;

public class Solution {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int V = sc.nextInt();
        int E = sc.nextInt();

        // Create adjacency list: V empty ArrayLists
        ArrayList<ArrayList<Integer>> adj = new ArrayList<>();
        for (int i = 0; i < V; i++) {
            adj.add(new ArrayList<>());
        }

        for (int i = 0; i < E; i++) {
            int u = sc.nextInt();
            int v = sc.nextInt();

            adj.get(u).add(v);
            adj.get(v).add(u); // Remove for directed graph
        }

        // Print the adjacency list
        for (int i = 0; i < V; i++) {
            System.out.print(i + " -> ");
            for (int neighbor : adj.get(i)) {
                System.out.print(neighbor + " ");
            }
            System.out.println();
        }
    }
}

// --- Weighted graph variant ---
// Use ArrayList<ArrayList<int[]>> to store {neighbor, weight} pairs:
//
// ArrayList<ArrayList<int[]>> adj = new ArrayList<>();
// for (int i = 0; i < V; i++) adj.add(new ArrayList<>());
//
// int u = sc.nextInt(), v = sc.nextInt(), w = sc.nextInt();
// adj.get(u).add(new int[]{v, w});
// adj.get(v).add(new int[]{u, w}); // Remove for directed
#include <bits/stdc++.h>
using namespace std;

int main() {
    int V, E;
    cin >> V >> E;

    // Adjacency list: V empty vectors
    vector<vector<int>> adj(V);

    for (int i = 0; i < E; i++) {
        int u, v;
        cin >> u >> v;

        adj[u].push_back(v);
        adj[v].push_back(u); // Remove for directed graph
    }

    // Print the adjacency list
    for (int i = 0; i < V; i++) {
        cout << i << " -> ";
        for (int neighbor : adj[i]) {
            cout << neighbor << " ";
        }
        cout << endl;
    }

    return 0;
}

// --- Weighted variant ---
// vector<vector<pair<int,int>>> adj(V);
// adj[u].push_back({v, w});
// adj[v].push_back({u, w}); // Remove for directed
def build_adjacency_list(V, edges, directed=False):
    # Create V empty lists
    adj = [[] for _ in range(V)]

    for u, v in edges:
        adj[u].append(v)
        if not directed:
            adj[v].append(u)  # Mirror for undirected

    return adj


V, E = map(int, input().split())
edges = []
for _ in range(E):
    u, v = map(int, input().split())
    edges.append((u, v))

adj = build_adjacency_list(V, edges)
for i in range(V):
    print(f"{i} -> {' '.join(map(str, adj[i]))}")


# --- Weighted variant ---
# Store tuples (neighbor, weight):
# def build_weighted_list(V, edges, directed=False):
#     adj = [[] for _ in range(V)]
#     for u, v, w in edges:
#         adj[u].append((v, w))
#         if not directed:
#             adj[v].append((u, w))
#     return adj

Complexity Analysis

Time Complexity:

  • Building the list: O(V + E)

    • Creating V empty ArrayLists takes O(V).
    • Each edge is processed in O(1) amortized time (ArrayList add() is amortized O(1) in Java).
    • Total: O(V + E).
  • Checking if edge (u, v) exists: O(degree(u))

    • Must scan vertex u's neighbor list. In the worst case, degree can be V−1, but for sparse graphs it is typically much smaller.
    • If O(1) edge checking is critical, you can additionally maintain a HashSet per vertex.
  • Finding all neighbors of vertex u: O(degree(u))

    • Just iterate over adj.get(u). This visits only actual neighbors, not all V vertices.

Space Complexity: O(V + E)

  • V ArrayList objects (one per vertex) → O(V).
  • For undirected graphs, each edge appears in two lists → 2E total entries → O(E).
  • For directed graphs, each edge appears once → E entries → O(E).
  • Total: O(V + E).

For sparse graphs (E << V²), this is dramatically more efficient. A graph with V = 10,000 and E = 20,000 uses ~50,000 list entries vs. 100,000,000 matrix cells.

Summary — When to Use Which:

OperationAdjacency MatrixAdjacency List
SpaceO(V²)O(V + E)
Check if edge existsO(1)O(degree)
List all neighborsO(V)O(degree)
Add an edgeO(1)O(1) amortized
Best forDense graphs, O(1) edge queriesSparse graphs, BFS/DFS, most algorithms
Java structureint[][]ArrayList<ArrayList<Integer>>