“老师,我想学会如何走迷宫,你可以教我吗?”,“授人予鱼不如授人予鱼,我就教你两大神器吧”。就这样,不会DFS和BFS的我,终于可以认真学习一次这两大神器了。冲冲冲!
一、小思考
BFS和DFS的重要性不言而喻,想要在图的世界里面探探头,必须要将这两个算法掌握。在我们开始算法之前。如果给你一个简单的图,你会如何遍历它呢?
我们可能总是会按照生活中的规律来遍历,例如看到这个图,会从左往右,从上往下,或者把与结点相关的点都走完一遍,依次走完所有的点。这些都是不错的方法。但是当我们总是需要有规律或者有迹可循,这样在下次遇到的时候,我们总是可以拿出当前较好的办法(可能不是最优)。我想这是我们总结算法和进行题目训练的目的,那么接下来对于两个算法的学习。我总是会从算法的步骤--->算法描述来说,虽然这样和其它书本没有什么差别,但还是比较好掌握的。
由于我们的目的是掌握算法的精髓,所以后面的例子中,都采用邻接矩阵来存储图结构(这样不好,但是邻接矩阵较为简单,使我们的精力可以放在算法的精髓上)。
二、广度优先搜索(BFS)
【算法步骤】
(1)从图中某个顶点v出发,访问v。
(2)依次访问v的各个未曾访问过的邻接点。
(3)分别从这些邻接点出发依次访问它们的邻接点,并使“未被访问的顶点的邻接点”优先“后被访问的顶点的邻接点”被访问。重复步骤(3),直至图中所有已被访问的顶点的邻接点都被访问到。
(4)若是非连通图,此时图中尚有顶点未被访问,则另选图中的一个未被访问的顶点作为始点,重复上述步骤,直到所有顶点都被访问到为止。
Thinking:
假设我们现在有了一个图,要实现现在的步骤,现在我们可是已经掌握了很多工具了。首先我们肯定用数组来保存这个图,然后我们需要一个数组来记录结点是否被访问了(例如0,1)。为了保证“未被访问的顶点的邻接点”优先“后被访问的顶点的邻接点”被访问,我们需要一个“先进先出的工具——队列”。之后无非就是进行循环判断了,那么来看看我们行不行呢?
在写代码之前,一定一定要在纸上演算一遍这个过程,特别是队列里面的变化,这样才能写出代码!!!
【算法描述】
bool visited[MAX_VERTEX_NUM];//访问标记数组
void BFSTraverse(Graph G) {//对图G进行广度优先遍历
for(i=0;i<G.vexnum;++i)
visited[i]=FALSE;//访问标记数组初始化
InitQueue(Q);//初始化辅助队列Q
for(i=0;i<G.vexnum;++i)//从0号顶点开始遍历
if(!visited[i])//对每个连通分量调用一次BFS
BFS(G,i); //vi未访问过,从vi开始BFS
}
void BFS(Graph G,int v) {//从顶点v出发,广度优先遍历图G
visit(v);//访问初始顶点v
visited[v]=TRUE;//对v做已访问标记
Enqueue(Q,v);//对顶点v入队列Q
while(!isEmpty(Q)){
DeQueue(Q,v);//顶点v出队列
for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)) //检测v所有的邻接点
if(!visited[w]){//w为v的尚未访问的邻接顶点
visit(w);//访问顶点w
visited[w]=TRUE;//对w做已访问标记
EnQueue(Q,w);//顶点w入队列
}
}
}
三、深度优先搜索(DFS)
【算法步骤】
(1)从图中某个顶点v出发,访问v。
(2)找出刚访问过的顶点的第一个未被访问的邻接点,访问该顶点。以该顶点作为新顶点,重复此步骤,直至刚访问过的顶点没有被访问的邻接点为止。
(3)返回前一个访问过的且仍有未被访问的邻接点的顶点,找出改顶点的下一个未被访问的顶点,访问该顶点。
(4)重复步骤(2)和(3),直至图中所有顶点都被访问过,搜索结束。
Thinking
仔细查看此步骤,就会发现对每个顶点做的事情都是一样的,我们就思考是不是可以利用递归来实现。上一个BFS因为要使用队列,所以用递归时不可以的,这里可以用递归来实现。
【算法描述】
bool visited[MAX_VERTEX_NUM];//标记访问数组
void DFSTraverse(Graph G){//对图G进行深度优先遍历
for(v=0;v<G.vexnum;++v)
visited[v]=FALSE;//初始化被访问标记数组
for(v=0;v<G.vexnum;++v)//本代码中是从v=0开始遍历
if(!visited[v])
DFS(G,v);
}
void DFS(Graph G,int v){//从顶点v出发,深度优先遍历图G
visit(v);//访问顶点v
visited[v]=TRUE;//设已被访问标记
for(w=FirstNeighbor(G,v);w>=0;w=NexNeighbor(G,v,w))
if(!visited[w]) {//w为v的未被访问的邻接顶点
DFS(G,w);
}
}
小对比:
我们发现BFS和DFS在代码上的表现是相似的,为了加上对于非联通图的算法,所以DFSTraverse()和BFSTraverse()是一致的,只不过BFSTraverse多了一行对队列的初始化的语句。而之后BFS()中是利用循环和队列来实现遍历,我们的DFS就直接利用了递归。
四、连通性检测
这里没有具体的连通性检测的示例代码,因为我觉得如果掌握了下面遍历算法,连通性仅仅只是去从一个顶点出发判断是否能够遍历完所有的顶点而已(下面代码均来自严蔚敏老师数据结构一书)。让我们一起来把上面的算法描述变成代码吧~
(1)图的广度优先搜索遍历连通图实例代码:
//广度优先搜索遍历连通图
#include <iostream>
using namespace std;
#include<stdlib.h>
#define MVNum 100 //最大顶点数
#define MAXQSIZE 100 //最大队列长度
typedef char VerTexType; //假设顶点的数据类型为字符型
typedef int ArcType; //假设边的权值类型为整型
bool visited[MVNum]; //访问标志数组,其初值为"false"
//-----图的邻接矩阵存储表示-----
typedef struct{
VerTexType vexs[MVNum]; //顶点表
ArcType arcs[MVNum][MVNum]; //邻接矩阵
int vexnum,arcnum; //图的当前点数和边数
}Graph;
//----队列的定义及操作--------
typedef struct{
ArcType *base; //初始化的动态分配存储空间
int front; //头指针,若队列不空,指向队头元素
int rear; //尾指针,若队列不空,指向队尾元素的下一个位置
}sqQueue;
void InitQueue(sqQueue &Q){
//构造一个空队列Q
Q.base = new ArcType[MAXQSIZE];
if(!Q.base) exit(1); //存储分配失败
Q.front = Q.rear = 0;
}//InitQueue
void EnQueue(sqQueue &Q, ArcType e){
//插入元素e为Q的新的队尾元素
if((Q.rear + 1) % MAXQSIZE == Q.front)
return;
Q.base[Q.rear] = e;
Q.rear = (Q.rear + 1) % MAXQSIZE;
}//EnQueue
bool QueueEmpty(sqQueue Q){
//判断是否为空队
if(Q.rear == Q.front)
return true;
return false;
}//QueueEmpty
void DeQueue(sqQueue &Q, ArcType &u){
//队头元素出队并置为u
u = Q.base[Q.front];
Q.front = (Q.front + 1) % MAXQSIZE;
}//DeQueue
//--------------------------------------------------
int LocateVex(Graph G , VerTexType v){
//确定点v在G中的位置
for(int i = 0; i < G.vexnum; ++i)
if(G.vexs[i] == v)
return i;
return -1;
}//LocateVex
void CreateUDN(Graph &G){
//采用邻接矩阵表示法,创建无向网G
int i , j , k;
cout <<"请输入总顶点数,总边数,以空格隔开:";
cin >> G.vexnum >> G.arcnum; //输入总顶点数,总边数
cout << endl;
cout << "输入点的名称,如a" << endl;
for(i = 0; i < G.vexnum; ++i){
cout << "请输入第" << (i+1) << "个点的名称:";
cin >> G.vexs[i]; //依次输入点的信息
}
cout << endl;
for(i = 0; i < G.vexnum; ++i) //初始化邻接矩阵,边的权值均置为极大值MaxInt
for(j = 0; j < G.vexnum; ++j)
G.arcs[i][j] = 0;
cout << "输入边依附的顶点,如a b" << endl;
for(k = 0; k < G.arcnum;++k){ //构造邻接矩阵
VerTexType v1 , v2;
cout << "请输入第" << (k + 1) << "条边依附的顶点:";
cin >> v1 >> v2; //输入一条边依附的顶点
i = LocateVex(G, v1); j = LocateVex(G, v2); //确定v1和v2在G中的位置,即顶点数组的下标
G.arcs[i][j] = 1; //边<v1, v2>的权值置为w
G.arcs[j][i] = G.arcs[i][j]; //置<v1, v2>的对称边<v2, v1>的权值为w
}//for
}//CreateUDN
int FirstAdjVex(Graph G , int v){
//返回v的第一个邻接点
int i;
for(i = 0 ; i < G.vexnum ; ++i){
if(G.arcs[v][i] == 1 && visited[i] == false)
return i;
}
return -1;
}//FirstAdjVex
int NextAdjVex(Graph G , int u , int w){
//返回v相对于w的下一个邻接点
int i;
for(i = w ; i < G.vexnum ; ++i){
if(G.arcs[u][i] == 1 && visited[i] == false)
return i;
}
return -1;
}//NextAdjVex
void BFS (Graph G, int v){
//按广度优先非递归遍历连通图G
sqQueue Q;
ArcType u;
ArcType w;
cout << G.vexs[v] << " "; visited[v] = true; //访问第v个顶点,并置访问标志数组相应分量值为true
InitQueue(Q); //辅助队列Q初始化,置空
EnQueue(Q, v); //v进队
while(!QueueEmpty(Q)){ //队列非空
DeQueue(Q, u); //队头元素出队并置为u
for(w = FirstAdjVex(G, u); w >= 0; w = NextAdjVex(G, u, w)){
//依次检查u的所有邻接点w ,FirstAdjVex(G, u)表示u的第一个邻接点
//NextAdjVex(G, u, w)表示u相对于w的下一个邻接点,w≥0表示存在邻接点
if(!visited[w]){ //w为u的尚未访问的邻接顶点
cout << G.vexs[w] << " "; visited[w] = true; //访问w,并置访问标志数组相应分量值为true
EnQueue(Q, w); //w进队
}//if
}//for
}//while
}//BFS
int main(){
cout << "************广度优先搜索遍历连通图**************" << endl << endl;
Graph G;
CreateUDN(G);
cout << endl;
cout << "无向连通图G创建完成!" << endl << endl;
cout << "请输入遍历连通图的起始点:";
VerTexType c;
cin >> c;
int i;
for(i = 0 ; i < G.vexnum ; ++i){
if(c == G.vexs[i])
break;
}
cout << endl;
while(i >= G.vexnum){
cout << "该点不存在,请重新输入!" << endl;
cout << "请输入遍历连通图的起始点:";
cin >> c;
for(i = 0 ; i < G.vexnum ; ++i){
if(c == G.vexs[i])
break;
}
}
cout << "深度优先搜索遍历连通图结果:" << endl;
BFS(G , i);
cout <<endl;
return 0;
}//main
(2)图的深度优先搜索遍历连通图实例代码:
//采用邻接矩阵表示图的深度优先搜索遍历
#include <iostream>
using namespace std;
#define MVNum 100 //最大顶点数
typedef char VerTexType; //假设顶点的数据类型为字符型
typedef int ArcType; //假设边的权值类型为整型
//------------图的邻接矩阵------------------
typedef struct{
VerTexType vexs[MVNum]; //顶点表
ArcType arcs[MVNum][MVNum]; //邻接矩阵
int vexnum,arcnum; //图的当前点数和边数
}Graph;
bool visited[MVNum]; //访问标志数组,其初值为"false"
int FirstAdjVex(Graph G , int v); //返回v的第一个邻接点
int NextAdjVex(Graph G , int v , int w); //返回v相对于w的下一个邻接点
int LocateVex(Graph G , VerTexType v){
//确定点v在G中的位置
for(int i = 0; i < G.vexnum; ++i)
if(G.vexs[i] == v)
return i;
return -1;
}//LocateVex
void CreateUDN(Graph &G){
//采用邻接矩阵表示法,创建无向网G
int i , j , k;
cout <<"请输入总顶点数,总边数,以空格隔开:";
cin >> G.vexnum >> G.arcnum; //输入总顶点数,总边数
cout << endl;
cout << "输入点的名称,如a" <<endl;
for(i = 0; i < G.vexnum; ++i){
cout << "请输入第" << (i+1) << "个点的名称:";
cin >> G.vexs[i]; //依次输入点的信息
}
cout << endl;
for(i = 0; i < G.vexnum; ++i) //初始化邻接矩阵,边的权值均置为极大值MaxInt
for(j = 0; j < G.vexnum; ++j)
G.arcs[i][j] = 0;
cout << "输入边依附的顶点,如a b" << endl;
for(k = 0; k < G.arcnum;++k){ //构造邻接矩阵
VerTexType v1 , v2;
cout << "请输入第" << (k + 1) << "条边依附的顶点:";
cin >> v1 >> v2; //输入一条边依附的顶点及权值
i = LocateVex(G, v1); j = LocateVex(G, v2); //确定v1和v2在G中的位置,即顶点数组的下标
G.arcs[j][i] = G.arcs[i][j] = 1; //置<v1, v2>的对称边<v2, v1>的权值为w
}//for
}//CreateUDN
void DFS(Graph G, int v){
//图G为邻接矩阵类型
int w;
cout << G.vexs[v] << " "; visited[v] = true; //访问第v个顶点,并置访问标志数组相应分量值为true
for(w = 0; w < G.vexnum; w++) //依次检查邻接矩阵v所在的行
if((G.arcs[v][w] != 0)&& (!visited[w])) DFS(G, w); //G.arcs[v][w]!=0表示w是v的邻接点,如果w未访问,则递归调用DFS
}//DFS
int FirstAdjVex(Graph G , int v){
//返回v的第一个邻接点
int i;
for(i = 0 ; i < G.vexnum ; ++i){
if(G.arcs[v][i] == 1 && visited[i] == false)
return i;
}
return -1;
}//FirstAdjVex
int NextAdjVex(Graph G , int v , int w){
//返回v相对于w的下一个邻接点
int i;
for(i = w ; i < G.vexnum ; ++i){
if(G.arcs[v][i] == 1 && visited[i] == false)
return i;
}
return -1;
}//NextAdjVex
int main(){
cout << "************采用邻接矩阵表示图的深度优先搜索遍历**************" << endl << endl;
Graph G;
CreateUDN(G);
cout << endl;
cout << "无向图G创建完成!" << endl << endl;
cout << "请输入遍历无向图G的起始点:";
VerTexType c;
cin >> c;
int i;
for(i = 0 ; i < G.vexnum ; ++i){
if(c == G.vexs[i])
break;
}
cout << endl;
while(i >= G.vexnum){
cout << "该点不存在,请重新输入!" << endl;
cout << "请输入遍历连通图的起始点:";
cin >> c;
for(i = 0 ; i < G.vexnum ; ++i){
if(c == G.vexs[i])
break;
}
}
cout << "深度优先搜索遍历无向图G结果:" << endl;
DFS(G , i);
cout <<endl;
return 0;
}//main
五、总结与展望
在不少算法题中总是遇到关于图的连通性判断呀,图中的最短路径的计算呀,这两个是最基础的部分。一定要认真掌握,我自己写一遍也是让自己重视起来,一起加油吧~