广度优先搜索
核心思想:把⼀些问题抽象成图,从⼀个点开始,向四周开始扩散。⼀般来说,我们写 BFS 算法都是⽤「队列」这种数据结构,每次将⼀个节点周围的所有节点加⼊队列。
常⻅场景:问题的本质就是让你在⼀幅「图」中找到从起点start 到终点 target 的最近距离。
BFS 相对 DFS 的最主要的区别是:BFS 找到的路径⼀定是最短的,但代价就是空间复杂度可能⽐ DFS ⼤很多。
BFS的代码框架如下:
111. 二叉树最小深度
解法:BFS
⾸先明确⼀下起点 start 和终点 target 是什么,怎么判断到达了终点?
显然起点就是 root 根节点,终点就是最靠近根节点的那个「叶⼦节点」嘛,叶⼦节点就是两个⼦节点都是None 的节点。按照上述框架撰写求解代码如下:
from queue import Queue
class Solution:
def minDepth(self, root: TreeNode) -> int:
if not root:
return 0
que = Queue()
que.put(root)
# root 本身就是⼀层,depth 初始化为 1
step = 1
while not que.empty():
size = que.qsize()
# 将当前队列中的所有节点向四周扩散
for i in range(size):
node = que.get()
# 判断是否到达终点
if not node.left and not node.right:
return step
# 将 cur 的相邻节点加⼊队列
if node.left:
que.put(node.left)
if node.right:
que.put(node.right)
# 层数+1
step += 1
return step
752. 打开转盘锁
解法:BFS
难点就在于,不能出现 deadends,应该如何计算出最少的转动次数呢?
第⼀步,我们不管所有的限制条件,不管 deadends 和 target 的限制,就思考⼀个问题:如果让你设计⼀个算法,穷举所有可能的密码组合,你怎么做?
直接的想法就是穷举,再简单⼀点,如果你只转⼀下锁,有⼏种可能?总共有 4 个位置,每个位置可以向上转,也可以向下转,也就是有 8 种可能。
⽐如说从 “0000” 开始,转⼀次,可以穷举出 “1000”, “9000”, “0100”, “0900”… 共 8 种密码。然后,再以这 8 种密码作为基础,对每个密码再转⼀下,穷举出所有可能…
这个过程可以抽象成⼀幅图,每个节点有 8 个相邻的节点,⼜让你求最短距离,这就是典型的BFS问题。可以使用上述框架求解。同时我们需要考虑以下情况的处理,对框架代码稍加修改:
1、会⾛回头路。⽐如说我们从 “0000” 拨到 “1000”,但是等从队列拿出 “1000” 时,还会拨出⼀个"0000",这样的话会产⽣死循环。
2、没有终⽌条件,按照题⽬要求,我们找到 target 就应该结束并返回拨动的次数。
3、没有对 deadends 的处理,按道理这些「死亡密码」是不能出现的,也就是说你遇到这些密码的时候需要跳过。
from queue import Queue
class Solution:
def openLock(self, deadends: List[str], target: str) -> int:
passwords = '0000'
que = Queue()
que.put(passwords)
# 记录已经穷举过的密码,防⽌⾛回头路
visited = set(deadends)
# 从起点开始启动⼴度优先搜索
step = 0
# 记录需要跳过的死亡密码
deads = set(deadends)
while not que.empty():
size = que.qsize()
# 将当前队列中的所有节点向周围扩散
for _ in range(size):
node = que.get()
# 判断是否到达终点
if node in deads:
continue
if node == target:
return step
# 将⼀个节点的未遍历相邻节点加⼊队列
for i in range(4):
up = self.addOne(node, i)
if up not in visited:
que.put(up)
visited.add(up)
down = self.minusOne(node, i)
if down not in visited:
que.put(down)
visited.add(down)
# 在这⾥增加步数
step += 1
# 如果穷举完都没找到⽬标密码,那就是找不到了
return -1
# 将 passwords[position] 向上拨动⼀次
def addOne(self, passwords, position):
passwords = list(map(lambda x: int(x), list(passwords)))
if passwords[position] == 9:
passwords[position] = 0
else:
passwords[position] += 1
return "".join(map(lambda x: str(x), passwords))
# 将 passwords[position] 向下拨动⼀次
def minusOne(self, passwords, position):
passwords = list(map(lambda x: int(x), list(passwords)))
if passwords[position] == 0:
passwords[position] = 9
else:
passwords[position] -= 1
return "".join(map(lambda x: str(x), passwords))
总结
1、为什么 BFS 可以找到最短距离,DFS 不⾏吗?
DFS也能找到最短距离,但是根据BFS 的逻辑,depth 每增加⼀次,队列中的所有节点都向前迈⼀步,这保证了第⼀次到达终点的时候,⾛的步数是最少的。
形象点说,DFS 是线,BFS 是⾯;DFS 是单打独⽃,BFS 是集体⾏动。
2、既然 BFS 那么好,为啥 DFS 还要存在?
BFS 可以找到最短距离,但是空间复杂度⾼,⽽ DFS 的空间复杂度较低。
以处理⼆叉树问题为例⼦,假设给你的这个⼆叉树是满⼆叉树,节点数为 N,对于 DFS 算法来说,空间复杂度⽆⾮就是递归堆栈,最坏情况下顶多就是树的⾼度,也就是 O(logN)。
但是你想想 BFS 算法,队列中每次都会储存着⼆叉树⼀层的节点,这样的话最坏情况下空间复杂度应该是树的最底层节点的数量,也就是 N/2,⽤ Big O 表示的话也就是 O(N)。
由此观之,BFS 还是有代价的,⼀般来说在找最短路径的时候使⽤ BFS,其他时候还是 DFS 使⽤得多⼀些。