算法数据结构——图的遍历之深度优先搜索算法(Depth First Search)

article/2025/9/13 1:37:49

1. 深度优先搜索简介

深度优先搜索算法(Depth First Search):英文缩写为 DFS。是一种用于搜索树或图的算法。所谓深度优先,就是说每次都尝试向更深的节点走。

深度优先搜索采用了回溯思想,该算法沿着树的深度遍历树的节点,会尽可能深的搜索树的分支。当节点 v 的所在边都己被探寻过,搜索将回溯到发现节点 v 的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。

在深度优先遍历的过程中,我们需要将当前遍历节点 v 的相邻节点暂时存储起来,以便于在回退的时候可以继续访问它们。遍历到的节点顺序符合「后进先出」的特点,这正是「递归」和「堆栈」所遵循的规律,所以深度优先搜索可以通过「递归」或者「堆栈」来实现。

2. 深度优先搜索过程演示

接下来我们以一个无向图为例,演示一下深度优先搜索的过程。

我们用邻接字典的方式存储无向图结构,对应结构如下:

# 定义无向图结构
graph = {"A": ["B", "C"],"B": ["A", "C", "D"],"C": ["A", "B", "D", "E"],"D": ["B", "C", "E", "F"],"E": ["C", "D"],"F": ["D"]
}

该无向图对应的邻接字典表示:无向图中有 ABCDEF6 个节点,其中与 A 节点相连的有 BC 两个节点,与 B 节点相连的有 ACD 三个节点,等等。

该无向图的结构如图左所示,其深度优先搜索的遍历路径如图右所示。

其深度优先搜索的遍历过程如下动态图所示。

3. 基于递归实现的深度优先搜索

3.1 基于递归实现的深度优先搜索实现步骤

  1. 定义 graph 为存储无向图的字典变量,visited 为标记访问节点的 set 集合变量。start 为当前遍历边的开始节点。def dfs_recursive(graph, start, visited): 为递归实现的深度优先搜索方法。

  2. start 标记为已访问,即将 start 节点放入 visited 中(visited.add(start))。

  3. 访问节点 start,并对节点进行相关操作(看具体题目要求)。

  4. 遍历与节点 start 相连并构成边的节点 end

    1. 如果 end 没有被访问过,则从 end 节点调用递归实现的深度优先搜索方法,即 dfs_recursive(graph, end, visited)

3.2 基于递归实现的深度优先搜索实现代码

def dfs_recursive(graph, start, visited):# 标记节点visited.add(start)# 访问节点print(start)
​for end in graph[start]:if end not in visited:# 深度优先遍历节点dfs_recursive(graph, end, visited)

4. 基于堆栈实现的深度优先搜索

4.1 基于堆栈实现的深度优先搜索实现步骤

  1. start 为开始节点。定义 visited 为标记访问节点的 set 集合变量。定义 stack 用于存放临时节点的栈结构。

  2. 首先访问起始节点,并对节点进行相关操作(看具体题目要求)。

  3. 然后将起始节点放入栈中,并标记访问。即 visited = set(start)stack = [start]

  4. 如果栈不为空,取 stack 栈顶元素 node_u

  5. 遍历与节点 node_u 相连并构成边的节点 node_v

    • 如果 node_v 没有被访问过,则:

      • 访问节点 node_v,并对节点进行相关操作(看具体题目要求)。

      • node_v 节点放入栈中,并标记访问,即 stack.append(node_v)visited.add(node_v)

      • 跳出遍历 node_v 的循环。

    • 继续遍历 node_v

  6. 如果 node_u 相邻的节点都访问结束了,从栈顶弹出 node_u,即 stack.pop()

  7. 重复步骤 4 ~ 6,直到 stack 为空。

4.2 基于堆栈实现的深度优先搜索实现代码

def dfs_stack(graph, start):print(start)                        # 访问节点 startvisited = set(start)                # 使用 visited 标记访问过的节点,先标记 startstack = [start]                     # 创建一个栈,并将 start 加入栈中while stack:node_u = stack[-1]              # 取栈顶元素i = 0while i < len(graph[node_u]):   # 遍历栈顶元素,遇到未访问节点,访问节点并跳出。node_v = graph[node_u][i]if node_v not in visited:   # node_v 未访问过print(node_v)           # 访问节点 node_vstack.append(node_v)    # 将 node_v 加入栈中visited.add(node_v)     # 标记为访问过 node_vbreaki += 1if i == len(graph[node_u]):    # node_u 相邻的节点都访问结束了,弹出 node_ustack.pop()

