前言
图论是计算机科学中一个极其重要的领域,它研究的是由节点(顶点)和边组成的图结构。无论是在算法竞赛、面试准备,还是在实际的软件开发中,图论算法都扮演着至关重要的角色。
本文旨在为 Java 开发者提供一个实用的图论算法指南,涵盖了从基础的 DFS/BFS 遍历,到经典的拓扑排序、最短路径、最小生成树等核心算法。每个算法都配有清晰的 Java 实现代码和关键思路解析,力求让你能够:
- 理解原理:掌握每个算法的核心思想和适用场景
- 快速上手:获得可直接使用的 Java 代码模板
- 灵活应用:学会根据实际问题选择合适的图论算法
文章结构从易到难,先通过 LeetCode 经典题目(如岛屿数量、腐烂的橘子)引入基础概念,再深入讲解更复杂的算法模板。最后,我们还提供了一个算法选择速查表,帮助你在面对具体问题时快速定位合适的算法。
无论你是正在准备技术面试,还是希望在项目中应用图论算法,相信这篇文章都能为你提供有价值的参考。让我们开始探索图论的奇妙世界吧!
图论
文章目录
DFS
java
void dfs(int[][] grid, int r, int c) {
// 判断 base case
// 如果坐标 (r, c) 超出了网格范围,直接返回
if (!inArea(grid, r, c)) {
return;
}
// 访问上、下、左、右四个相邻结点
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
return 0 <= r && r < grid.length
&& 0 <= c && c < grid[0].length;
}
200.岛屿数量
- 0 ------ 海洋格子
- 1 ------ 陆地格子(未遍历过)
- 2 ------ 陆地格子(已遍历过)
java
class Solution {
public int numIslands(char[][] grid) {
int count = 0;
//来遍历整张二维表格中所有的陆地
for(int i = 0; i<grid.length;i++){
for(int j =0;j<grid[0].length;j++){
//取出所有的陆地
if(grid[i][j] == '1'){
//深度递归,遍历所有的陆地
dfs(grid,i,j);
count++;
}
}
}
return count;
}
public void dfs(char[][] grid,int i, int j){
if(i<0||j<0||i>=grid.length||j>=grid[0].length||grid[i][j]=='0') return;
//将遍历过的陆地改为海洋,防止重复遍历
grid[i][j]='0';
dfs(grid,i+1,j);
dfs(grid,i-1,j);
dfs(grid,i,j+1);
dfs(grid,i,j-1);
}
}
994. 腐烂的橘子
- DFS 是一条线,适合探索未知;
- BFS 是一圈圈的水波纹,适合计算"最短时间/最少步数
所以这题应该用多源BFS
怎么判断所有橘子是否全部腐烂?
开始定义一个变量记录新鲜橘子的数量,结束后如果新鲜橘子数量不为0就返回-1

java
class Solution {
public int orangesRotting(int[][] grid) {
Queue<int[]> q = new ArrayDeque<>();
int time = 0,fresh = 0;
//初始化
for(int i = 0;i<grid.length;i++){
for(int j =0;j<grid[0].length;j++){
if(grid[i][j] == 1){
fresh++;//得到新鲜橘子的个数
}else if(grid[i][j] == 2){
q.add(new int[]{i,j});//让腐烂的橘子进队
}
}
}
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
//队列不为空,并且新鲜橘子数量大于0的情况下
//访问队列中的所有元素,把相邻的新鲜橘子入队
while(!q.isEmpty() && fresh > 0){
int qLen = q.size();
for(int i=0;i<qLen;i++){
int[] cur = q.poll();
//判断相邻是否有新鲜橘子 有就入队
for(int[] dir : dirs){
int r = cur[0] + dir[0];
int c = cur[1] + dir[1];
if(r<0||r>=grid.length||c<0||c>=grid[0].length||grid[r][c]!=1){
//不满足条件
continue;
}
grid[r][c] = 2;
q.add(new int[]{r,c});
fresh--;
}
}
time++;
}
if(fresh > 0)return -1;
else return time;
}
}
拓扑排序
必须在有向无环图中才能进行下去
每次选入度为0 的点,然后删除这个点和它的出边
用一个数组记录每个节点的入度,再通过BFS对有向图进行拓扑排序
207. 课程表
本题用的就是拓扑排序
count用于记录遍历过的节点数(或者直接用numCourses--),数组ingree用于标记每个节点的入度数,列表preCourse标记每个节点的关联,队列q用于操作每个入度为0的节点
java
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
int[] ingree = new int[numCourses];
List<List<Integer>> preCourse = new ArrayList<>();
//初始化列表里的子列表
for (int i = 0; i < numCourses; i++) {
preCourse.add(new ArrayList<>());
}
//初始化ingree和preCourse 记录每个节点的入度
for(int[] temp : prerequisites){
preCourse.get(temp[1]).add(temp[0]);
ingree[temp[0]]++;
}
Queue<Integer> q = new ArrayDeque<>();
//初始化队列
for(int i= 0;i<numCourses;i++){
if(ingree[i] == 0){
q.add(i);
}
}
while(!q.isEmpty()){
int cur = q.poll();
numCourses--;
for(int temp : preCourse.get(cur)){
ingree[temp]--;
if(ingree[temp] == 0){
q.add(temp);
}
}
}
return numCourses == 0;
}
}
208. 实现 Trie (前缀树)

