Lesson 112 of 164
Graphs in Java
A graph is a set of vertices (nodes) connected by edges. Trees and linked lists are special kinds of graphs. General graphs can have cycles, several paths between two vertices, and parts that aren't connected at all.
graph LR
0 --> 1
0 --> 2
1 --> 3
2 --> 3
3 --> 4| Term | Meaning |
|---|---|
| directed / undirected | an edge goes one way (0 → 1) or both ways (0 — 1) |
| weighted | each edge has a cost, such as a distance or a price |
neighbors of v |
the vertices an edge from v leads to |
| degree | the number of edges at a vertex. Directed graphs distinguish in-degree and out-degree |
| path | a sequence of vertices joined by edges |
| cycle | a path that starts and ends at the same vertex |
| connected (undirected) | every vertex can reach every other vertex |
| DAG | a directed acyclic graph: directed, with no cycles |
V is the number of vertices and E the number of edges. A graph can have up to V² edges (dense) or about as many edges as vertices (sparse).
Number the vertices, store the edges as indices
A tree node holds references to its children, and each node has one parent. In a graph, a vertex can be reached from many others, and cycles are allowed. A Node class with a List<Node> neighbors field works in Java, because object references can point anywhere and the garbage collector reclaims unreachable nodes. Such nodes still need a way to be identified, copied, and marked as visited, and a Node needs equals and hashCode to be used as a map key.
The standard approach is to number the vertices 0 to V - 1 and store edges as indices into a list. Indices are plain int values: they can be stored in arrays, queues, sets, and stacks and compared with ==, and a visited flag is one boolean[] slot.
Adjacency list
adj.get(v) is the list of v's neighbors. This is the default representation. It is a List<List<Integer>>: an ArrayList whose elements are ArrayLists of neighbor indices.
import java.util.ArrayList;
import java.util.List;
public class Main {
static List<List<Integer>> directed(int n, int[][] edges) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) {
adj.get(e[0]).add(e[1]);
}
return adj;
}
static List<List<Integer>> undirected(int n, int[][] edges) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) {
adj.get(e[0]).add(e[1]);
adj.get(e[1]).add(e[0]); // store the edge in both directions
}
return adj;
}
public static void main(String[] args) {
int[][] edges = {{0, 1}, {0, 2}, {1, 3}, {2, 3}, {3, 4}};
List<List<Integer>> d = directed(5, edges);
List<List<Integer>> u = undirected(5, edges);
System.out.println("directed: " + d);
System.out.println("undirected: " + u);
System.out.println("neighbors of 3 (undirected): " + u.get(3));
}
}directed: [[1, 2], [3], [3], [4], []]
undirected: [[1, 2], [0, 3], [0, 3], [1, 2, 4], [3]]
neighbors of 3 (undirected): [1, 2, 4]For a weighted graph, store (neighbor, weight) pairs, for example as int[]{neighbor, weight} in a List<List<int[]>>.
Adjacency matrix
matrix[u][v] is true if there's an edge from u to v. Checking a specific edge is O(1), but the matrix always uses V² space, and listing a vertex's neighbors means scanning a whole row.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
int n = 4;
boolean[][] matrix = new boolean[n][n];
int[][] edges = {{0, 1}, {0, 2}, {1, 3}, {2, 3}};
for (int[] e : edges) {
matrix[e[0]][e[1]] = true;
}
System.out.println("edge 0 -> 2? " + matrix[0][2]);
List<Integer> neighborsOf0 = new ArrayList<>();
for (int v = 0; v < n; v++) {
if (matrix[0][v]) neighborsOf0.add(v);
}
System.out.println("neighbors of 0: " + neighborsOf0);
}
}edge 0 -> 2? true
neighbors of 0: [1, 2]Edge list
A plain int[][] where each row is {u, v}. It's the usual input format, and some algorithms work on it directly (union-find, Kruskal's algorithm). Most traversals convert it to an adjacency list first.
Comparing the representations
| Adjacency list | Adjacency matrix | Edge list | |
|---|---|---|---|
| space | O(V + E) |
O(V²) |
O(E) |
is there an edge u → v? |
O(deg u) |
O(1) |
O(E) |
list the neighbors of v |
O(deg v) |
O(V) |
O(E) |
| add an edge | O(1) |
O(1) |
O(1) |
| best for | most problems (sparse graphs) | dense graphs, many edge checks | union-find, sorting edges by weight |
Vertices with names
When vertices are strings or other values, either give each name an index first, or use a HashMap of neighbor lists directly (computeIfAbsent creates the list on the first edge from a name, and putIfAbsent stores a value only when the key is missing):
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Main {
public static void main(String[] args) {
String[][] flights = {{"SFO", "JFK"}, {"SFO", "SEA"}, {"SEA", "JFK"}, {"JFK", "LHR"}};
// Option 1: a HashMap from each name to its neighbors
Map<String, List<String>> adj = new HashMap<>();
for (String[] f : flights) {
adj.computeIfAbsent(f[0], k -> new ArrayList<>()).add(f[1]);
}
System.out.println("from SFO: " + adj.get("SFO"));
// Option 2: map names to indices once, then use a List<List<Integer>>
Map<String, Integer> id = new HashMap<>();
for (String[] f : flights) {
for (String name : f) {
id.putIfAbsent(name, id.size());
}
}
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < id.size(); i++) graph.add(new ArrayList<>());
for (String[] f : flights) {
graph.get(id.get(f[0])).add(id.get(f[1]));
}
int sfo = id.get("SFO");
System.out.println(id.size() + " airports, SFO is vertex " + sfo + " with neighbors " + graph.get(sfo));
}
}from SFO: [JFK, SEA]
4 airports, SFO is vertex 0 with neighbors [1, 2]Keeping track of visited vertices
Because graphs can have cycles, every traversal must remember which vertices it has already processed, or it may loop forever. With vertices numbered 0..V, a boolean[n] is simpler and faster than a HashSet. When vertices are not numbered, Set.add marks a value and returns false if it was already in the set:
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<Integer> seen = new HashSet<>();
boolean first = !seen.add(1); // add returns false when the value was already present
boolean second = !seen.add(1);
System.out.println(first + " " + second);
}
}false trueWhich algorithm for which question
| Question | Algorithm | Time | Lesson |
|---|---|---|---|
| visit every vertex | BFS or DFS | O(V + E) |
BFS, DFS |
can a reach b? |
BFS or DFS | O(V + E) |
Path exists |
fewest edges from a to b |
BFS | O(V + E) |
Shortest path |
| cheapest path, non-negative weights | Dijkstra with a min-heap | O((V + E) log V) |
|
| does a directed graph have a cycle? | DFS with three states, or topological sort | O(V + E) |
Detect a cycle |
| a vertex that reaches all others | DFS, keeping the last root | O(V + E) |
Mother vertex |
| is an undirected graph a tree? / connected parts | union-find | about O(E) |
Graph valid tree |
| order tasks by their dependencies | topological sort | O(V + E) |
