跳到主要内容图遍历算法详解:DFS 与 BFS 原理及 Java 实现 | 极客日志Javajava算法
图遍历算法详解:DFS 与 BFS 原理及 Java 实现
综述由AI生成图的两种核心遍历算法:深度优先搜索(DFS)和广度优先搜索(BFS)。内容涵盖定义、思想、实现依赖(栈与队列)、步骤演示、Java 代码实现(递归与非递归)、时间复杂度分析及应用场景。通过邻接表结构,展示了如何避免重复访问、处理非连通图及求解无权图最短路径。对比了两者在辅助结构、访问顺序及适用场景上的差异,为实际开发中根据需求选择合适算法提供依据。
利刃24 浏览 
一、图的遍历介绍
1.1 遍历的定义与要求
图的遍历是指从图中任意指定顶点(初始点)出发,按照特定搜索策略沿着边访问所有顶点且每个顶点仅访问一次的过程,遍历得到的顶点序列称为遍历序列。
要求:全访问、不重复——这是因为图存在环和多对多连接,相比二叉树(无环、一对多),遍历需增加访问标记避免重复访问,是图遍历的关键要点。
1.2 两种搜索策略
根据访问顶点的顺序不同,图的遍历分为深度优先搜索(DFS)和广度优先搜索(BFS),二者是图论中最基础、应用最广泛的算法,分别基于栈和队列实现,适配不同的业务场景:
- DFS:深度优先,'一条路走到黑',适合判断连通性(是否存在路径);
- BFS:广度优先,'地毯式层层推进',适合求解最短路径(无权图)。
1.3 与二叉树遍历的关联与差异
二叉树的前/中/后序遍历本质是特殊的图遍历(二叉树是无环的连通图),但图的遍历更复杂,差异如下:
| 对比维度 | 二叉树遍历 | 图的遍历 |
|---|
| 结构特性 | 无环、一对多、连通 | 可能有环、多对多、可能非连通 |
| 访问标记 | 无需(无重复路径) | 必须有(数组/集合标记,避免环导致重复访问) |
| 辅助结构 | 递归(隐式栈)/显式栈 | DFS 用栈、BFS 用队列 |
| 遍历结果 | 唯一(固定左右子树顺序) | 不唯一(依赖邻接顶点的访问顺序) |
二、深度优先搜索(DFS,Depth First Search)
2.1 思想
DFS 的是**'深度优先,回溯探索',类似走迷宫:从起点出发,选择一个方向一直往前走,直到走到'死胡同'(当前顶点无未访问的邻接顶点),再回溯到上一个顶点,尝试其他未走的方向,直到遍历所有顶点。
简单总结:先深后广,遇阻回溯。
2.2 实现依赖
DFS 的实现依赖栈(Stack)(后进先出 LIFO),有两种实现方式:
- 递归实现:利用 JVM 的方法调用栈(隐式栈),代码更简洁,适合小规模图;
- 非递归实现:显式创建栈对象,手动控制入栈/出栈,适合大规模图(避免栈溢出)。
无论哪种方式,都需要访问标记数组/集合(如 visited[]),标记顶点是否已被访问。
2.3 遍历步骤(以顶点 A 为起点,邻接顶点按字母序访问)

