1.深度优先遍历
深度优先遍历(Depth_First_Search),也有称为深度优先搜索,简称DFS。话不多说,直接上个实例,它的原理也就大致清楚了。
我们对图1进行深度优先遍历。 深度优先遍历的过程如下:
这里先和大家回顾一下遍历的概念,在不重复经过某点的情况下,即每个点只能经过一次,依次走完所有点。就好比,你回老家给亲戚们拜年,他们住址都相隔不远,这时你规划一条路线,给你所有亲戚拜个年。好,回归正题,图2看不懂没关系,一共是八个步骤,每个步骤我都会详细和大家解释清楚,
- 第一步:我们假设从A点开始遍历,当然你也可以是图中的任意点,这时从A点去往下一个点的方向有两个,B点,F点,这里我们前往B点(也可以前往F点)。
- 第二步:到达B点后,这时从B点去往下一个点的方向有四个,A点、C点、I点、G点。因为A点已经遍历过,所以排除A点,还剩下C点、I点、G点,这里我们前往C点(也可以前往I点或者G点)。
- 第三步:到达C点后,这时从C点去往下一个点的方向有三个,B点,I点,D点。因为B点已经遍历过,所以排除B点,还剩I点、D点,这里我们前往D点(也可以前往I点)。
- 第四步:到达D点后,这时从D点去往下一个点的方向有五个,C点,I点,G点,H点,E点。因为C点已经遍历过,所以排除C点,还剩I点,G点,H点,E点,这里我们前往E点(也可以前往H点、G点或者I点)。
- 第五步:到达E点后,这时从E点去往下一个点的方向有三个,D点,H点,F点。因为D点已经遍历过,所以排除D点,还剩H点、F点,这里我们前往F点(也可以前往H点)。
- 第六步:到达F点后,这时从F点去往下一个点的方向有三个,A点、G点、E点。因为A点,E点已经遍历过,所以排除A点、E点,这里我们只能前往G点。
- 第七步:到达G点,这时从G点去往下一个点的方向有四个,B点、F点、D点、H点。因为B点、F点、D点已经遍历过,所以排除B点、F点、D点。这里我们只能前往H点。
- 第八步:遍历结束。
如果理解了深度优先遍历的过程,不难发现,它其实就是个递归过程。它从图中某个顶点V出发,访问此顶点,然后从V的未被访问的邻接点出发深度优先遍历图,直至图中所有和V有路径相同的顶点都被访问到。事实上,我们这里讲到的是连通图,对于非连通图,只需要对他的连通分量分别进行深度优先遍历,即在先前一个顶点进行一次深度优先遍历后,若图中尚有顶点未被访问,则选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
如果我们用的是邻接矩阵的方式,则代码如下:
typedef int Boolean;//Boolean是布尔类型,其值是TRUE或FALSE
Boolean visited[MAX];//访问标志的数组
//邻接矩阵的深度优先地柜算法
void DFS(MGraph G,int i)
{
int j;
visited[i]=TRUE;
cout<<G.vexs[i];//打印顶点,也可以其他操作
for(j=0;j<G.numVertexes;j++)
{
if(G.arc[i][j]==1&&!visited[j])
DFS(G,j);
}
}
//邻接矩阵的深度遍历操作
void DFSTraverse(MGraph G)
{
int i;
for(i=0;i<G.numVertexes;i++)
visited[i]=FALSE;//初始化所有顶点状态都是未访问过状态
for(i=0;i<G.numVertexes;i++)
if(!visited[i])//对未访问过的顶点调用DFS,若是连通图,只会执行一次
DFS(G,I);
}
如果图结构是邻接表结构,其DFSTraverse函数的代码是几乎相同的,只是在递归函数中因为将数组换成了链表而有所不同,代码如下。
typedef int Boolean;//Boolean是布尔类型,其值是TRUE或FALSE
Boolean visited[100];
//邻接表的深度优先递归算法
void DFS(GraphAdjList GL, int i) {
EdgeNode *p;
visited[i] = 1;
cout << GL.adjList[i].data << " ";//打印顶点,也可以其他操作
p = GL.adjList[i].firstedge;
while (p) {
if (!visited[p->adjvex])
DFS(GL, p->adjvex);//对未访问的邻接顶点递归调用
p = p->next;
}
}
//邻接表的深度遍历操作
void DFSTraverse(GraphAdjList GL) {
int i;
for (i = 0; i < GL.numVertexes; i++) {
visited[i] = 0;//初始所有顶点状态都是未访问过状态
}
for (i = 0; i < GL.numVertexes; i++) {
if (!visited[i])//对未访问过的顶点调用DFS,若是连通图,只会执行一次
DFS(GL, i);
}
}
对于n个顶点e条边的图来说,邻接矩阵由于是二维数组,要查找每个顶点需要访问矩阵中的所有元素,因此都需要O(n2)的时间。而邻接表做存储结构是,找邻接点所需的时间取决于顶点和边的数量,所以是O(n+e)。显然对于点多变少的稀疏图来说,邻接表结构使得算法在时间效率上大大提高。这里给出用邻接表存储图中数据,并遍历输出的实例,完整代码如下。
#include<iostream>
using namespace std;
#define MAXVEX 100
typedef char VertextType;//顶点类型应由用户定义
typedef int EdgeType;//边上的权值类型应由用户定义
typedef struct EdgeNode {//边表结点
int adjvex;//邻接点域,存储该顶点对应的下标
EdgeType weight;//用于存储权值,对于非网图可以不需要
struct EdgeNode *next;//链域,指向下一个邻接点
}EdgeNode;
typedef struct VertextNode {//顶点表结点
VertextType data;//顶点域,存储顶点信息
EdgeNode *firstedge;//边表头指针
}VertexNode,AdjList[MAXVEX];
typedef struct {
AdjList adjList;
int numVertexes, numEdges;//图中当前顶点数和边数
}GraphAdjList;
//建立图的邻接表结构
void CreateALGraph(GraphAdjList *G) {
int i, j, k;
EdgeNode *e;
cout << "输入顶点数和边数:" << endl;
cin >> G->numVertexes >> G->numEdges;//输入顶点数和边数
cout << "输入顶点信息" << endl;
for (i = 0; i < G->numVertexes; i++) {
cin >> G->adjList[i].data;//输入顶点信息
G->adjList[i].firstedge = NULL;//将边表置为空表
}
for (k = 0; k < G->numEdges; k++) {//建立边表
cout << "输入边(vi,vj)上的顶点序号:" << endl;
cin >> i >> j;//输入边(vi,vj)上的顶点序号
e = (EdgeNode *)malloc(sizeof(EdgeNode));//向内存申请空间,生成边表结点
e->adjvex = j;//邻接序号为j
e->next = G->adjList[i].firstedge;//将e指针指向当前顶点指向的结点
G->adjList[i].firstedge = e;//将当前顶点的指针指向e--------头插法
e = (EdgeNode *)malloc(sizeof(EdgeNode));//向内存申请空间,生成边表结点
e->adjvex = i;//邻接序号为i
e->next = G->adjList[j].firstedge;//将e指针指向当前顶点指向的结点
G->adjList[j].firstedge = e;//将当前顶点的指针指向e
}
}
typedef int Boolean;//Boolean是布尔类型,其值是TRUE或FALSE
Boolean visited[100];
//邻接表的深度优先递归算法
void DFS(GraphAdjList GL, int i) {
EdgeNode *p;
visited[i] = 1;
cout << GL.adjList[i].data << " ";//打印顶点,也可以其他操作
p = GL.adjList[i].firstedge;
while (p) {
if (!visited[p->adjvex])
DFS(GL, p->adjvex);//对未访问的邻接顶点递归调用
p = p->next;
}
}
//邻接表的深度遍历操作
void DFSTraverse(GraphAdjList GL) {
int i;
for (i = 0; i < GL.numVertexes; i++) {
visited[i] = 0;//初始所有顶点状态都是未访问过状态
}
for (i = 0; i < GL.numVertexes; i++) {
if (!visited[i])//对未访问过的顶点调用DFS,若是连通图,只会执行一次
DFS(GL, i);
}
}
int main() {
GraphAdjList *GL = (GraphAdjList*)malloc(sizeof(GraphAdjList));
CreateALGraph(GL);
DFSTraverse(*GL);
}
运行实例如下:
2.广度优先遍历
广度优先遍历(Breadth_First_Search),又称为广度优先搜索,简称BFS。我们也是先通过一个实例,演示过程来加深理解。
广度优先遍历的过程如下:
同样,这里会和大家解释广度优先遍历的每一步过程。
- 第一步:假设从A点开始遍历,当然也可以从图中任意一点开始遍历,A点入队列。
- 第二步:A点出队列,将与A点相邻的点(B点、F点)入队列,入队列的顺序无关紧要。
- 第三步:B点出队列,将与B点相邻的点(C点、I点、G点)入队列,A点虽然是B的相邻点,但A点已经遍历过,所以不再入队列,下列叙述同理。
- 第四步:F点出队列,将与F点相邻的点(G点、E点)入队列。
- 第五步:C点出队列,将与C点相邻的点(D点)入队列。
- 第六步:I点出队列,因为与I点相邻的点(B点、C点、D点)都已入过队列,所以这里无顶点入队列。
- 第七步:G点出队列,将与G点相邻的H点入队列。
- 第八步:E点出队列,无顶点入队列。
- 第九步:D点出队列,无顶点入队列。
- 第十步:H点出队列,遍历结束。
有了这个讲解,我们来看代码就非常容易了,以下是邻接矩阵结构的广度优先遍历算法。
void BFSTraverse(MGraph G)
{
int i,j;
Queue Q;
for(i=0;i<G.numVertexes;i++)
visited[i]=0;
InitQueue(&Q);//初始化一辅助用的队列
for(i=0;i<G.numVertexes;i++)//对每一个顶点做循环,若是连通图,只会进行一次
{
if(!visited[i])
{
visited[i]=1;
cout<<G.vexs[i];//打印顶点,也可以其他操作
EnQueue(&Q,i);
while(!QueueEmpty(Q))//若当前队列不为空
{
DeQueue(&Q,i);//将队中元素出队列,赋值给i
for(j=0;j<G.numVertexes;j++)
{
//判断其他顶点若与当前顶点存在边且未访问过
if(G.arc[i][j]==1&&!visited[j])
{
visited[j]=1;//将找到的此顶点标记为已访问
cout<<G.vexs[j];//打印顶点
EnQueue(&Q,j);//将找到的此顶点入队列
}
}
}
}
}
}
对于邻接表的广度优先遍历,代码与邻接矩阵差异不大,下面给出完整的代码示例。
#include<iostream>
#include<queue>
using namespace std;
#define MAXVEX 100
typedef char VertextType;//顶点类型应由用户定义
typedef int EdgeType;//边上的权值类型应由用户定义
typedef struct EdgeNode {//边表结点
int adjvex;//邻接点域,存储该顶点对应的下标
EdgeType weight;//用于存储权值,对于非网图可以不需要
struct EdgeNode *next;//链域,指向下一个邻接点
}EdgeNode;
typedef struct VertextNode {//顶点表结点
VertextType data;//顶点域,存储顶点信息
EdgeNode *firstedge;//边表头指针
}VertexNode,AdjList[MAXVEX];
typedef struct {
AdjList adjList;
int numVertexes, numEdges;//图中当前顶点数和边数
}GraphAdjList;
//建立图的邻接表结构
void CreateALGraph(GraphAdjList *G) {
int i, j, k;
EdgeNode *e;
cout << "输入顶点数和边数:" << endl;
cin >> G->numVertexes >> G->numEdges;//输入顶点数和边数
cout << "输入顶点信息" << endl;
for (i = 0; i < G->numVertexes; i++) {
cin >> G->adjList[i].data;//输入顶点信息
G->adjList[i].firstedge = NULL;//将边表置为空表
}
for (k = 0; k < G->numEdges; k++) {//建立边表
cout << "输入边(vi,vj)上的顶点序号:" << endl;
cin >> i >> j;//输入边(vi,vj)上的顶点序号
e = (EdgeNode *)malloc(sizeof(EdgeNode));//向内存申请空间,生成边表结点
e->adjvex = j;//邻接序号为j
e->next = G->adjList[i].firstedge;//将e指针指向当前顶点指向的结点
G->adjList[i].firstedge = e;//将当前顶点的指针指向e--------头插法
e = (EdgeNode *)malloc(sizeof(EdgeNode));//向内存申请空间,生成边表结点
e->adjvex = i;//邻接序号为i
e->next = G->adjList[j].firstedge;//将e指针指向当前顶点指向的结点
G->adjList[j].firstedge = e;//将当前顶点的指针指向e
}
}
typedef int Boolean;//Boolean是布尔类型,其值是TRUE或FALSE
Boolean visited[100];
//邻接表的广度优先递归算法
void BFSTraverse(GraphAdjList GL) {
int i;
EdgeNode *p;
queue<int> Q;
for (i = 0; i < GL.numVertexes; i++)
visited[i] = 0;
for (i = 0; i < GL.numVertexes; i++) {
if (!visited[i]) {
visited[i] = 1;
cout << GL.adjList[i].data<<" ";//打印顶点,也可以是其他操作
Q.push(i);
while (!Q.empty()) {
i = Q.front();
Q.pop();
p = GL.adjList[i].firstedge;//找到当前顶点边表链表头指针
while (p) {
if (!visited[p->adjvex])//若此顶点未被访问
{
visited[p->adjvex] = 1;
cout << GL.adjList[p->adjvex].data<<" ";
Q.push(p->adjvex);//将此顶点入队列
}
p = p->next;//指针指向下一个邻接点
}
}
}
}
}
int main() {
GraphAdjList *GL = (GraphAdjList*)malloc(sizeof(GraphAdjList));
CreateALGraph(GL);
BFSTraverse(*GL);
}
运行实例如下:
3.总结
对比图的深度优先遍历与广度优先遍历算法,你会发现,它们在时间复杂度上是一样的,不同之处仅仅在于对顶点访问的顺序不同。如果说图的深度优先遍历类似树的前序遍历,那么图的广度优先遍历就类似于树的层序遍历了。可见两者在全图遍历上是没有优劣之分的,只是视不同的情况选择不同的算法。