一、故事场景:迷宫中的勇往直前
小明又被困在了一个迷宫里,但这次他选择了不同的策略:
-
选择一个方向:比如一直向右走
-
勇往直前:遇到岔路时,选择第一个看到的方向继续走,不回头
-
遇到死胡同就回溯:如果走不通,回到上一个岔路口,尝试下一个方向
-
标记已走过的路:在墙上画箭头,避免重复走
这就是 ** 深度优先搜索(DFS)** 的核心思想:一条路走到黑,走不通再回头。
二、DFS 的核心概念
- 递归(或栈) :用递归函数(或显式栈)来实现回溯机制
- 访问标记:记录已访问的节点,避免重复访问
- 回溯:当当前路径走不通时,返回上一步继续探索
三、Java 代码实现:迷宫搜索
java
public class DepthFirstSearch {
// 迷宫搜索示例
public static void main(String[] args) {
// 迷宫:0表示通路,1表示墙
int[][] maze = {
{0, 1, 0, 0, 0},
{0, 1, 0, 1, 0},
{0, 0, 0, 0, 0},
{0, 1, 1, 1, 0},
{0, 0, 0, 1, 0}
};
int startX = 0, startY = 0; // 起点坐标
int endX = 4, endY = 4; // 终点坐标
// 创建访问标记数组
boolean[][] visited = new boolean[maze.length][maze[0].length];
// 调用DFS方法搜索路径
boolean found = dfs(maze, startX, startY, endX, endY, visited);
if (found) {
System.out.println("恭喜!找到出口了!");
} else {
System.out.println("遗憾!没有找到出口。");
}
}
public static boolean dfs(int[][] maze, int x, int y, int endX, int endY, boolean[][] visited) {
// 检查坐标是否越界或为墙或已访问
if (x < 0 || x >= maze.length || y < 0 || y >= maze[0].length
|| maze[x][y] == 1 || visited[x][y]) {
return false;
}
// 标记当前位置为已访问
visited[x][y] = true;
// 检查是否到达终点
if (x == endX && y == endY) {
return true;
}
// 定义四个方向:上、下、左、右
// 注意:这里的顺序会影响搜索路径的选择
int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
// 尝试所有方向
for (int[] dir : directions) {
int newX = x + dir[0];
int newY = y + dir[1];
// 递归搜索新位置
if (dfs(maze, newX, newY, endX, endY, visited)) {
return true; // 找到路径,立即返回
}
}
// 所有方向都走不通,回溯
return false;
}
}
四、DFS 的执行过程详解
-
初始化:
- 从起点
(0,0)开始 - 标记起点为已访问
- 从起点
-
递归深入:
- 尝试向右走
(0,1),是墙,失败 - 尝试向下走
(1,0),是通路,继续递归 - 从
(1,0)尝试向下走(2,0),继续递归 - 从
(2,0)尝试向右走(2,1),继续递归 - 从
(2,1)尝试各个方向,均失败,回溯到(2,0)
- 尝试向右走
-
回溯与重试:
- 从
(2,0)尝试向下走(3,0),继续递归 - 重复上述过程,直到找到终点或所有路径都探索完毕
- 从
五、递归 vs 显式栈
上面的代码使用了递归实现 DFS,也可以用显式栈来模拟:
java
import java.util.Stack;
public class DepthFirstSearchWithStack {
public static boolean dfsWithStack(int[][] maze, int startX, int startY, int endX, int endY) {
int rows = maze.length;
int cols = maze[0].length;
boolean[][] visited = new boolean[rows][cols];
Stack<int[]> stack = new Stack<>();
// 将起点压入栈
stack.push(new int[]{startX, startY});
visited[startX][startY] = true;
// 定义四个方向
int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
while (!stack.isEmpty()) {
int[] current = stack.pop();
int x = current[0];
int y = current[1];
// 检查是否到达终点
if (x == endX && y == endY) {
return true;
}
// 尝试所有方向(注意:栈是后进先出,所以方向顺序要反过来)
for (int i = directions.length - 1; i >= 0; i--) {
int newX = x + directions[i][0];
int newY = y + directions[i][1];
if (newX >= 0 && newX < rows && newY >= 0 && newY < cols
&& maze[newX][newY] == 0 && !visited[newX][newY]) {
stack.push(new int[]{newX, newY});
visited[newX][newY] = true;
}
}
}
return false;
}
}
六、DFS vs BFS:不同的搜索策略
| 维度 | 深度优先搜索(DFS) | 广度优先搜索(BFS) |
|---|---|---|
| 数据结构 | 栈(递归或显式栈) | 队列(Queue) |
| 搜索顺序 | 一条路走到黑,回溯再探索 | 逐层扩展,先近后远 |
| 适用场景 | 所有路径探索、连通性检查 | 最短路径、最小步数问题 |
| 空间复杂度 | O (H)(H 为最大深度) | O (V)(最坏情况下存储整层节点) |
七、DFS 的其他应用场景
- 家谱寻根:查找家族中某人的所有后代
- 游戏 AI:探索游戏地图的所有可能路径
- 拓扑排序:在有向无环图中进行任务排序
- 连通分量:找出图中所有独立的连通区域
八、总结:DFS 的核心思想
DFS 就像在迷宫中勇往直前:
-
一条路走到黑:从起点开始,沿着一条路径尽可能深入
-
回溯机制:当遇到死胡同时,返回上一个岔路口,尝试其他方向
-
递归或栈:用递归函数或显式栈来管理回溯过程
掌握 DFS,就像掌握了在复杂网络中探索所有可能路径的方法,是算法学习的重要基础。