算法是基于特定数据结构之上的,深度优先搜索算法和广度优先搜索算法都是基于“图”这种数据结构的。
树是图的一种特例(连通无环的图就是树)。
图上的搜索算法,最直接的理解就是,在图中找出从一个顶点出发,到另一个顶点的路径。具体方法有很多,两种最简单、最“暴力”的深度优先、广度优先搜索,还有 A*
、IDA*
等启发式搜索算法。深度优先搜索算法和广度优先搜索算法,既可以用在无向图,也可以用在有向图上。
图(采用邻接表存储)的 C++
代码实现如下:
#include
// 无向图结构的定义
class Graph{
private:
int v; // 顶点个数
list<int> adj() // 存储的邻接表
public:
// 构造函数定义
Graph(int v){
adj = new List<int>(v);
for (int i = 0; i < v; i++){
adj[i] = new list<int> ();
}
}
// 无向图一条边存储 2 次
void addEdge(int s, int t){
adj[s].push_back(t);
adj[t].push_back(s);
}
}
广度优先搜索(Breadth-First-Search),我们平常都简称 BFS
。直观地讲,它其实就是一种“地毯式”层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索。
求图中起始顶点 s
到终止顶点 t
的最短路径,可使用 bfs
算法,代码如下:
#include
using namespace std;
void bfs(int s, int t){
if( s==t ) return;
bool visited[v]; // 用来记录已经访问过的顶点, v 表示顶点个数
queue<int> queue; // 一个队列,用来存储已经被访问、但相连的顶点还没有被访问的顶点。
queue.push_back(s); // 添加起始顶点
vector<int> prev(v, -1); // prev 用来记录搜索路径, prev[w]存储的是,顶点 w 是从哪个前驱顶点遍历过来的。
while(queue.size() != 0){
int w = queue.pop();
for(int i= 0; i<adj[w].size();++i){
int q = adj[w].at(i);
if(!visited[q]){
prev[q] = w;
if(q==t){
print_map(prev, s, t);
return;
}
visited[q] = true;
queue.add(q);
}
}
}
void print_map(int prev[], int s, int t){
if(prev[t] != -1 && t!=s){
print_map(prev, s, prev[t]);
}
cout << t << "+ ";
}
queue 是一个队列,用来存储已经被访问、但相连的顶点还没有被访问的顶点。因为广度优先搜索是逐层访问的,也就是说,我们只有把第 k 层的顶点都访问完成之后,才能访问第 k+1 层的顶点。当我们访问到第 k 层的顶点的时候,我们需要把第 k 层的顶点记录下来,稍后才能通过第 k 层的顶点来找第 k+1 层的顶点。所以,我们用这个队列来实现记录的功能。
prev 用来记录搜索路径。当我们从顶点 s 开始,广度优先搜索到顶点 t 后,prev 数组中存储的就是搜索的路径。不过,这个路径是反向存储的。prev[w]存储的是,顶点 w 是从哪个前驱顶点遍历过来的。比如,我们通过顶点 2 的邻接表访问到顶点 3,那 prev[3]就等于 2。为了正向打印出路径,我们需要递归地来打印。
最坏情况下,终止顶点 t
离起始顶点 s
很远,需要遍历完整个图才能找到。这个时候,每个顶点都要进出一遍队列,每个边也都会被访问一次,所以,广度优先搜索的时间复杂度是 O ( V + E ) O(V+E) O(V+E),其中,V
表示顶点的个数,E
表示边的个数。当然,对于一个连通图来说,也就是说一个图中的所有顶点都是连通的,E
肯定要大于等于 V-1
,所以,广度优先搜索的时间复杂度也可以简写为 O ( E ) O(E) O(E)。
广度优先搜索的空间消耗主要在几个辅助变量 visited
数组、queue
队列、prev
数组上。这三个存储空间的大小都不会超过顶点的个数,所以空间复杂度是 O ( V ) O(V) O(V)。
深度优先搜索(Depth-First-Search),简称 DFS
。
最直观的例子就是“走迷宫”。假设你站在迷宫的某个岔路口,然后想找到出口。你随意选择一个岔路口来走,走着走着发现走不通的时候,你就回退到上一个岔路口,重新选择一条路继续走,直到最终找到出口。这种走法就是一种深度优先搜索策略。
bool found = false; // 类成员变量
void dfs(int s, int t){
found = false;
bool visited[v]; // v 表示顶点个数
int prev[v];
for(int i=0; i< v;i++){
prev[i] = -1;
}
recurDFS(s, t, visited, prev);
print_map(prev, s, t)
}
void recurDFS(int w, int t, bool visited[], int prev[]){
if(found == true) return;
visited[w] = true;
if(w == t){
found = true;
return;
}
for(int i = 0; i < adj[w].size();++i){
int q = adj[w].at(i);
if(!visited[q]){
prev[q] = w;
recurDFS(q, t, visited, prev);
}
}
}
广度优先搜索,通俗的理解就是,地毯式层层推进,从起始顶点开始,依次往外遍历。广度优先搜索需要借助队列来实现,遍历得到的路径就是,起始顶点到终止顶点的最短路径。深度优先搜索用的是回溯思想,非常适合用递归实现。换种说法,深度优先搜索是借助栈来实现的。在执行效率方面,深度优先和广度优先搜索的时间复杂度都是 O ( E ) O(E) O(E),空间复杂度是 O ( V ) O(V) O(V)。
《数据结构与算法之美》-深度和广度优先搜索