大话数据结构---(六)图的深度优先遍历和广度优先遍历

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.总结

对比图的深度优先遍历与广度优先遍历算法,你会发现,它们在时间复杂度上是一样的,不同之处仅仅在于对顶点访问的顺序不同。如果说图的深度优先遍历类似树的前序遍历,那么图的广度优先遍历就类似于树的层序遍历了。可见两者在全图遍历上是没有优劣之分的,只是视不同的情况选择不同的算法。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赶路的苟狗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值