5. 深度优先搜索应用

5.1 岛屿数量

5.1.1 题目链接

  • 200. 岛屿数量 - 力扣(LeetCode)

5.1.2 题目大意

描述:给定一个由字符 '1'(陆地)和字符 '0'(水)组成的的二维网格 grid

要求:计算网格中岛屿的数量。

说明

  • 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

  • 此外,你可以假设该网格的四条边均被水包围。

  • $m == grid.length$。

  • $n == grid[i].length$。

  • $1 \le m, n \le 300$。

  • grid[i][j] 的值为 '0''1'

示例

输入:grid = [["1","1","1","1","0"],["1","1","0","1","0"],["1","1","0","0","0"],["0","0","0","0","0"]
]
输出:1
​
​
输入:grid = [["1","1","0","0","0"],["1","1","0","0","0"],["0","0","1","0","0"],["0","0","0","1","1"]
]
输出:3

5.1.3 解题思路

如果把上下左右相邻的字符 '1' 看做是 1 个连通块,这道题的目的就是求解一共有多少个连通块。

使用深度优先搜索或者广度优先搜索都可以。

思路 1:深度优先搜索

  1. 遍历 grid

  2. 对于每一个字符为 '1' 的元素,遍历其上下左右四个方向,并将该字符置为 0,保证下次不会被重复遍历。

  3. 如果超出边界,则返回 0

  4. 对于 (i, j) 位置的元素来说,递归遍历的位置就是 (i - 1, j)(i, j - 1)(i + 1, j)(i, j + 1) 四个方向。每次遍历到底,统计数记录一次。

  5. 最终统计出深度优先搜索的次数就是我们要求的岛屿数量。

思路 1:代码

class Solution:def dfs(self, grid, i, j):n = len(grid)m = len(grid[0])if i < 0 or i >= n or j < 0 or j >= m or grid[i][j] == '0':return 0grid[i][j] = '0'self.dfs(grid, i + 1, j)self.dfs(grid, i, j + 1)self.dfs(grid, i - 1, j)self.dfs(grid, i, j - 1)
​def numIslands(self, grid: List[List[str]]) -> int:count = 0for i in range(len(grid)):for j in range(len(grid[0])):if grid[i][j] == '1':self.dfs(grid, i, j)count += 1return count

思路 1:复杂度分析

  • 时间复杂度:$O(m \times n)$。其中 $m$ 和 $n$ 分别为行数和列数。

  • 空间复杂度:$O(m \times n)$。

5.2 克隆图

5.2.1 题目链接

  • 133. 克隆图 - 力扣(LeetCode)

5.2.2 题目大意

描述:以每个节点的邻接列表形式(二维列表)给定一个无向连通图,其中 adjList[i] 表示值为 i + 1的节点的邻接列表,adjList[i][j] 表示值为 i + 1 的节点与值为 adjList[i][j] 的节点有一条边。

要求:返回该图的深拷贝。

说明

  • 节点数不超过 100

  • 每个节点值 $Node.val$ 都是唯一的,$1 \le Node.val \le 100$。

  • 无向图是一个简单图,这意味着图中没有重复的边,也没有自环。

  • 由于图是无向的,如果节点 p 是节点 q 的邻居,那么节点 q 也必须是节点 p 的邻居。

  • 图是连通图,你可以从给定节点访问到所有节点。

示例

输入: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 。

输入:adjList = [[2],[1]]
输出:[[2],[1]]

5.2.3 解题思路

所谓深拷贝,就是构建一张与原图结构、值均一样的图,但是所用的节点不再是原图节点的引用,即每个节点都要新建。

可以用深度优先搜索或者广度优先搜索来做。

思路 1:深度优先搜索

  1. 使用哈希表 visitedDict 来存储原图中被访问过的节点和克隆图中对应节点,键值对为 原图被访问过的节点:克隆图中对应节点。

  2. 从给定节点开始,以深度优先搜索的方式遍历原图。

    1. 如果当前节点被访问过,则返回隆图中对应节点。

    2. 如果当前节点没有被访问过,则创建一个新的节点,并保存在哈希表中。

    3. 遍历当前节点的邻接节点列表,递归调用当前节点的邻接节点,并将其放入克隆图中对应节点。

  3. 递归结束,返回克隆节点。

