深度优先搜索算法和广度优先搜索算法都是基于“图”这种数据结构的。
作为图的搜索算法,既可用于有向图,也可用于无向图,以下均用无向图讲解。
广度优先搜索
Breadth-First-Search,BFS。
一种“地毯式”层层推进的搜索策略,先查找离起始顶点最近的,然后是次近的,依次往外搜索。
s 表示起始顶点,t 表示终止顶点。搜索一条从 s 到 t 的路径。
实际上,求得的路径就是从 s 到 t 的最短路径。
代码(下面有完整代码)中包含3个重要的辅助变量:
- visited -> list,保存已经访问的顶点,避免重复访问;
- q -> queue,队列,逐层访问。
每次弹出第k层顶点时,将第与k连接的k+1层顶点添加至队尾(添加前会通过visited检测是否已经访问,只入队未访问过的顶点); - prev -> list,保存搜索路径,例如prev[3] = 2,代表顶点3的前驱顶点为顶点2,用于后期打印。
时间复杂度O(n),空间复杂度O(n)。
最坏的情况,s 与 t 相距较远,需要遍历完整的图才能找到,而遍历是不重复的,最多遍历全部顶点,所以复杂度是O(n)。
中间的三个辅助变量,大小均不会超过顶点个数,所以空间复杂度也是O(n)。
深度优先搜索
Depth-First-Search,DFS。
用“走迷宫”举例,随意选择一个岔路口来走,走着走着发现走不通,你就回退到上一个岔路口,重新选择一条路继续走,直到最终找到出口。 这种走法就是一种深度优先搜索策略。
用的是回溯思想,后面会单独讲回溯。
深度优先搜索找出的路径并不是最短路径。
代码中用到的辅助变量:
- visited
- prev
- found -> bool,标识作用,已经找到终止顶点 t 后,不再递归查找。
前两个变量与上面的广度优先遍历一致。
时间复杂度O(n),空间复杂度O(n)。
每条边最多会被访问两次,所以时间复杂度为O(n)。
空间复杂度原因与广度优先搜索一致。
代码
from collections import deque
class Graph:
"""无向图"""
def __init__(self, num_vertices):
# 顶点数量
self._num_vertices = num_vertices
# [[], [], [] ...],内部每个小列表存储该顶点的相连的其他顶点
self._adjacency = [[] for _ in range(num_vertices)]
def add_edge(self, s, t):
"""无向图一条边存两次"""
self._adjacency[s].append(t)
self._adjacency[t].append(s)
def _generate_path(self, s, t, prev):
# 用or会打印第一个顶点,用and则不会
if prev[t] or s != t:
yield from self._generate_path(s, prev[t], prev)
yield str(t)
def bfs(self, s, t):
"""通过广度优先搜索,打印s -> t的路径"""
if s == t:
return
# 定义文中提到的的三个重要变量,并将第一个顶点s加入各变量中
visited = [False] * self._num_vertices
visited[s] = True
q = deque()
q.append(s)
prev = [None] * self._num_vertices
while q:
v = q.popleft()
# 遍历顶点v相连的其他顶点,并将每个顶点加入到队列中继续遍历
for neighbour in self._adjacency[v]:
# 各顶点未被遍历
if not visited[neighbour]:
# 记录path
prev[neighbour] = v
# 符合终止条件则打印
if neighbour == t:
print(' -> '.join(self._generate_path(s, t, prev)))
return
# 记录遍历过的顶点
visited[neighbour] = True
# 将加入队列,后面会遍历该顶点
q.append(neighbour)
def dfs(self, s, t):
found = False
visited = [False] * self._num_vertices
prev = [None] * self._num_vertices
# 递归
def _dfs(from_vertex):
nonlocal found
if found:
return
visited[from_vertex] = True
if from_vertex == t:
found = True
return
for neighbour in self._adjacency[from_vertex]:
if not visited[neighbour]:
prev[neighbour] = from_vertex
_dfs(neighbour)
_dfs(s)
print(' -> '.join(self._generate_path(s, t, prev)))
if __name__ == "__main__":
"""
0 ----- 1 ----- 2
| / | / |
| / | / |
| / | / |
| / | / |
| / | / |
3 ----- 4 ----- 5
| / |
| / |
| / |
| / |
| / |
6 ----- 7
"""
graph = Graph(8)
graph.add_edge(0, 1)
graph.add_edge(0, 3)
graph.add_edge(1, 2)
graph.add_edge(1, 4)
graph.add_edge(2, 5)
graph.add_edge(3, 4)
graph.add_edge(4, 5)
graph.add_edge(4, 6)
graph.add_edge(5, 7)
graph.add_edge(6, 7)
print(graph._adjacency)
# 输出结果:[[1, 3], [0, 2, 4], [1, 5], [0, 4], [1, 3, 5, 6], [2, 4, 7], [4, 7], [5, 6]]
graph.bfs(0, 7)
# 输出结果:0 -> 1 -> 2 -> 5 -> 7
graph.dfs(0, 7)
# 输出结果:0 -> 1 -> 2 -> 5 -> 4 -> 6 -> 7
本文是极客时间王争 数据结构与算法 课程的笔记,推荐此课,喜欢可以购买课程。
代码引用自 https://github.com/wangzheng0822/algo/blob/master/python/31_bfs_dfs/bfs_dfs.py ,根据代码作了注释和部分优化。