以包含顶点 A、B、C、D、E、F、G、H、aoyou 的连通图为例,步骤可概括为**'入栈标记→深度探索→死胡同出栈→回溯再探索'**,关键节点:
- 起点 A 入递归调用栈,标记已访问,探索邻接顶点(字母序:B、D、G、aoyou),首先访问 B,B 入栈标记;
- 从 B 探索未访问邻接顶点(字母序:E、F),首先访问 E,E 入栈标记;
- E 无邻接顶点(或邻接点均无出边),为死胡同,回溯到 B;
- B 继续探索下一个未访问邻接顶点 F,F 入栈标记;
- 从 F 探索未访问邻接顶点 C,C 入栈标记;
- 从 C 探索未访问邻接顶点 H,H 入栈标记;
- H 无邻接顶点,回溯到 C;C 无其他邻接点,回溯到 F;F 无剩余未访问邻接点,回溯到 B;B 邻接点已全部访问,回溯到 A;
- A 继续探索下一个未访问邻接顶点 D,D 入栈标记;
- D 的邻接点为 E 和 F,但 E 和 F 均已被访问,D 无新路径,回溯到 A;
- A 继续探索下一个未访问邻接顶点 G,G 入栈标记;
- G 无邻接顶点,回溯到 A;
- A 继续探索最后一个未访问邻接顶点 aoyou,aoyou 入栈标记;
- aoyou 的邻接点为 G,但 G 已被访问,aoyou 无新路径,回溯到 A;
- A 所有邻接点均已访问,递归结束,遍历完成。
2.4 代码实现(邻接表版,递归 + 非递归)
基于之前的邻接表图结构,实现 DFS 的递归版(简洁)和非递归版(通用),融入 aoyou 顶点,贴合示例要求:
import java.util.*;
public class AoyouGraphDFS {
static class Vertex {
String name;
Edge next;
public Vertex(String name, Edge next) {
this.name = name;
this.next = next;
}
}
static class Edge {
String target;
int weight;
Edge next;
public Edge(String target, int weight, Edge next) {
this.target = target;
this.weight = weight;
this.next = next;
}
}
private Map<String, Vertex> vertexMap;
private Set<String> visited;
public AoyouGraphDFS() {
vertexMap = new HashMap<>();
visited = new HashSet<>();
}
public void insertVertex(String name) {
vertexMap.putIfAbsent(name, new Vertex(name, null));
}
public void insertEdge(String begin, String end, int weight) {
Vertex beginV = vertexMap.get(begin);
Edge newEdge = new Edge(end, weight, beginV.next);
beginV.next = newEdge;
}
public void dfsRecursive(String start) {
if (!vertexMap.containsKey(start) || visited.contains(start)) {
return;
}
System.out.print(start + " → ");
visited.add(start);
Vertex current = vertexMap.get(start);
Edge edge = current.next;
List<String> neighbors = new ArrayList<>();
while (edge != null) {
neighbors.add(edge.target);
edge = edge.next;
}
Collections.sort(neighbors);
for (String neighbor : neighbors) {
if (!visited.contains(neighbor)) {
dfsRecursive(neighbor);
}
}
}
public void dfsNonRecursive(String start) {
if (!vertexMap.containsKey(start)) {
return;
}
Stack<String> stack = new Stack<>();
Set<String> visited = new HashSet<>();
stack.push(start);
visited.add(start);
System.out.print("非递归 DFS:");
while (!stack.isEmpty()) {
String current = stack.pop();
System.out.print(current + " → ");
Vertex v = vertexMap.get(current);
Edge edge = v.next;
List<String> neighbors = new ArrayList<>();
while (edge != null) {
neighbors.add(edge.target);
edge = edge.next;
}
Collections.sort(neighbors, Collections.reverseOrder());
for (String neighbor : neighbors) {
if (!visited.contains(neighbor)) {
stack.push(neighbor);
visited.add(neighbor);
}
}
}
}
public void resetVisited() {
visited.clear();
}
public static void main(String[] args) {
AoyouGraphDFS graph = new AoyouGraphDFS();
String[] vertexes = {"A", "B", "C", "D", "E", "F", "G", "H", "aoyou"};
for (String v : vertexes) {
graph.insertVertex(v);
}
graph.insertEdge("A", "B", 4);
graph.insertEdge("A", "D", 5);
graph.insertEdge("A", "G", 1);
graph.insertEdge("A", "aoyou", 8);
graph.insertEdge("B", "E", 2);
graph.insertEdge("B", "F", 3);
graph.insertEdge("D", "F", 4);
graph.insertEdge("D", "E", 3);
graph.insertEdge("F", "C", 2);
graph.insertEdge("C", "H", 5);
graph.insertEdge("aoyou", "G", 2);
System.out.print("递归版 DFS:");
graph.dfsRecursive("A");
System.out.println("结束");
graph.dfsNonRecursive("A");
System.out.println("结束");
}
}
2.5 运行结果
递归版 DFS:A → B → E → F → C → H → D → G → aoyou → 结束 非递归 DFS:A → B → E → F → C → H → D → G → aoyou → 结束
2.6 时间复杂度
DFS 的时间复杂度与图的存储方式相关,取决于访问所有顶点和遍历所有边的开销:
邻接矩阵存储:查找每个顶点的邻接顶点需遍历整行,耗时 O(V^2),总时间复杂度 O(V^2)。
邻接表存储:访问所有顶点耗时 O(V)(V 为顶点数),遍历所有边耗时 O(E)(E 为边数),总时间复杂度 O(V+E);
2.7 应用场景
DFS 专注于**'是否存在路径'**,不关心路径长度,典型应用:
- 判断图的连通性(无向图是否为连通图、有向图是否强连通);
- 寻找图中的环(如检测任务依赖是否有循环);
- 拓扑排序(有向无环图 DAG)、迷宫探索、排列组合问题;
- 深度优先的搜索遍历(如文件夹的递归遍历,本质是树的 DFS)。
三、广度优先搜索(BFS,Breadth First Search)
3.1 思想
BFS 的是**'广度优先,层层推进'**,类似水面波纹扩散:从起点出发,先访问起点的所有邻接顶点(第一层),再依次访问第一层每个顶点的邻接顶点(第二层),以此类推,直到遍历所有顶点。
简单总结:先广后深,层层遍历。
3.2 实现依赖
BFS 的实现依赖队列(Queue)(先进先出 FIFO),必须显式实现(无递归版),同时需要访问标记数组/集合,原因:队列保证'层层推进'的顺序,标记避免同一顶点被多次入队。
3.3 遍历步骤(以顶点 A 为起点,邻接顶点按字母序入队)
以上述连通图为例,BFS 的是**'入队标记→出队访问→邻接顶点入队标记'**,层层遍历:
- 起点 A 入队,标记已访问;
- A 出队并访问,将 A 的邻接顶点 B、D、G、aoyou(字母序)依次入队,标记已访问(第一层);
- B 出队并访问,将 B 的未访问邻接顶点 E、F 入队,标记(第二层);
- D 出队并访问,其邻接顶点均已标记,无入队;G 出队并访问,无未访问邻接顶点;aoyou 出队并访问,无未访问邻接顶点;
- E 出队并访问,无未访问邻接顶点;F 出队并访问,将 F 的未访问邻接顶点 C 入队,标记(第三层);
- C 出队并访问,将 C 的未访问邻接顶点 H 入队,标记(第四层);
- H 出队并访问,无未访问邻接顶点;队列为空,遍历结束。
3.4 代码实现(邻接表版,求解最短路径)
基于同一邻接表结构,实现 BFS 遍历,并增加无权图最短路径求解功能(BFS 的经典应用):
import java.util.*;
public class AoyouGraphBFS {
static class Vertex {
String name;
Edge next;
public Vertex(String name, Edge next) {
this.name = name;
this.next = next;
}
}
static class Edge {
String target;
int weight;
Edge next;
public Edge(String target, int weight, Edge next) {
this.target = target;
this.weight = weight;
this.next = next;
}
}
private Map<String, Vertex> vertexMap;
public AoyouGraphBFS() {
vertexMap = new HashMap<>();
}
public void insertVertex(String name) {
vertexMap.putIfAbsent(name, new Vertex(name, null));
}
public void insertEdge(String begin, String end, int weight) {
Vertex beginV = vertexMap.get(begin);
Edge newEdge = new Edge(end, weight, beginV.next);
beginV.next = newEdge;
}
public void bfsTraverse(String start) {
if (!vertexMap.containsKey(start)) {
System.out.println("起点顶点不存在!");
return;
}
Queue<String> queue = new LinkedList<>();
Set<String> visited = new HashSet<>();
queue.offer(start);
visited.add(start);
System.out.print("BFS 遍历序列:");
while (!queue.isEmpty()) {
String current = queue.poll();
System.out.print(current + " → ");
Vertex v = vertexMap.get(current);
Edge edge = v.next;
List<String> neighbors = new ArrayList<>();
while (edge != null) {
neighbors.add(edge.target);
edge = edge.next;
}
Collections.sort(neighbors);
for (String neighbor : neighbors) {
if (!visited.contains(neighbor)) {
queue.offer(neighbor);
visited.add(neighbor);
}
}
}
System.out.println("结束");
}
public int bfsShortestPath(String start, String target) {
if (!vertexMap.containsKey(start) || !vertexMap.containsKey(target)) {
return -1;
}
if (start.equals(target)) {
return 0;
}
Queue<String> queue = new LinkedList<>();
Set<String> visited = new HashSet<>();
Map<String, Integer> pathLen = new HashMap<>();
queue.offer(start);
visited.add(start);
pathLen.put(start, 0);
while (!queue.isEmpty()) {
String current = queue.poll();
Vertex v = vertexMap.get(current);
Edge edge = v.next;
while (edge != null) {
String neighbor = edge.target;
if (neighbor.equals(target)) {
return pathLen.get(current) + 1;
}
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.offer(neighbor);
pathLen.put(neighbor, pathLen.get(current) + 1);
}
edge = edge.next;
}
}
return -1;
}
public static void main(String[] args) {
AoyouGraphBFS graph = new AoyouGraphBFS();
String[] vertexes = {"A", "B", "C", "D", "E", "F", "G", "H", "aoyou"};
for (String v : vertexes) {
graph.insertVertex(v);
}
graph.insertEdge("A", "B", 4);
graph.insertEdge("A", "D", 5);
graph.insertEdge("A", "G", 1);
graph.insertEdge("A", "aoyou", 8);
graph.insertEdge("B", "E", 2);
graph.insertEdge("B", "F", 3);
graph.insertEdge("D", "F", 4);
graph.insertEdge("D", "E", 3);
graph.insertEdge("F", "C", 2);
graph.insertEdge("C", "H", 5);
graph.insertEdge("aoyou", "G", 2);
graph.bfsTraverse("A");
System.out.println("A 到 aoyou 的最短路径长度:" + graph.bfsShortestPath("A", "aoyou"));
System.out.println("A 到 H 的最短路径长度:" + graph.bfsShortestPath("A", "H"));
System.out.println("aoyou 到 H 的最短路径长度:" + graph.bfsShortestPath("aoyou", "H"));
}
}
3.5 运行结果
BFS 遍历序列:A → B → D → G → aoyou → E → F → C → H → 结束 A 到 aoyou 的最短路径长度:1 A 到 H 的最短路径长度:4 aoyou 到 H 的最短路径长度:-1
3.6 时间复杂度
BFS 的时间复杂度与 DFS完全一致,仅依赖存储方式,与遍历策略无关:
- 邻接表存储:访问顶点 + 遍历边 → 总 O(V+E);
- 邻接矩阵存储:查找邻接顶点 → 总 O(V^2)。
3.7 应用场景
BFS 专注于**'最短路径'**(无权图),层层推进的特性使其适合多源遍历,典型应用:
- 求解无权图的最短路径(如迷宫的最短走出路线、社交网络的一度/二度好友);
- 社交网络的好友推荐(一度好友、二度好友查找,层层推进);
- 多源 BFS(如洪水填充、地图上的多起点路径规划);
- 双端 BFS(从起点和终点同时 BFS,大幅提升长路径搜索效率);
- 图的连通性判断、层序遍历(如二叉树的层序遍历是特殊的 BFS)。
四、DFS 与 BFS 对比
从思想、辅助结构、访问顺序、适用场景等 8 个维度做全面对比,是实际开发中选择遍历算法的关键依据:
| 对比维度 | 深度优先搜索(DFS) | 广度优先搜索(BFS) |
|---|
| 思想 | 深度优先,一条路走到黑,遇阻回溯 | 广度优先,地毯式层层推进,逐层遍历 |
| 辅助结构 | 栈(递归:隐式方法栈;非递归:显式栈) | 队列(LinkedList,显式实现) |
| 访问顺序 | 先深后广,不按距离排序 | 先广后深,按到起点的距离(边数)排序 |
| 特性 | 不保证最短路径,适合连通性判断 | 保证无权图的最短路径,适合路径求解 |
| 空间复杂度 | 最坏 O(V)(栈存储顶点) | 最坏 O(V)(队列存储顶点) |
| 时间复杂度 | 邻接表 O(V+E),邻接矩阵 O(V^2) | 与 DFS 完全一致 |
| 实现方式 | 递归(简洁)、非递归(通用) | 仅显式队列实现(无递归) |
| 典型应用 | 连通性判断、找环、拓扑排序、迷宫探索 | 无权图最短路径、好友推荐、双端 BFS、洪水填充 |
五、注意事项
- 访问标记是必须的:图存在环,必须用
visited 数组/集合标记已访问顶点,否则会陷入无限循环;
- 非连通图的遍历:上述代码均基于连通图,若图为非连通图,需遍历所有顶点,对未标记的顶点重新执行 DFS/BFS;
- 邻接顶点的访问顺序:遍历结果不唯一,依赖邻接顶点的访问顺序(如字母序、插入序),可通过排序保证顺序一致;
- 有向图与无向图的适配:无向图的边是双向的,插入边时需同时插入
u→v 和 v→u,否则会出现遍历不完整;
- 栈溢出问题:递归版 DFS 适合小规模图,大规模图需用非递归版 DFS(显式栈),避免 JVM 方法栈溢出;
- 最短路径的适用范围:BFS 仅能求解无权图的最短路径,带权图的最短路径需用 Dijkstra、Floyd 等算法。
六、总结
- 图的遍历要求访问所有顶点且仅访问一次,因图有环和多对多连接,访问标记是实现的关键,分为 DFS 和 BFS 两种策略;
- DFS 基于栈实现,是'深度优先、回溯探索',适合判断连通性,不保证最短路径,有递归(简洁)和非递归(通用)两种实现;
- BFS 基于队列实现,是'地毯式层层推进',是无权图最短路径的最优解,典型应用为社交网络好友查找、迷宫最短路径;
- DFS 和 BFS 的时间复杂度一致:邻接表存储 O(V+E),邻接矩阵存储 O(V^2),空间复杂度均为最坏 O(V);
- 实际开发中,根据业务需求选择算法:判断是否存在路径用 DFS,求解无权图最短路径用 BFS;
- 非连通图需遍历所有顶点,对未标记顶点重新执行遍历;无向图插入边时需双向插入,保证遍历完整。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online