大家好,我是唐叔。今天我们要探讨的是一个在图论和树结构中另一种非常重要的算法——深度优先遍历(Depth-First Search, DFS)。它以一种深入探索的方式从起点出发,尽可能地沿着一条路径前进,直到不能再前进为止,然后再回退到最近的一个未完全探索过的节点继续探索。本文将带你深入了解深度优先遍历的基本原理、应用场景以及如何通过几个具体的LeetCode题目来实践这一技巧。
PS:上一次说在图论和树结构中非常重要的算法是昨天了,没错,就是广度优先遍历。详情可以看往期文章。【唐叔学算法】第十天:广度优先遍历-探索图结构的逐层之旅
一、什么是深度优先遍历?
定义
深度优先遍历是一种用于遍历或搜索图(Graph)和树(Tree)数据结构的算法。它的核心思想是从某个起始节点开始,首先访问该节点,然后递归地对每个相邻且未被访问过的节点进行同样的操作,直到所有可达节点都被访问过。
应用场景
深度优先遍历常用于解决以下问题:
- 搜索问题:寻找从源点到目标点的所有可能路径,如寻找图中的所有路径、迷宫问题、八皇后问题等。
- 图论问题:如图的连通性、判断两个节点是否相连、拓扑排序等。
- 动态规划问题:求解背包问题、最长公共子序列等。
- 树形结构问题:遍历二叉树、多叉树等。
- 游戏AI:如国际象棋中的Minimax算法。
算法实现
深度优先遍历通常有两种实现方式:递归和非递归(基于栈)。递归版本更为直观易懂,而非递归版本则需要显式地使用栈来模拟递归调用过程。
递归实现步骤
- 将起始节点标记为已访问,并对其进行处理。
- 对于每个与当前节点直接相连且未被访问过的邻居节点,重复执行第1步。
- 当没有更多未访问的邻居时返回。
非递归实现步骤
- 初始化一个空栈,并将起始节点压入栈中。
- 当栈不为空时,重复以下操作:
- 从栈顶弹出一个节点并将其标记为已访问。
- 对于每个与当前节点直接相连且未被访问过的邻居节点,依次压入栈中。
- 栈为空时结束。
注意事项
- 防止重复访问:需要维护一个访问状态表来避免同一节点被多次处理,即标记已访问的节点。
- 边界条件:考虑空图、图成环、单节点等情况。
- 时间复杂度:对于稠密图,DFS的时间复杂度为O(V + E),其中V表示顶点数,E表示边的数量。
二、实战解析
入门题:104. 二叉树的最大深度
题目链接:104. 二叉树的最大深度
题目描述:给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
解题思路
这个问题非常适合用深度优先遍历来解决。我们可以使用递归的方法,分别计算左右子树的最大深度,最后取两者较大值加1即为当前树的最大深度。
Java代码实现
public class Solution {
public int maxDepth(TreeNode root) {
if (root == null) return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
}
中等题:200. 岛屿数量
题目链接:200. 岛屿数量
题目描述:给定一个由 ‘1’(陆地)和 ‘0’(水)组成的二维网格,计算岛屿的数量。岛屿总是被水包围,并且每个岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。
解题思路
为了找到所有的岛屿,我们可以遍历整个网格。每当遇到一块未访问过的陆地时(即值为 ‘1’ 的单元格),我们就启动一次新的DFS,从这块陆地开始,标记所有与之相连的陆地,这样就找到了一个完整的岛屿。为了避免重复计数同一个岛屿,已经访问过的陆地会被标记为已访问状态(例如将其置为 ‘0’ 或其他非 ‘1’ 的符号)。
递归解法-Java代码实现
public class Solution {
private int rows, cols;
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) return 0;
rows = grid.length;
cols = grid[0].length;
int count = 0;
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
if (grid[i][j] == '1') {
dfs(grid, i, j);
count++;
}
}
}
return count;
}
private void dfs(char[][] grid, int r, int c) {
if (r < 0 || c < 0 || r >= rows || c >= cols || grid[r][c] == '0') return;
grid[r][c] = '0'; // 标记为已访问
dfs(grid, r - 1, c); // 上
dfs(grid, r + 1, c); // 下
dfs(grid, r, c - 1); // 左
dfs(grid, r, c + 1); // 右
}
}
栈解法-Java代码实现
class Solution {
private static final int[][] DIRECTIONS = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) return 0;
int rows = grid.length;
int cols = grid[0].length;
int islandCount = 0;
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
if (grid[i][j] == '1') {
// 发现新的岛屿,开始DFS
islandCount++;
dfsUsingStack(grid, i, j);
}
}
}
return islandCount;
}
private void dfsUsingStack(char[][] grid, int x, int y) {
Stack<int[]> stack = new Stack<>();
stack.push(new int[]{x, y});
grid[x][y] = '0'; // 标记为已访问
while (!stack.isEmpty()) {
int[] current = stack.pop();
for (int[] dir : DIRECTIONS) {
int newX = current[0] + dir[0];
int newY = current[1] + dir[1];
if (newX >= 0 && newX < grid.length && newY >= 0 && newY < grid[0].length && grid[newX][newY] == '1') {
stack.push(new int[]{newX, newY});
grid[newX][newY] = '0'; // 标记为已访问
}
}
}
}
}
在这个解决方案中,我们使用了栈来代替递归来执行DFS,这有助于避免由于递归调用过多而导致的栈溢出错误,尤其是在非常大的输入情况下。同时,这种方法也使得算法逻辑更加直观易懂。
三、更多LeetCode题目推荐
如果您对深度优先遍历算法感兴趣,希望挑战更多题目,以下是一些LeetCode上推荐的题目:
四、总结
通过今天的分享,相信大家已经掌握了深度优先遍历的基本原理及其在实际问题中的应用。作为一种强大的搜索工具,DFS能够帮助我们高效地解决许多涉及图和树结构的问题。希望各位读者朋友能够在实践中灵活运用这些知识,解决更多的编程挑战。
希望这篇文章能够帮助大家更好地理解和应用深度优先遍历技术。如果你喜欢这篇文章,不妨点赞、收藏或转发给你的朋友们哦!也欢迎关注我的微信公众号【唐叔在学习】,获取更多技术文章和学习资料。我是唐叔,我们下次再见!