思路 1:代码

class Solution:def cloneGraph(self, node: 'Node') -> 'Node':if not node:return nodevisitedDict = dict()
​def dfs(node: 'Node') -> 'Node':if node in visitedDict:return visitedDict[node]
​clone_node = Node(node.val, [])visitedDict[node] = clone_nodefor neighbor in node.neighbors:clone_node.neighbors.append(dfs(neighbor))return clone_node
​return dfs(node)

思路 1:复杂度分析

  • 时间复杂度:$O(n)$。其中 $n$ 为图中节点数量。

  • 空间复杂度:$O(n)$。


http://chatgpt.dhexx.cn/article/5X1a8gMc.shtml

相关文章

【新书速递】实用安全多方计算导论

安全多方计算&#xff08;MPC&#xff09;是解决数据安全与隐私保护问题的关键安全数据交换技术&#xff0c;近年来发展迅速&#xff0c;但由于MPC涉及复杂的密码学和工程实现技术&#xff0c;行业长期缺乏同时具备MPC研究、应用和实现能力的综合性人才&#xff0c;这阻碍了MPC…

百万富翁问题--安全多方计算

百万富翁问题—安全多方计算 是由图灵奖获得者姚期智提出的。 有A、B两个富翁&#xff0c;A资产i亿元&#xff0c;B资产j亿元&#xff0c;i、j均在0-10范围内&#xff0c;在互不让对方知道自己资产的情况下&#xff0c;比较A和B的资产谁多谁少。 那么如何去比较呢&#xff1f;…

隐私保护技术之安全多方计算

