什么时候需要用到BFS呢?通常图的遍历(Traversal in Graph)
主要有以下四种情况:
- 层级遍历 - Level Order Traversal
最经典的就是二叉树的level order traversal - 由点及面 Connected Component
即由一个点,找出图中所有的点。比如给出无向连通图(Undirected Connected Graph)中的一个点,找到这个图里的所有点。 - 拓扑排序 Topological Sorting
拓扑排序是对有向无环图的顶点的一种排序,它使得如果存在一条从顶点A到顶点B的路径,那么在排序中B出现在A的后面。注意这里一定是有向图。
注意:
3.1 一个有向图的拓扑排序可能存在多个,通常的拓扑排序程序只需要输出一个即可。
3.2 注意一个有向图的拓扑排序也可能不存在,一个有向图的拓扑排序的充要条件就是存在环。
3.3 拓扑排序也可以用DFS,但不如BFS简单。
3.4 找有向图的环也可以用Tarjan算法。 - 简单图的最短路径 Shortest Path in Simple Graph
注意这里一定要是简单图,也就是图中每条边长度都是1(或边长都相同)。为什么呢? 因为宽搜可以理解为一层层进行的,就跟二叉树遍历一样。要是层和层之间,或每层的长度都不一样,宽搜就没法进行了。通常这里简单图求最短路径用到BFS的都是无向图,极少数是有向图。注意:
4.1 求最短路径也可以用DP,Dijkstra, Bellman-Ford算法等。
4.2 BFS不能用来求简单图的最长路径。Dijkstra也不能求最长路径。Bellman-Ford算法可以求最长路径,有环也可以,只要把图中的边权改为原来的相反数即可。也可以用Floyd-Warshall求最长路径,但不能有环(Floyd算法的本质是DP)。
4.3 BFS对无向图和有向图都适用。
4.4 二叉树中进行 BFS 和图中进行 BFS 最大的区别就是二叉树中无需使用 HashSet或HashMap来存储访问过的节点(丢进过 queue 里的节点)
因为二叉树这种数据结构没有环,不可能出现一个节点的儿子的儿子是自己的情况。但是在图中,一个节点的邻居的邻居就可能是自己了。
另外,BFS还有双向BFS, DFS却没有双向DFS。双向BFS适用于如下场景:
- 无向图
- 所有边的长度都为 1 或者长度都一样
- 同时给出了起点和终点
以上 3 个条件都满足的时候,可以使用双向宽度优先搜索来求出起点和终点的最短距离。
什么是用到DFS呢?
- 找所有解/方案(或者所有组合,全部路径之类)的题,基本上都可以用DFS。当然有些用BFS也可以做。
- 求最长路径也可以用DFS。
注意:
- DP不能用来求所有解,而只能求所有解的个数。另外,DP 也用来求最优解,比如说最短路径(BFS也可以)。但DP可以求最长路径但BFS不可以。
- DFS的题目除了二叉树以外,90%都是排列组合题。
- DFS如果迭代层数太深,会有stack overflow问题 。但是对于二叉树和排列组合之类的问题用DFS一般不会有stack overflow。二叉树的迭代深度平均O(logn),最差O(n)。排列组合的迭代深度最多是O(n),而n一般不会很,比较典型的是20,30左右。
4)关于BFS和DFS的时间复杂度
DFS搜索的时间复杂度是O(答案个数*构造每个答案时间),或者说是O(解的个数 * 产生解的复杂度)。所以subsets的时间复杂度是O(n*2^n), permutation的时间复杂度是O(n*n!)。
我们以subsets举例 (引用自九章):
搜索时间复杂度公式:O(解的个数 * 产生解的复杂度), 因为解的平均长度是n/2,所以扩2倍也没有关系,可以认为是n的。 实际复杂度是O(2^n * n / 2) = O(2^(n-1) * n), 这是根据公式估算的。具体来看这个复杂度用数学推到如何算出来;
严格意义上来讲是复杂度是这样的:
**1 * 长度为1的解的个数 + 2 * 长度为2的解的个数 + 3 * 长度为3的解的个数 + n * 长度为n的解的个数 = **
1 * c(n, 1) + 2 * c(n, 2) + … + n * c(n,n)
// c(m,n) 是排列组合中的组合
又因为
i * C(n, i) = i * n! / [i! (n-i)!] = n! / [(i-1)! (n-i)!] = n * (n-1)! / [(i-1)! (n-i)!] = n * C(n-1,i-1)
所以
**1 * c(n, 1) + 2 * c(n, 2) + … + n * c(n,n) = sum(i * c(n, i)) = sum(n * c(n - 1, i - 1)) = n * sum(c(n-1, i-1)) **
其中 1 <= i <=n
sum(c(n - 1, i - 1)) 就是杨辉三角的某一层的求和 ,为2^(n - 1)
最后得出:1 * c(n, 1) + 2 * c(n, 2) + … + n * c(n,n) = 2^(n - 1) * n
跟我们的估算完全一致。有时候不需要数学推导,因为是大O,又是搜索算法,故用我们的公式估计一个大概就可以了。
permutations 可以用类似的方式推导。
注意这里是搜索的时间复杂度估计的公式,并不是特别指DFS的,BFS也适合。
对于BFS,因为每个点进入queue一次,加上每个点出来时候处理的复杂度,可以认为是O(点的个数 * 每个点处理的平均复杂度)
再对比一下DP。DP的时间复杂度是O(状态总数×计算每个状态时间复杂度)。
5) 关于BFS和DFS的空间复杂度
BFS的空间复杂度是同一层的最大宽度(最坏情况所有点都在一层,所以是O(n))。
DFS的空间复杂度取决于深度,一般堆和栈都要计算在内。但有时人们认为系统栈很小,所以只算堆空间。
对于排列组合问题的空间复杂度来说,一般DFS优于BFS。因为排列的所有解个数是O(n!),组合的所有解个数是O(2^n) 。
而DFS递归的深度是O(n),即DFS的空间复杂度是O(n)。而BFS对排列最坏空间复杂度是排列O(n!),对组合的最坏空间复杂度是O(2^n)。
下面内容引用自
https://blog.csdn.net/Charles_ke/article/details/82497543
v为图的顶点数,E为边数。
BFS的复杂度分析:
BFS是一种借用队列来存储的过程,分层查找,优先考虑距离出发点近的点。无论是在邻接表还是邻接矩阵中存储,都需要借助一个辅助队列,v个顶点均需入队,最坏的情况下,空间复杂度为O(v)。
邻接表形式存储时,每个顶点均需搜索一次,时间复杂度T1=O(v),从一个顶点开始搜索时,开始搜索,访问未被访问过的节点。最坏的情况下,每个顶点至少访问一次,每条边至少访问1次,这是因为在搜索的过程中,若某结点向下搜索时,其子结点都访问过了,这时候就会回退,故时间复 杂度为O(E),算法总的时间复 度为O(|V|+|E|)。
邻接矩阵存储方式时,查找每个顶点的邻接点所需时间为O(V),即该节点所在的该行该列。又有n个顶点,故算总的时间复杂度为O(|V|^2)。
DFS复杂度分析
DFS算法是一一个递归算法,需要借助一个递归工作栈,故它的空问复杂度为O(V)。
遍历图的过程实质上是对每个顶点查找其邻接点的过程,其耗费的时间取决于所采用结构。
邻接表表示时,查找所有顶点的邻接点所需时间为O(E),访问顶点的邻接点所花时间为O(V),此时,总的时间复杂度为O(V+E)。
邻接矩阵表示时,查找每个顶点的邻接点所需时间为O(V),要查找整个矩阵,故总的时间度为O(V^2)。