前言:图的遍历算法DFS和BFS是许多图算法的基础,所以有必要单独拎出来总结一下。DFS和BFS主要是运用于对于图和树的搜索,很多问题模型都是可以建模变成一个图或者树的,所以差不多不少问题都会涉及到这两个。比如求二叉树深度,可以是递归的方法,属于DFS(深度优先搜索);另一种方法是按照层次遍历,属于BFS(广度优先搜索),想看代码的可以看《剑指Offer(三十八):二叉树的深度》。再比如寻找一条路径,利用DFS或BFS寻找从源顶点theSource到终点theDestination的路径。
DFS算法可以产生目标图的相应拓扑排序表,利用拓扑排序表可以方便的解决很多相关的图论问题,如最大路径问题等等。
BFS算法是连通图的一种遍历算法,这一算法也是很多重要的图的算法的原型,Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。
1、DFS的概念
①出现背景
深度优先搜索是一种在开发爬虫早期使用较多的方法。它的目的是要达到被搜索结构的叶结点(即那些不包含任何超链的HTML文件) 。在一个HTML文件中,当一个超链被选择后,被链接的HTML文件将执行深度优先搜索,即在搜索其余的超链结果之前必须先完整地搜索单独的一条链。深度优先搜索沿着HTML文件上的超链走到不能再深入为止,然后返回到某一个HTML文件,再继续选择该HTML文件中的其他超链。当不再有其他超链可选择时,说明搜索已经结束。
②DFS定义
DFS属于图遍历算法的一种,其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。
深度优先遍历图的算法思想是,从图中某顶点v出发:
(1)访问顶点v;
(2)依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;
(3)若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行DFS,直到图中所有顶点均被访问过为止。
DFS就是像走迷宫一样一条路走到头直到走不通才回到前一个换一条路,所以当人们刚刚掌握DFS的时候常常用它来走迷宫。
③遍历过程
比如这个图,DFS的话从1开始,先找到其中一个相连的,2被找到了,然后直接开始从2开始搜索,3被找到了,然后从3开始搜索,4被找到了,然后从4开始搜索,5被找到了,然后从5开始搜索,忽略已经找到的所以啥都没找到。然后没路可走了,回到前面去再走另一条路,从4开始,6被找到了,然后又没路可走了,然后再回去前面4,然后没路了,回去前面3,然后一直这样。
④Java代码
DFS需要一个栈,因为每次都是搜到之后不停的往下搜,走不通后再回溯到前一个节点换一条路搜索,符合先进先出。但是一般来说不用栈,而是直接通过函数的递归就行。
/*
* DFS,深度优先搜索算法
*/
public class DFSTraverse
{
private boolean[] visited;
//从顶点index开始遍历
public DFSTraverse(Digraph graph, int index) {
visited = new boolean[graph.getVertexsNum()];
dfs(graph,index);
}
private void dfs(Digraph graph, int index) {
visited[index] = true;
for(int i : graph.adj(index)) {
if(!visited[i])
dfs(graph,i);
}
}
}
2、BFS的概念
①出现背景
求解节点间的最短路径,因为它的特点是 "搜到就是最优解"。
②定义
广度优先搜索(Breadth-First Search),又称作宽度优先搜索。BFS是一种完备策略,即只要问题有解,它就一定可以找到解。并且,广度优先搜索找到的解,还一定是路径最短的解。但是它盲目性较大,尤其是当目标节点距初始节点较远时,将产生许多无用的节点,因此其搜索效率较低。一般需求最优解的时候用BFS。
所谓广度,就是一层一层的往向下遍历,算法首先搜索和定义v距离为k的所有顶点,然后再去搜索和v距离为k+l的其他顶点。
算法思想:
1、访问顶点vi ;
2、访问vi 的所有未被访问的邻接点w1 ,w2 , …wk ;
3、依次从这些邻接点(在步骤②中访问的顶点)出发,访问它们的所有未被访问的邻接点; 依此类推,直到图中所有访问过的顶点的邻接点都被访问;
伪代码:
BFS()
{
初始化队列
while(队列不为空且未找到目标节点)
{
取队首节点扩展,并将扩展出的节点放入队尾;
必要时要记住每个节点的父节点;
}
}
③遍历过程
还是这张图,如果从1开始进行搜索的话,BFS的步骤就是,先搜索所有和1相连的,也就是2和5被找到了,然后再从2开始搜索和他相连的,也就是3被找到了,然后从5搜,也就是4被找到了,然后从3开始搜索,4被找到了,但是4之前已经被5找到了,所以忽略掉就行。然后3开始搜索,忽略4所以啥都没搜到,然后从4开始,6被找到了。
④Java代码
BFS需要一个队列这种数据结构来保存,因为每次找到和u相连的之后要一个个找这些点,符合先进先出。
/*
* BFS,广度优先搜索
*/
public class BFSTraverse
{
private boolean[] visited;
public BFSTraverse(AdjListDigraph graph, int index)
{
visited = new boolean[graph.getVertexsNum()];
bfs(graph,index);
}
private void bfs(AdjListDigraph graph, int index)
{
//在JSE中LinkedList实现了Queue接口
Queue queue = new LinkedList<>();
visited[index] = true;
queue.add(index);
while(!queue.isEmpty())
{
int vertex = queue.poll();
for(int i : graph.adj(vertex))
{
if(!visited[i])
{
visited[i] = true;
queue.offer(i);
}
}
}
}
}
1、深度优先搜索算法(Depth-First-Search,缩写为 DFS):
是一种利用递归实现的搜索算法。简单来说,其搜索过程和 “不撞南墙不回头” 类似。
DFS其实就是暴力把所有的路径都搜索出来,它运用了回溯,保存这次的位置,深入搜索,都搜索完了便回溯回来,换条路搜下一个位置,直到把所有最深位置都搜一遍,要注意的一点是,搜索的时候有记录走过的位置,标记完后可能要改回来。
这里面有个很重要的算法设计方法,回溯法。
回溯法是一种搜索法,按条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法。
2、广度优先搜索算法(Breadth-First-Search,缩写为 BFS):
是一种利用队列实现的搜索算法。简单来说,其搜索过程和 “湖面丢进一块石头激起层层涟漪” 类似。
BFS从某点开始,走四面可以走的路,然后在从这些路,在找可以走的路,直到最先找到符合条件的。
空间复杂度
因为所有节点都必须被储存,因此BFS的空间复杂度为 O(|V| + |E|),其中 |V| 是节点的数目,而 |E| 是图中边的数目。注:另一种说法称BFS的空间复杂度为O(B^M),其中 B 是最大分支系数,而 M 是树的最长路径长度。由于对空间的大量需求,因此BFS并不适合解非常大的问题。
时间复杂度
最差情形下,BFS必须寻找所有到可能节点的所有路径,因此其时间复杂度为 O(|V| + |E|),其中 |V| 是节点的数目,而 |E| 是图中边的数目。
3、区别
BFS 的重点在于队列,而 DFS 的重点在于递归。这是它们的本质区别。
BFS适合用来搜索最短径路的解,DFS适合搜索全部的解。
DFS是舍弃时间换取空间,BFS是舍去空间换取时间。
因为DFS要走很多的路径,可能都是没用的,(做有些题目的时候要进行剪枝,就是确定不符合条件的就可以退出,以免浪费时间,否则有些题目会TLE即时间超限);而BFS可以走的点要存起来,需要队列,因此需要空间来储存,但是快一点。
BFS用来搜索最短路径的解是比较合适的,比如求最少步数的解,最少交换次数的解。因为bfs搜索过程中遇到的解一定是离最初位置最近的,所以遇到一个解,一定就是最优解,此时搜索算法可以终止,而如果用dfs,会搜一些其他的位置,需要搜很多次,然后还要一个东西来记录这次找的位置,之后找到的还要和这次找到的进行比较,这样就比较麻烦。
DFS适合搜索全部的解。因为要搜索全部的解,在记录路径的时候也会简单一点,而BFS搜索过程中,遇到离根最近的解,并没有什么用,也必须遍历完整棵搜索树。
举个典型例子,如下图,灰色代表墙壁,绿色代表起点,红色代表终点,规定每次只能走一步,且只能往下或右走。求一条绿色到红色的最短路径。
对于上面的问题,BFS 和 DFS 都可以求出结果,它们的区别就是在复杂度上存在差异。我可以先告诉你,该题 BFS 是较佳算法。
所以我们可以总结一下DFS和BFS的应用方向:
BFS 常用于找单一的最短路线,它的特点是 "搜到就是最优解",而 DFS 用于找所有解的问题,它的空间效率高,而且找到的不一定是最优解,必须记录并完成整个搜索,故一般情况下,深搜需要非常高效的剪枝(剪枝的概念请百度)。