安全多方计算(Secure Multi-Party Computation&#xff0c;SMPC)用于解决一组互不信任的参与方各自持有秘密数据&#xff0c; 协同计算一个既定函数的问题。安全多方计算在保证参与方获得正确计算结果的同时&#xff0c;无法获得计算结果之外的任何信息。 在整个计算过程中&…

基于同态加密体制的安全多方计算

本文首发公众号VenusBlockChain&#xff0c;关注公众号后可免费阅读&#xff01;VenusBlockChain致力于区块链技术研究&#xff0c;传播区块链技术和解决方案、区块链应用落地、区块链行业动态等。有兴趣的小伙伴们&#xff0c;欢迎关注。 安全多方计算&#xff08;Secure Mu…

多方安全计算

说明&#xff0c;本文是转载的&#xff0c;个人觉得作者讲解清晰明了&#xff0c;收录用于学习&#xff0c;原文链接&#xff1a;https://blog.csdn.net/yuxinqingge/article/details/104588197。 如今&#xff0c;互联网已经完成了从IT时代向DT时代转变&#xff0c;数据已经成…

多方安全计算MPC

1.多方安全计算的价值 MPC是密码学的一个重要分支&#xff0c;旨在解决一组互不信任的参与方之间保护隐私的协同计算问题&#xff0c;为数据需求方提供不泄露原始数据前提下的多方协同计算能力。 在目前个人数据毫无隐私的环境下&#xff0c;对数据进行确权并实现数据价值显得…

安全多方计算(MPC)

MPC既适用于特定的算法&#xff0c;如加法、乘法、AES&#xff0c;集合交集等&#xff1b;也适用于所有可表示成计算过程的通用算法。 根据计算参与方个数不同&#xff0c;可分为只有两个参与方的2PC和多个参与方&#xff08;≥3&#xff09;的通用MPC 1&#xff09;安全两方&a…

安全多方计算之二:一文搞懂百万富翁问题

百万富翁问题 1. 解决方案2. 协议描述3. 协议说明4. 协议举例5. 协议扩展 两个百万富翁Alice和Bob想知道他们两个谁更富有&#xff0c;但他们都不想让对方及其他第三方知道自己财富的任何信息&#xff0c;这是由中国计算机科学家、2000年图灵奖获得者姚启智教授于1982年在论文《…

安全多方计算-入门学习笔记(三)

四、基于非噪音的安全多方计算介绍 1概念 非噪音方法一般是通过密码学方法将数据编码或加密&#xff0c;得到一些奇怪的数字&#xff0c;而且这些奇怪的数字有一些神奇的性质&#xff0c;比如看上去很随机但其实保留了原始数据的线性关系&#xff0c;或者顺序明明被打乱但人们…

基于隐私保护的安全多方计算区块链融合技术的智能合约

安全多方计算与区块链的融合技术 SMPC-安全多方计算综述安全的定义安全模型SMPC效率问题 区块链应用&#xff1a;智能合约智能合约的问题SMPC需求引入 基于SMPC的智能合约辅助&#xff1a;MPI协议-效率提升 SMPC-安全多方计算综述 随着物联网、移动计算、大数据、云计算的快速…

安全多方计算之GMW协议

安全多方计算之GMW协议 一、背景 论文原文《How to play any mental game》。作者&#xff1a;Micali S, Goldreich O, Wigderson A. 发表在&#xff1a;Proceedings of the Nineteenth ACM Symp on Theory of Computing, STOC. GMW协议可以同时适用在布尔电路和算术电路&am…

多方安全计算概述

多方安全计算&#xff08;Secure Multi-Party Computation&#xff0c; MPC&#xff09;是密码学的一个分支&#xff0c;在无可信第三方的情况下&#xff0c;仍可安全地按照公开的计算逻辑&#xff0c;进行数据协同计算&#xff0c;并输出结果。 即使参与各方输入的数据只有自…

安全多方计算之一:什么是安全多方计算

安全多方计算 1. 安全多方计算简介2. 基本概念2.1 参与者2.2 攻击者 3. 安全多方计算的模型4. 安全多方计算的密码学工具5. 学习参考 1. 安全多方计算简介 安全多方计算问题SMC&#xff0c;Secure Multi-party Computation&#xff09;由由中国计算机科学家、2000年图灵奖获得…

安全多方计算简介

文章目录 一、安全多方计算定义二、安全多方计算安全模型1.行为模型2.安全门限 三、安全多方计算关键技术1.秘密共享(Secret Sharing, SS)2.不经意传输(Oblivious Transfer, OT)3.混淆电路(Garbled Circuit, GC) 参考资料 一、安全多方计算定义 安全多方计算(Secure Multi-Par…

安全多方计算 # 个人笔记

一个优美令人如痴如醉的领域。 Data is the oil of the 21st century 欢迎读者拍砖和提供本文修改建议。本文长期维护。 第二次编辑于2021/10/20&#xff0c;新增了部分阅读材料&#xff0c;调整了文章内容的组织。 0 研究背景 【最后更新于2021/10/20】 时代背景&#xff1…

Verilog操作符

操作符优先级表 Verilog中的大小(size)与符号 Verilog根据表达式中变量的长度对表达式的值自动地进行调整; Verilog自动截断或扩展赋值语句中右边的值以适应左边变量的长度; 当一个负数赋值给无符号变量如reg时,Verilog自动完成二进制补码计算; 算术运算符 加(+)、减(-…

C语言——操作符详解

目录 一、算术操作符 二、移位操作符 三、位操作符 四、赋值操作符 五、单目操作符 六、关系操作符 七、逻辑操作符 八、条件操作符 九、逗号表达式 十、下标引用、函数调用和结构成员 以上就是C语言中涉及到得操作符&#xff0c;我将用代码实例配合说明&#xff0c…

教妹学Java(十一):操作符简介

大家好&#xff0c;我是沉默王二&#xff0c;一个和黄家驹一样身高&#xff0c;和刘德华一样颜值的程序员。本篇文章通过我和三妹对话的形式来谈一谈“Java 中的操作符”。 教妹学 Java&#xff0c;没见过这么有趣的标题吧&#xff1f;“语不惊人死不休”&#xff0c;没错&…

位运算操作符详解

文章目录 一. 移位操作符>> <<1. 整数的二进制表示Ps:怎么确定一个数在内存中占几位呢&#xff1f; 2. <<左移操作符3. >>右移操作符 二. 位操作符& &#xff5c;^1. &按&#xff08;二进制&#xff09;位与2. &#xff5c;按&#xff08;二进…

【C语言】操作符详解

文章目录 前言一、操作符分类二、用法详述1.算数操作符2.移位操作符2.1左移操作符2.2右移操作符2.3警告&#xff1a; 3.位操作符3.1 & 按位与3.2 | 按位或3.3 \^ 按位异或 4.赋值操作符5.单目操作符5.1逻辑反操作符 !5.2 自增、自减操作符 、--5.3 按位取反 ~ 6.关系操作符…