leetcode【数据结构简介】《队列&栈》卡片 - 栈和深度优先搜索

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. 克隆图🚩

给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。

图中的每个节点都包含它的值 valint) 和其邻居的列表(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 。

提示:

  1. 节点数不超过 100 。
  2. 每个节点值 Node.val 都是唯一的,1 <= Node.val <= 100。
  3. 无向图是一个简单图,这意味着图中没有重复的边,也没有自环。
  4. 由于图是无向的,如果节点 p 是节点 q 的邻居,那么节点 q 也必须是节点 p 的邻居。
  5. 图是连通图,你可以从给定节点访问到所有节点。
/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     int numNeighbors;
 *     struct Node** neighbors;
 * };
 */
分析

题目降低了难度,从提示中我们可以get到很多信息,善用信息,会轻松很多。

做题方法:由此题环境,自然是使用DFS - 模板Ⅰ,即使用递归。

而且我们来分析一下提示里给出的五个tips:

  1. 节点数不超过一百。我们最开始只要分配一个能放下100个节点的空间,我们之后的拷贝操作(克隆操作)都是在这个空间里完成的。
  2. 每个节点值 Node.val 都是唯一的。我们可以利用节点值来遍历。在之前分配的空间(这是一个连续的空间),从1到100进行标号。标号同时也是对应节点的节点值。
  3. 无向图是一个简单图,没有重复的边,也没有环。我们不需要考虑这些重复的边和环的因素。
  4. p为q的邻居,则q为p的邻居。解释无向图的性质,防止我们犯错。
  5. 图为连通图。这是递归能实现图的克隆的必要条件 —— 如果不连通,递归就必定无法到达某些点,也就无法完成依次完美的克隆。
步骤
  1. 递归返回条件 - 曾遍历否?
  2. 分配空间
  3. 给val,numNeighbor 赋值
  4. 利用 numNeighbor 给 neighbors 这个指针分配空间
  5. 遍历s的邻节点
  6. 返回
代码实现以及执行结果
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暴力法解决

我们需要下面三个计数器:

  1. 计数器Ⅰ - 记录遍历到了第几个元素
  2. 计数器Ⅱ - 记录此时累计的值的和为多少
  3. 计数器Ⅲ - 有几种情况满足条件(计数器Ⅰ为数组最后元素,且计数器Ⅱ和目标值相等)

本文中的三个计数器分别为:index,count, num(其中num的指针作为形参传入函数)

步骤
  1. 返回条件:计数器Ⅰ到达数组末尾元素
  2. 遍历情况:类似树的遍历。关于树的遍历详情请点击传送门。
代码实现以及执行结果
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循环和栈来模拟递归期间的系统调用栈

步骤
  1. 递归,得到树的节点个数(为后面分配空间做准备)
  2. 迭代:入栈、出栈、存储到数组
动图演示

用手绘图来制作动态图,真的只要亿点细节

为了让你更清晰的观看,我放慢了播放速度,希望对正在观看的你有所帮助。
如果有帮助,可以滑到文末给我点个赞吗?(天哪好卑微)

在这里插入图片描述

代码实现以及执行结果
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;
}

在这里插入图片描述

抓住你了!都看到这里了竟然想不点赞就溜走?(╯▔皿▔)╯

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AuthurLEE

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值