1.核心数据结构:26 叉树
每个 Node 节点包含:
son[26]:一个长度为 26 的数组,分别对应 a~z 26 个字母。如果某个字母存在,对应位置就指向下一个节点;如果不存在,就是null。end:标记从根节点到当前节点的路径是否构成一个完整的单词。
2.三个核心操作的逻辑
insert(String word) --- 插入单词
从根节点出发,逐个字符往下走:
- 如果当前字符对应的子节点不存在 (
son[c] == null),就new一个新节点。 - 如果已存在,直接走过去。
- 走到最后一个字符时,把
end标记为true,表示这里是一个完整单词的结尾。
find(String word) --- 统一查找(核心设计)
从根节点出发,逐个字符往下走:
- 如果中途某个字符对应的子节点为
null,说明这条路根本不存在,直接返回0(未找到)。 - 如果顺利走完了所有字符,说明至少前缀匹配 成功,返回
1。 - 如果走完后发现
end == true,说明这是一个完整单词 ,返回2。
3.search 和 startsWith --- 调用 find 做判断
search:必须返回2(完全匹配),即这个单词确实被插入过。startsWith:只要返回1或2都行(!= 0),即只要前缀存在即可
java
class Trie {
private static class Node {
Node[] son = new Node[26];
boolean end = false;
}
private final Node root = new Node();
public Trie() {
}
public void insert(String word) {
Node cur = root;
for (char c : word.toCharArray()) {
c -= 'a';
if (cur.son[c] == null) { // 无路可走
cur.son[c] = new Node(); // new一个新的
}
cur = cur.son[c];
}
cur.end = true;
}
public boolean search(String word) {
return find(word) == 2 ? true : false;
}
public boolean startsWith(String prefix) {
//完全匹配也算前缀匹配
return find(prefix) != 0 ? true : false;
}
public int find(String word){
Node cur = root;
for(char c : word.toCharArray()){
c -= 'a';
if(cur.son[c] == null){
return 0;
}
cur = cur.son[c];
}
//2:完全匹配,1:前缀匹配
return cur.end ? 2 : 1;
}
}
/**
* Your Trie object will be instantiated and called as such:
* Trie obj = new Trie();
* obj.insert(word);
* boolean param_2 = obj.search(word);
* boolean param_3 = obj.startsWith(prefix);
*/
总结
下面把之前 C++ 的图论模板全部翻译成 Java 版 ,统一使用 List<List<int[]>> 作为图的存储结构,和你之前写课程表时用的风格完全一致,拿来就能用。
一、建图模板
java
// 建图:graph.get(u) 存的是 u 的所有出边,每条边是 int[]{终点, 边权}
List<List<int[]>> graph = new ArrayList<>();
for (int i = 0; i < n; i++) {
graph.add(new ArrayList<>());
}
// 添加有向边 u -> v,权值为 w
graph.get(u).add(new int[]{v, w});
// 添加无向边 u <-> v,权值为 w(加两条)
graph.get(u).add(new int[]{v, w});
graph.get(v).add(new int[]{u, w});
二、堆优化 Dijkstra
适用场景 :求一个起点到所有点的最短距离,边权非负。时间复杂度 O ( m log n ) O(m \log n) O(mlogn)。
java
public int[] dijkstra(List<List<int[]>> graph, int start) {
int n = graph.size();
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[start] = 0;
boolean[] visited = new boolean[n];
// 小根堆:存 {距离, 节点}
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
pq.offer(new int[]{0, start});
while (!pq.isEmpty()) {
int[] cur = pq.poll();
int d = cur[0], u = cur[1];
if (visited[u]) continue; // 已确定最短路,跳过
visited[u] = true;
for (int[] edge : graph.get(u)) {
int v = edge[0], w = edge[1];
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.offer(new int[]{dist[v], v});
}
}
}
return dist;
}
三、SPFA
适用场景:有负权边但无负环。平均 O(m),最坏O(nm)
java
public int[] spfa(List<List<int[]>> graph, int start) {
int n = graph.size();
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[start] = 0;
boolean[] inQueue = new boolean[n];
Queue<Integer> q = new ArrayDeque<>();
q.offer(start);
inQueue[start] = true;
while (!q.isEmpty()) {
int u = q.poll();
inQueue[u] = false;
for (int[] edge : graph.get(u)) {
int v = edge[0], w = edge[1];
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
if (!inQueue[v]) {
q.offer(v);
inQueue[v] = true;
}
}
}
}
return dist;
}
四、Floyd
java
public int[][] floyd(int[][] g) {
int n = g.length;
// g[i][j] 初始为 i 到 j 的直接边权,无直接边则为 INF
for (int k = 0; k < n; k++) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (g[i][k] != Integer.MAX_VALUE && g[k][j] != Integer.MAX_VALUE) {
g[i][j] = Math.min(g[i][j], g[i][k] + g[k][j]);
}
}
}
}
return g;
}
⚠️ 注意 :
k必须在最外层!这是 Floyd 的核心。
五、Kruskal
适用场景:连接所有点且总边权最小。时间复杂度 O(m \log m)
java
// 并查集(Kruskal 的搭档)
class UnionFind {
int[] parent;
public UnionFind(int n) {
parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
}
public int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]); // 路径压缩
return parent[x];
}
public boolean union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) return false; // 已在同一集合,会形成环
parent[ra] = rb;
return true;
}
}
// Kruskal 主逻辑
public int kruskal(int n, int[][] edges) {
// edges 每行是 {u, v, weight}
Arrays.sort(edges, (a, b) -> a[2] - b[2]); // 按边权升序排序
UnionFind uf = new UnionFind(n);
int totalWeight = 0, edgeCount = 0;
for (int[] edge : edges) {
int u = edge[0], v = edge[1], w = edge[2];
if (uf.union(u, v)) { // 不形成环才加入
totalWeight += w;
if (++edgeCount == n - 1) break; // 凑够 n-1 条边即停
}
}
return edgeCount == n - 1 ? totalWeight : -1; // -1 表示无法连通
}
六、染色法判定二分图
适用场景:判断图能否被分成两组,使得同组内无边。时间复杂度O(n + m)
java
public boolean isBipartite(List<List<int[]>> graph) {
int n = graph.size();
int[] color = new int[n]; // 0: 未染色, 1: 颜色A, 2: 颜色B
for (int i = 0; i < n; i++) {
if (color[i] == 0) { // 未访问过的连通块
Queue<Integer> q = new ArrayDeque<>();
q.offer(i);
color[i] = 1;
while (!q.isEmpty()) {
int u = q.poll();
for (int[] edge : graph.get(u)) {
int v = edge[0];
if (color[v] == 0) {
color[v] = 3 - color[u]; // 染相反颜色
q.offer(v);
} else if (color[v] == color[u]) {
return false; // 相邻节点颜色相同,不是二分图
}
}
}
}
}
return true;
}
算法选择速查表
| 题目特征 | 优先算法 | 关键判断点 |
|---|---|---|
| 无权图最短路 / 最少步数 | BFS | 队列,分层 |
| 连通块 / 路径存在性 / 回溯 | DFS | 递归栈 |
| 任务依赖 / 判断环 / 拓扑序 | 拓扑排序 | 入度为0入队 |
| 单源最短路,边权 ≥ 0 | Dijkstra (堆) | 优先队列 + visited[] |
| 单源最短路,有负权边 | SPFA | 队列 + inQueue[] |
| 任意两点最短路,n ≤ 500 | Floyd | 三重循环,k 在最外层 |
| 最小生成树,稀疏图 | Kruskal | 边排序 + 并查集 |
| 二分图判定 | 染色法 | 相邻点颜色不同 |