|
1 | 1 | ## 1. 深度优先搜索简介
|
2 | 2 |
|
3 | | -> **深度优先搜索算法(Depth First Search)**:英文缩写为 DFS,是一种用于搜索树或图结构的算法。深度优先搜索算法采用了回溯思想,从起始节点开始,沿着一条路径尽可能深入地访问节点,直到无法继续前进时为止,然后回溯到上一个未访问的节点,继续深入搜索,直到完成整个搜索过程。 |
| 3 | +> **深度优先搜索算法(Depth First Search,简称 DFS)**:是一种用于遍历或搜索树、图等结构的经典算法。其核心思想是「沿一条路径尽可能深入」,遇到无法继续的节点时再回溯到上一个分叉点,继续探索其他路径,直到遍历完整个结构或找到目标为止。 |
4 | 4 |
|
5 | | -深度优先搜索算法中所谓的深度优先,就是说优先沿着一条路径走到底,直到无法继续深入时再回头。 |
| 5 | +深度优先的含义,就是每次优先选择一条路径一直走到底,只有在无法继续时才回退,尝试其他分支。 |
6 | 6 |
|
7 | | -在深度优先遍历的过程中,我们需要将当前遍历节点 $u$ 的相邻节点暂时存储起来,以便于在回退的时候可以继续访问它们。遍历到的节点顺序符合「后进先出」的特点,这正是「递归」和「堆栈」所遵循的规律,所以深度优先搜索可以通过「递归」或者「堆栈」来实现。 |
| 7 | +在 DFS 的遍历过程中,我们通常需要暂存当前节点 $u$ 的相邻节点,以便回溯时继续访问未遍历的节点。由于遍历顺序具有「后进先出」的特性,DFS 可以通过递归(系统栈)或显式使用栈结构来实现。 |
8 | 8 |
|
9 | 9 | ## 2. 深度优先搜索算法步骤
|
10 | 10 |
|
11 | | -接下来我们以一个无向图为例,介绍一下深度优先搜索的算法步骤。 |
| 11 | +下面以无向图为例,简要梳理深度优先搜索的基本流程: |
12 | 12 |
|
13 | | -1. 选择起始节点 $u,ドル并将其标记为已访问。 |
14 | | -2. 检查当前节点是否为目标节点(看具体题目要求)。 |
15 | | -3. 如果当前节点 $u$ 是目标节点,则直接返回结果。 |
16 | | -4. 如果当前节点 $u$ 不是目标节点,则遍历当前节点 $u$ 的所有未访问邻接节点。 |
17 | | -5. 对每个未访问的邻接节点 $v,ドル从节点 $v$ 出发继续进行深度优先搜索(递归)。 |
18 | | -6. 如果节点 $u$ 没有未访问的相邻节点,回溯到上一个节点,继续搜索其他路径。 |
19 | | -7. 重复 2ドル \sim 6$ 步骤,直到遍历完整个图或找到目标节点为止。 |
| 13 | +1. 选定起始节点 $u,ドル将其标记为已访问。 |
| 14 | +2. 判断当前节点 $u$ 是否为目标节点(具体依据题目要求)。 |
| 15 | +3. 如果 $u$ 为目标节点,直接返回结果。 |
| 16 | +4. 如果 $u$ 不是目标节点,则遍历 $u$ 的所有未被访问的邻接节点。 |
| 17 | +5. 对每一个未访问的邻接节点 $v,ドル递归地从 $v$ 继续进行深度优先搜索。 |
| 18 | +6. 如果 $u$ 没有未访问的邻接节点,则回溯到上一个节点,继续探索其他分支。 |
| 19 | +7. 重复步骤 2ドル \sim 6,ドル直到遍历完整个图或找到目标节点为止。 |
20 | 20 |
|
21 | 21 | ::: tabs#DFS
|
22 | 22 |
|
|
46 | 46 |
|
47 | 47 | :::
|
48 | 48 |
|
49 | | -## 3. 基于递归实现的深度优先搜索 |
| 49 | +## 3. 基于递归的深度优先搜索 |
50 | 50 |
|
51 | | -### 3.1 基于递归实现的深度优先搜索算法步骤 |
| 51 | +### 3.1 基于递归的深度优先搜索算法步骤 |
52 | 52 |
|
53 | | -深度优先搜索算法可以通过递归来实现,以下是基于递归实现的深度优先搜索算法步骤: |
| 53 | +深度优先搜索(DFS)可以通过递归方式实现。其基本递归流程如下: |
54 | 54 |
|
55 | | -1. 定义 $graph$ 为存储无向图的嵌套数组变量,$visited$ 为标记访问节点的集合变量。$u$ 为当前遍历边的开始节点。定义 `def dfs_recursive(graph, u, visited):` 为递归实现的深度优先搜索方法。 |
56 | | -2. 选择起始节点 $u$,并将其标记为已访问,即将节点 $u$ 放入 $visited$ 中(`visited.add(u)`)。 |
57 | | -3. 检查当前节点 $u$ 是否为目标节点(看具体题目要求)。 |
58 | | -4. 如果当前节点 $u$ 是目标节点,则直接返回结果。 |
59 | | -5. 如果当前节点 $u$ 不是目标节点,则遍历当前节点 $u$ 的所有未访问邻接节点。 |
60 | | -6. 对每个未访问的邻接节点 $v,ドル从节点 $v$ 出发继续进行深度优先搜索(递归),即调用 `dfs_recursive(graph, v, visited)`。 |
61 | | -7. 如果节点 $u$ 没有未访问的相邻节点,则回溯到最近访问的节点,继续搜索其他路径。 |
62 | | -8. 重复 3ドル \sim 7$ 步骤,直到遍历完整个图或找到目标节点为止。 |
| 55 | +1. 设 $graph$ 为无向图的邻接表,$visited$ 为已访问节点的集合,$u$ 为当前节点。定义递归函数 `def dfs_recursive(graph, u, visited):`。 |
| 56 | +2. 将起始节点 $u$ 标记为已访问(`visited.add(u)`)。 |
| 57 | +3. 判断当前节点 $u$ 是否为目标节点(具体依据题目要求)。 |
| 58 | +4. 如果 $u$ 为目标节点,直接返回结果。 |
| 59 | +5. 如果 $u$ 不是目标节点,则遍历 $u$ 的所有未访问的邻接节点 $v$。 |
| 60 | +6. 对每个未访问的邻接节点 $v,ドル递归调用 `dfs_recursive(graph, v, visited)` 继续搜索。 |
| 61 | +7. 如果 $u$ 没有未访问的邻接节点,则自动回溯到上一个节点,继续探索其他分支。 |
| 62 | +8. 重复步骤 3ドル \sim 7,ドル直到遍历完整个图或找到目标节点为止。 |
63 | 63 |
|
64 | | -### 3.2 基于递归实现的深度优先搜索实现代码 |
| 64 | +### 3.2 基于递归的深度优先搜索实现代码 |
65 | 65 |
|
66 | 66 | ```python
|
67 | 67 | class Solution:
|
68 | 68 | def dfs_recursive(self, graph, u, visited):
|
69 | | - print(u) # 访问节点 |
70 | | - visited.add(u) # 节点 u 标记其已访问 |
71 | | - |
| 69 | + """ |
| 70 | + 递归实现深度优先搜索(DFS) |
| 71 | + :param graph: 字典表示的邻接表,key为节点,value为邻接节点列表 |
| 72 | + :param u: 当前访问的节点 |
| 73 | + :param visited: 已访问节点的集合 |
| 74 | + """ |
| 75 | + print(u) # 访问当前节点 u |
| 76 | + visited.add(u) # 标记节点 u 已访问 |
| 77 | + |
| 78 | + # 遍历所有邻接节点 |
72 | 79 | for v in graph[u]:
|
73 | | - if v not in visited: # 节点 v 未访问过 |
74 | | - # 深度优先搜索遍历节点 |
| 80 | + if v not in visited: # 如果邻接节点 v 未被访问 |
| 81 | + # 递归访问邻接节点 v |
75 | 82 | self.dfs_recursive(graph, v, visited)
|
76 | 83 |
|
77 | 84 |
|
| 85 | +# 示例图(邻接表形式,节点为字符串,边为无向边) |
78 | 86 | graph = {
|
79 | | - "A": ["B", "C"], |
80 | | - "B": ["A", "C", "D"], |
81 | | - "C": ["A", "B", "D", "E"], |
82 | | - "D": ["B", "C", "E", "F"], |
83 | | - "E": ["C", "D"], |
84 | | - "F": ["D", "G"], |
85 | | - "G": [] |
| 87 | + "1": ["2", "3"], |
| 88 | + "2": ["1", "3", "4"], |
| 89 | + "3": ["1", "2", "4", "5"], |
| 90 | + "4": ["2", "3", "5", "6"], |
| 91 | + "5": ["3", "4"], |
| 92 | + "6": ["4", "7"], |
| 93 | + "7": [] |
86 | 94 | }
|
87 | 95 |
|
88 | | -# 基于递归实现的深度优先搜索 |
| 96 | +# 初始化已访问节点集合 |
89 | 97 | visited = set()
|
90 | | -Solution().dfs_recursive(graph, "A", visited) |
| 98 | +# 从节点 "1" 开始进行深度优先搜索 |
| 99 | +Solution().dfs_recursive(graph, "1", visited) |
91 | 100 | ```
|
92 | 101 |
|
93 | | -## 4. 基于堆栈实现的深度优先搜索 |
| 102 | +## 4. 基于堆栈的深度优先搜索 |
94 | 103 |
|
95 | | -### 4.1 基于堆栈实现的深度优先搜索算法步骤 |
| 104 | +### 4.1 基于堆栈的深度优先搜索算法步骤 |
96 | 105 |
|
97 | | -深度优先搜索算法除了基于递归实现之外,还可以基于堆栈来实现。同时,为了防止多次遍历同一节点,在使用栈存放节点访问记录时,我们将「当前节点」以及「下一个将要访问的邻接节点下标」一同存入栈中,从而在出栈时,可以通过下标直接找到下一个邻接节点,而不用遍历所有邻接节点。 |
| 106 | +除了递归方式,深度优先搜索(DFS)还可以用显式的栈结构实现。为避免重复访问同一节点,栈中不仅存储当前节点,还记录下一个待访问的邻接节点下标。这样每次出栈时,可以直接定位下一个邻接节点,无需遍历全部邻接点。 |
98 | 107 |
|
99 | | -以下是基于堆栈实现的深度优先搜索的算法步骤: |
| 108 | +基于堆栈的 DFS 步骤如下: |
100 | 109 |
|
101 | | -1. 定义 $graph$ 为存储无向图的嵌套数组变量,$visited$ 为标记访问节点的集合变量。$start$ 为当前遍历边的开始节点。定义 $stack$ 用于存放节点访问记录的栈结构。 |
| 110 | +1. 设 $graph$ 为无向图的邻接表,$visited$ 为已访问节点集合,$stack$ 为辅助栈。 |
102 | 111 | 2. 选择起始节点 $u,ドル检查当前节点 $u$ 是否为目标节点(看具体题目要求)。
|
103 | | -3. 如果当前节点 $u$ 是目标节点,则直接返回结果。 |
104 | | -4. 如果当前节点 $u$ 不是目标节点,则将节点 $u$ 以及节点 $u$ 下一个将要访问的邻接节点下标 0ドル$ 放入栈中,并标记为已访问,即 `stack.append([u, 0])`,`visited.add(u)`。 |
105 | | -5. 如果栈不为空,取出 $stack$ 栈顶元素节点 $u,ドル以及节点 $u$ 下一个将要访问的邻接节点下标 $i$。 |
106 | | -6. 根据节点 $u$ 和下标 $i,ドル取出将要遍历的未访问过的邻接节点 $v$。 |
107 | | -7. 将节点 $u$ 以及节点 u 的下一个邻接节点下标 $i + 1$ 放入栈中。 |
108 | | -8. 访问节点 $v,ドル并对节点进行相关操作(看具体题目要求)。 |
109 | | -9. 将节点 $v$ 以及节点 $v$ 下一个邻接节点下标 0ドル$ 放入栈中,并标记为已访问,即 `stack.append([v, 0])`,`visited.add(v)`。 |
110 | | -10. 重复步骤 5ドル \sim 9,ドル直到 $stack$ 栈为空或找到目标节点为止。 |
| 112 | +3. 如果 $u$ 是目标节点,直接返回结果。 |
| 113 | +4. 如果 $u$ 不是目标节点,将 $[u, 0]$(节点 $u$ 及其下一个邻接节点下标 0ドル$)压入栈,并将 $u$ 标记为已访问:`stack.append([u, 0])`,`visited.add(u)`。 |
| 114 | +5. 当栈非空时,弹出栈顶元素 $[u, i],ドル其中 $i$ 表示下一个待访问的邻接节点下标。 |
| 115 | +6. 如果 $i$ 小于 $graph[u]$ 的邻接节点数,则取出 $v = graph[u][i]$。 |
| 116 | +7. 将 $[u, i + 1]$ 压回栈中,表示下次将访问 $u$ 的下一个邻接节点。 |
| 117 | +8. 如果 $v$ 未被访问,则对 $v$ 进行相关操作(如打印、判定等),并将 $[v, 0]$ 压入栈,同时标记 $v$ 已访问。 |
| 118 | +9. 重复步骤 5ドル \sim 8,ドル直到栈为空或找到目标节点为止。 |
| 119 | + |
| 120 | +这种实现方式可以模拟递归调用过程,且便于控制递归深度,适合递归层数较深或需手动管理回溯的场景。 |
111 | 121 |
|
112 | 122 | ### 4.2 基于堆栈实现的深度优先搜索实现代码
|
113 | 123 |
|
114 | 124 | ```python
|
115 | 125 | class Solution:
|
116 | 126 | def dfs_stack(self, graph, u):
|
117 | | - print(u) # 访问节点 u |
118 | | - visited, stack = set(), [] # 使用 visited 标记访问过的节点, 使用栈 stack 存放临时节点 |
119 | | - |
120 | | - stack.append([u, 0]) # 将节点 u,节点 u 的下一个邻接节点下标放入栈中,下次将遍历 graph[u][0] |
121 | | - visited.add(u) # 将起始节点 u 标记为已访问 |
122 | | - |
123 | | - |
| 127 | + """ |
| 128 | + 基于显式栈的深度优先搜索(DFS),适用于无向图/有向图的邻接表表示。 |
| 129 | + :param graph: dict,邻接表,key为节点,value为邻接节点列表 |
| 130 | + :param u: 起始节点 |
| 131 | + """ |
| 132 | + visited = set() # 记录已访问节点,防止重复遍历 |
| 133 | + stack = [] # 显式栈,模拟递归过程 |
| 134 | + |
| 135 | + stack.append([u, 0]) # 入栈:节点u及其下一个待访问邻接节点的下标 0 |
| 136 | + visited.add(u) # 标记起始节点已访问 |
| 137 | + print(u) # 访问起始节点 |
| 138 | + |
124 | 139 | while stack:
|
125 | | - u, i = stack.pop() # 取出节点 u,以及节点 u 下一个将要访问的邻接节点下标 i |
126 | | - |
127 | | - if i < len(graph[u]): |
128 | | - v = graph[u][i] # 取出邻接节点 v |
129 | | - stack.append([u, i + 1]) # 下一次将遍历 graph[u][i + 1] |
130 | | - if v not in visited: # 节点 v 未访问过 |
131 | | - print(v) # 访问节点 v |
132 | | - stack.append([v, 0]) # 下一次将遍历 graph[v][0] |
133 | | - visited.add(v) # 将节点 v 标记为已访问 |
134 | | - |
| 140 | + cur, idx = stack.pop() # 取出当前节点及其下一个邻接节点下标 |
| 141 | + neighbors = graph[cur] # 当前节点的所有邻接节点 |
| 142 | + |
| 143 | + # 如果还有未遍历的邻接节点 |
| 144 | + if idx < len(neighbors): |
| 145 | + v = neighbors[idx] # 取出下一个邻接节点 |
| 146 | + stack.append([cur, idx + 1]) # 当前节点下标 + 1,回溯时继续遍历下一个邻接点 |
| 147 | + |
| 148 | + if v not in visited: |
| 149 | + print(v) # 访问新节点 |
| 150 | + visited.add(v) # 标记为已访问 |
| 151 | + stack.append([v, 0]) # 新节点入栈,准备遍历其邻接节点 |
| 152 | + |
| 153 | + # 也可以返回 visited 集合,便于后续处理 |
| 154 | + # return visited |
135 | 155 |
|
| 156 | +# 示例图(邻接表形式,节点为字符串,边为无向边) |
136 | 157 | graph = {
|
137 | | - "A": ["B", "C"], |
138 | | - "B": ["A", "C", "D"], |
139 | | - "C": ["A", "B", "D", "E"], |
140 | | - "D": ["B", "C", "E", "F"], |
141 | | - "E": ["C", "D"], |
142 | | - "F": ["D", "G"], |
143 | | - "G": [] |
| 158 | + "1": ["2", "3"], |
| 159 | + "2": ["1", "3", "4"], |
| 160 | + "3": ["1", "2", "4", "5"], |
| 161 | + "4": ["2", "3", "5", "6"], |
| 162 | + "5": ["3", "4"], |
| 163 | + "6": ["4", "7"], |
| 164 | + "7": [] |
144 | 165 | }
|
145 | 166 |
|
146 | | -# 基于堆栈实现的深度优先搜索 |
147 | | -Solution().dfs_stack(graph, "A") |
| 167 | +# 从节点 "1" 开始进行深度优先搜索 |
| 168 | +Solution().dfs_stack(graph, "1") |
148 | 169 | ```
|
149 | 170 |
|
150 | 171 | ## 5. 深度优先搜索应用
|
@@ -198,17 +219,19 @@ Solution().dfs_stack(graph, "A")
|
198 | 219 |
|
199 | 220 | #### 5.1.3 解题思路
|
200 | 221 |
|
201 | | -如果把上下左右相邻的字符 `'1'` 看做是 `1` 个连通块,这道题的目的就是求解一共有多少个连通块。 |
| 222 | +本题实质是统计二维网格中「上下左右」相连的 `'1'` 组成的连通块数量,即岛屿的个数。 |
202 | 223 |
|
203 | | -使用深度优先搜索或者广度优先搜索都可以。 |
| 224 | +可以采用深度优先搜索(DFS)或广度优先搜索(BFS)来实现。 |
204 | 225 |
|
205 | 226 | ##### 思路 1:深度优先搜索
|
206 | 227 |
|
207 | | -1. 遍历 $grid$。 |
208 | | -2. 对于每一个字符为 `'1'` 的元素,遍历其上下左右四个方向,并将该字符置为 `'0'`,保证下次不会被重复遍历。 |
209 | | -3. 如果超出边界,则返回 0ドル$。 |
210 | | -4. 对于 $(i, j)$ 位置的元素来说,递归遍历的位置就是 $(i - 1, j)$、$(i, j - 1)$、$(i + 1, j)$、$(i, j + 1)$ 四个方向。每次遍历到底,统计数记录一次。 |
211 | | -5. 最终统计出深度优先搜索的次数就是我们要求的岛屿数量。 |
| 228 | +1. 遍历整个 $grid$ 网格。 |
| 229 | +2. 当遇到字符为 `'1'` 的格子时,说明发现了新的岛屿,执行深度优先搜索(DFS): |
| 230 | + - 将当前格子标记为 `'0'`,避免重复访问。 |
| 231 | + - 递归访问其上下左右四个相邻格子(即 $(i - 1, j)$、$(i + 1, j)$、$(i, j - 1)$、$(i, j + 1)$)。 |
| 232 | + - 如果递归过程中遇到越界或当前格子为 `'0'`,则直接返回。 |
| 233 | +3. 每当一次完整的 DFS 结束,岛屿计数加一。 |
| 234 | +4. 最终 DFS 执行的次数即为岛屿的总数。 |
212 | 235 |
|
213 | 236 | ##### 思路 1:代码
|
214 | 237 |
|
@@ -290,12 +313,12 @@ class Solution:
|
290 | 313 |
|
291 | 314 | ##### 思路 1:深度优先搜索
|
292 | 315 |
|
293 | | -1. 使用哈希表 $visitedDict$ 来存储原图中被访问过的节点和克隆图中对应节点,键值对为「原图被访问过的节点:克隆图中对应节点」。 |
294 | | -2. 从给定节点开始,以深度优先搜索的方式遍历原图。 |
295 | | - 1. 如果当前节点被访问过,则返回隆图中对应节点。 |
296 | | - 2. 如果当前节点没有被访问过,则创建一个新的节点,并保存在哈希表中。 |
297 | | - 3. 遍历当前节点的邻接节点列表,递归调用当前节点的邻接节点,并将其放入克隆图中对应节点。 |
298 | | -3. 递归结束,返回克隆节点。 |
| 316 | +1. 使用哈希表 $visitedDict$ 记录原图节点与其对应的克隆节点,键为原图节点,值为克隆节点。 |
| 317 | +2. 从给定节点出发,采用深度优先搜索遍历原图: |
| 318 | + 1. 如果当前节点已在哈希表中,直接返回对应的克隆节点,避免重复克隆。 |
| 319 | + 2. 如果当前节点未被访问,则新建一个克隆节点,并将其加入哈希表。 |
| 320 | + 3. 遍历当前节点的所有邻居,对每个邻居递归调用克隆函数,并将返回的克隆节点加入当前克隆节点的邻居列表。 |
| 321 | +3. 递归返回当前节点的克隆节点。 |
299 | 322 |
|
300 | 323 | ##### 思路 1:代码
|
301 | 324 |
|
|
0 commit comments