Authur Whywait 做一块努力吸收知识的海绵
想看博主的所有leetcode卡片学习笔记?传送门点这儿
先决条件:树的遍历
与 BFS 类似,深度优先搜索(DFS)是用于在树/图中遍历/搜索的另一种重要算法。也可以在更抽象的场景中使用。
正如树的遍历中所提到的,我们可以用 DFS 进行 前序遍历,中序遍历和后序遍历。在这三个遍历顺序中有一个共同的特性:除非我们到达最深的结点,否则我们永远不会回溯
。
这也是 DFS 和 BFS 之间最大的区别,BFS永远不会深入探索,除非它已经在当前层级访问了所有结点。
通常,我们使用递归实现 DFS。栈在递归中起着重要的作用。在本章中,我们将解释在执行递归时栈的作用。我们还将向你展示 递归的缺点 ,并提供另一个没有递归的 DFS 实现。
DFS 的实际设计因题而异。本章重点介绍栈是如何在 DFS 中应用的,并帮助你更好地理解 DFS 的原理。要精通 DFS 算法,还需要大量的练习。
栈和DFS
结点的处理顺序 & 栈的入栈和退栈顺序
在我们到达最深的结点之后,我们只会回溯并尝试另一条路径。
我们首先将根结点推入到栈中;然后我们尝试第一个邻居 B 并将结点 B 推入到栈中;等等等等。当我们到达最深的结点 E 时,我们需要回溯。当我们回溯时,我们将从栈中弹出最深的结点,这实际上是推入到栈中的最后一个结点。
结点的处理顺序是完全相反的顺序,就像它们被添加到栈中一样,它是后进先出(LIFO)。这就是我们在 DFS 中使用栈的原因。
正如我们在本章的描述中提到的,在大多数情况下,我们在能使用 BFS 时也可以使用 DFS。但是有一个重要的区别:遍历顺序。
DFS - 模板Ⅰ (递归)
与 BFS 不同,更早访问的结点可能不是更靠近根结点的结点。因此,你在 DFS 中找到的第一条路径可能不是最短路径。
在本文中,我们将为你提供一个 DFS 的递归模板,并向你展示栈是如何帮助这个过程的。下面是Java伪代码:
/*
* Return true if there is a path from cur to target.
*/
boolean DFS(Node cur, Node target, Set<Node> visited) {
return true if cur is target;
for (next : each neighbor of cur) {
if (next is not in visited) {
add next to visted;
return true if DFS(next, target, visited) == true;
}
}
return false;
}
当我们递归地实现 DFS 时,似乎不需要使用任何栈。但实际上,我们使用的是由系统提供的隐式栈,也称为调用栈(Call Stack)。
举例
我们希望在下图中找到结点 0 和结点 3 之间的路径。我们还会在每次调用期间显示栈的状态。
在每个堆栈元素中,都有一个整数 cur
,一个整数 target
,一个对访问过的数组的引用
和一个对数组边界的引用
,这些正是我们在 DFS 函数中的参数。我们只在上面的栈中显示 cur。
每个元素都需要固定的空间。栈的大小正好是 DFS 的深度。因此,在最坏的情况下,维护系统栈需要 O(h),其中 h 是 DFS 的最大深度。在计算空间复杂度时,永远不要忘记考虑系统栈。
在上面的模板中,我们在找到第一条路径时停止。
如果我们想找到最短路径呢?
方案:再添加一个参数来指示我们已经找到的最短路径。
模板Ⅰ 相关练习
1. 岛屿数量🚩
给定一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
输入:
11110
11010
11000
00000
输出: 1
分析
这道练习题我们曾经在学习到BFS算法的时候使用BFS算法解决过了。在本文中,我们将用DFS算法来解决。
步骤
具体步骤和BFS算法类似,主要区别就是使用的搜索算法不同。BFS算法传送门在这里
关于报错
如果写程序中遇到如下问题:
runtime error: load of misaligned address 0x0000ffffffff for type 'int'...
点击传送门查看解决方法。传送门在这儿
代码实现以及执行结果
void DFS(char** grid, int gridSize, int* gridColSize,int i, int j){
if(grid[i][j]!='1') return;
grid[i][j] = '2';
int directions[4][2] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
for(int k=0; k<4; k++){
int x_next = i + directions[k][0];
int y_next = j + directions[k][1];
if(x_next<0 || x_next>gridSize-1 || y_next<0 || y_next>(*gridColSize)-1) continue;
DFS(grid, gridSize, gridColSize, x_next, y_next);
}
}
int numIslands(char** grid, int gridSize, int* gridColSize){
if(!grid || !gridSize || !(*gridColSize)) return 0;
int count=0;
for(int i=0; i<gridSize; i++)
for(int j=0; j<*gridColSize; j++)
if(grid[i][j]=='1'){
DFS(grid, gridSize, gridColSize, i, j);
count++;
}
return count;
}
2. 克隆图🚩
给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。
图中的每个节点都包含它的值 val
(int
) 和其邻居的列表(list[Node]
)。
//Definition for a Node.
struct Node {
int val;
int numNeighbors;
struct Node** neighbors;
};
测试用例格式:
简单起见,每个节点的值都和它的索引相同。例如,第一个节点值为 1(val = 1),第二个节点值为 2(val = 2),以此类推。该图在测试用例中使用邻接列表表示。
邻接列表 是用于表示有限图的无序列表的集合。每个列表都描述了图中节点的邻居集。
给定节点将始终是图中的第一个节点(值为 1)。你必须将 给定节点的拷贝 作为对克隆图的引用返回。
输入:adjList = [[2,4],[1,3],[2,4],[1,3]]
输出:[[2,4],[1,3],[2,4],[1,3]]
解释:
图中有 4 个节点。
节点 1 的值是 1,它有两个邻居:节点 2 和 4 。
节点 2 的值是 2,它有两个邻居:节点 1 和 3 。
节点 3 的值是 3,它有两个邻居:节点 2 和 4 。
节点 4 的值是 4,它有两个邻居:节点 1 和 3 。
提示:
- 节点数不超过 100 。
- 每个节点值 Node.val 都是唯一的,1 <= Node.val <= 100。
- 无向图是一个简单图,这意味着图中没有重复的边,也没有自环。
- 由于图是无向的,如果节点 p 是节点 q 的邻居,那么节点 q 也必须是节点 p 的邻居。
- 图是连通图,你可以从给定节点访问到所有节点。
/**
* Definition for a Node.
* struct Node {
* int val;
* int numNeighbors;
* struct Node** neighbors;
* };
*/
分析
题目降低了难度,从提示中我们可以get到很多信息,善用信息,会轻松很多。
做题方法:由此题环境,自然是使用DFS - 模板Ⅰ,即使用递归。
而且我们来分析一下提示里给出的五个tips:
- 节点数不超过一百。我们最开始只要分配一个能放下100个节点的空间,我们之后的拷贝操作(克隆操作)都是在这个空间里完成的。
- 每个节点值 Node.val 都是唯一的。我们可以利用节点值来遍历。在之前分配的空间(这是一个连续的空间),从1到100进行标号。标号同时也是对应节点的节点值。
- 无向图是一个简单图,没有重复的边,也没有环。我们不需要考虑这些重复的边和环的因素。
- p为q的邻居,则q为p的邻居。解释无向图的性质,防止我们犯错。
- 图为连通图。这是递归能实现图的克隆的必要条件 —— 如果不连通,递归就必定无法到达某些点,也就无法完成依次完美的克隆。
步骤
- 递归返回条件 - 曾遍历否?
- 分配空间
- 给val,numNeighbor 赋值
- 利用 numNeighbor 给 neighbors 这个指针分配空间
- 遍历s的邻节点
- 返回
代码实现以及执行结果
struct Node** vectors;
struct Node* dfs(struct Node* s) {
if(vectors[s->val]) return vectors[s->val];
vectors[s->val] = (struct Node *)malloc(sizeof(struct Node));
vectors[s->val]->val = s->val;
vectors[s->val]->numNeighbors = s->numNeighbors;
vectors[s->val]->neighbors = (struct Node **)malloc(sizeof(struct Node *) * s->numNeighbors);
for(int i=0; i<s->numNeighbors; i++){
vectors[s->val]->neighbors[i] = dfs(s->neighbors[i]);
}
return vectors[s->val];
}
struct Node *cloneGraph(struct Node *s) {
if(!s) return 0;
vectors = (struct Node *)calloc(101, sizeof(struct Node));
return dfs(s);
}
3. 目标和🚩
给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
测试用例举例:
输入: nums: [1, 1, 1, 1, 1], S: 3
输出: 5
解释:
-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3
一共有5种方法让最终目标和为3。
注意
- 数组非空,且长度不会超过20。
- 初始的数组的和不会超过1000。
- 保证返回的最终结果能被32位整数存下。
分析
与本文主题切合,此题使用DFS暴力法解决
我们需要下面三个计数器:
- 计数器Ⅰ - 记录遍历到了第几个元素
- 计数器Ⅱ - 记录此时累计的值的和为多少
- 计数器Ⅲ - 有几种情况满足条件(计数器Ⅰ为数组最后元素,且计数器Ⅱ和目标值相等)
本文中的三个计数器分别为:index,count, num(其中num的指针作为形参传入函数)
步骤
- 返回条件:计数器Ⅰ到达数组末尾元素
- 遍历情况:类似树的遍历。关于树的遍历详情请点击传送门。
代码实现以及执行结果
void cnt(int *nums, int numsSize, int index, int target, int count, int* num){
if(index == numsSize){
if(count == target) (* num)++;
return;
}
cnt(nums, numsSize, index+1, target, count + nums[index], num);
cnt(nums, numsSize, index+1, target, count - nums[index], num);
}
int findTargetSumWays(int* nums, int numsSize, int S){
if(!numsSize) return 0;
int num = 0, count = 0, index = 0;
cnt(nums, numsSize, index, S, count, &num);
return num;
}
DFS - 模板Ⅱ
递归解决方案的优点是它更容易实现。
但是,存在一个很大的缺点:如果递归的深度太高,你将遭受堆栈溢出。
在这种情况下,您可能会希望使用 BFS,或使用显式栈实现 DFS。
下面提供一个使用显式栈的模板(Java伪代码):
/*
* Return true if there is a path from cur to target.
*/
boolean DFS(int root, int target) {
Set<Node> visited;
Stack<Node> s;
add root to s;
while (s is not empty) {
Node cur = the top element in s;
return true if cur is target;
for (Node next : the neighbors of cur) {
if (next is not in visited) {
add next to s;
add next to visited;
}
}
remove cur from s;
}
return false;
}
模板中使用 while 循环
和栈
来模拟递归期间的系统调用栈。
模板Ⅱ 相关练习
二叉树的中序遍历🚩
给定一个二叉树,返回它的中序 遍历。
分析
此题已经类似题目的递归解法请点击传送门查看,本文中将使用while循环和栈来模拟递归期间的系统调用栈。
步骤
- 递归,得到树的节点个数(为后面分配空间做准备)
- 迭代:入栈、出栈、存储到数组
动图演示
用手绘图来制作动态图,真的只要亿点细节。
为了让你更清晰的观看,我放慢了播放速度,希望对正在观看的你有所帮助。
如果有帮助,可以滑到文末给我点个赞吗?(天哪好卑微)
代码实现以及执行结果
int treeSize(struct TreeNode * root){
if(!root) return 0;
return treeSize(root->left) + treeSize(root->right) + 1;
}
int* inorderTraversal(struct TreeNode* root, int* returnSize){
* returnSize = 0;
if(!root) return 0;
int size = treeSize(root);
struct TreeNode ** Stack = (struct TreeNode **)malloc(sizeof(struct TreeNode *) * size);
int p = 0;
int* res = (int *)malloc(sizeof(int) * size);
while(root || p){
while(root){
Stack[p++] = root;
root = root->left;
}
if(p){
root = Stack[--p];
res[(* returnSize)++] = root->val;
root = root->right;
}
}
return res;
}
抓住你了!都看到这里了竟然想不点赞就溜走?(╯▔皿▔)╯