使用BFS,DFS的题目,在leetcode上一般标记为medium或者hard。但从思维逻辑上看,其难度定义偏高。可能从代码量来看,写一道BFS或者DFS的篇幅比其他类型的题目要多。
BFS,DFS既然思维固定,必然有其套路。套用模板方法,让我们的解题更加流畅。有如庖丁解牛,游刃有余。
BFS思路是先将邻居加进来,如果只是口述的话难免枯燥。
下面将以图上的搜索(找start_node到end_node之间的最短距离长度)为例,阐述怎样将思维变成代码。
正如链表其实质为节点和指向下一个节点的指针,图的实质也就是图上的节点和连接各个节点的边。
由此,对于图(这里对有向图和无向图不做区分)其实有两种表示方法:
def initial_graph(n, edges):
dict_graph = {
}
for i in range(n):
dict_graph[i] = []
num_e = len(edges)
for i in range(num_e):
u = edges[i][0]
v = edges[i][1]
dict_graph[u].append(v)
dict_graph[v].append(u)
return dict_graph
bfs适用于层级搜索,队列先进先出,量身定做。
python中队列两种导入方法为:
# 1.使用queue
from queue import Queue
q = Queue() # 定义,为什么是这样涉及python的设计,不是很懂
q.put(node) # 放入
q.get() # 出队
# 2.使用deque
import collections
q = collections.deque() # 双向队列
q.append() # 入队
q.popleft() # 出队
# 使用Queue()定义
q.put(start_node)
# 为防止无向图中回溯,使用set阻断
hash_set = set()
hash_set.add(start_node)
step = 0
while not q.empty():
size = len(q)
step += 1
for iter_num in range(size):
node = q.get() # current node
# get the neighbor
for neighbor in dict_graph[node]:
if neighbor == end_node: # find it!!!!
return step
if neighbor in hash_set:
continue # avoid backtracking
hast_set.add(node)
q.put(neighbor)
return 0 # can't find
大体的bfs过程如上所示,一些题目难点在于bfs的细节实现,例如:
DFS思路是一条路走到底,撞到了墙再回头。
同上述BFS解析,下面将以找数组num的所有子集为例,对dfs的过程进行阐述。
已知数组num[1,2,3](假设数组已经排序),要求他的所有子集,dfs的过程如草图2所示,这和人的思维方式几乎一模一样。
从空集开始,然后将1加入,1加入之后,下一次加入2,之后加入3,这时startindex指针移出数组范围,开始回溯。将3移除后,回溯到2,(这里内部循环已经结束,执行到函数底部),2回溯之后,到1,1可以再加入3。这是程序的过程,关键是怎么写代码。
因为dfs涉及到程序的递归调用,一般dfs不嵌入到程序内部过程。在主函数中,将dfs作为辅助函数调用,故此将dfs函数命名为dfsHelper。
#python中,一般定义如下:
def dfsHelper(self,
input parameter...,
startIndex,
tmp,
result):
"""
其中:
input parameter一般指不变的已知量:比如数组num,在矩阵的搜索中指矩阵grid,在图中指图graph和目标节点end_node;
startIndex用来标记当前指针在数组(矩阵或者图中的位置),随着递归而改变;
tmp:用来暂存递归过程中的结构,随着递归而改变;
result:一般为全局变量,用来保存所有的结果。
在程序撰写中,可以定义很多经常使用的量为常量,从而使dfsHelper函数更加短小精悍。
"""
递归的过程就是先在tmp中加入当前元素,然后调用自身(这中间指针后移),最后在tmp中移除当前元素。
若不看dfsHelper的循环调用,可以这样理解;当tmp=[]时,将startIndex后移是依次将1,2,3加入tmp,也就是搜索的第一层。将"1"pop掉即保持了当前层的纯粹,没有这个操作会导致递归层次混乱。
for i in range(startIndex, len(nums)):
tmp.append(nums[i])
self.dfsHelper(nums, i + 1, tmp, result)
tmp.pop()
由于是求所有的子集,再每次递归时其实tmp保存了不同的子集,故在dfs函数的最开始要考虑将tmp加入到result。由于tmp其实是指向一个地址的指针,在递归中可能随时改变,因此在加入之前先将tmp的内容拷贝一份而不是直接拿出,保证运行结果的正确。
这里c++是否可以直接取值?有时间再研究下。
tmp_copy = tmp.copy()
result.append(tmp_copy)
dfs的出口是这类题目的难点,因其变化多端。当定义dfsHelper函数中使用startIndex,很明显需要借助此标志进行判定。所以有:
if startIndex == len(nums):
return
在其他的问题中,比如二维矩阵中,可能是index越界,可能是定义了visited而恰好当前位置访问过,这些针对特定的问题需要特殊对待。
将此问题的完整解法列在下面,对此问题便更进一步理解了。
class Solution:
"""
@param nums: A set of numbers
@return: A list of lists
"""
def subsets(self, nums):
# 从空集开始搜索,每次将nums的节点加入空集中
result = []
tmp = []
if nums is None:
return None
nums.sort()
startIndex = 0
self.dfsHelper(nums, startIndex, tmp, result)
return result
def dfsHelper(self, nums, startIndex, tmp, result):
# dfs出口
tmp_copy = tmp.copy() # 拷贝一份
result.append(tmp_copy)
if startIndex == len(nums):
return
for i in range(startIndex, len(nums)):
tmp.append(nums[i])
self.dfsHelper(nums, i + 1, tmp, result)
tmp.pop()
return
个人觉得,DFS比BFS要难,因其在定义dfsHelper函数时多种多样,可以很灵活地根据自己的需求和习惯定义。但,换了个马甲,其内部的思维并没有变,计算量(所说的复杂度)大致没有变。
所谓的算法,更侧重于“法”,而非“算”,即它更多的是一种方法,而不是一种算术。
更多精彩文章,欢迎关注公众号“Li的白日呓语”。