在# 10--图和图的存储中已经讲解过了图的基本知识和图的存储:邻接矩阵和邻接表。总结图的特征如下:
- 1.图是由
有限
个顶点
和顶点之间连接的边
组成的一种逻辑结构;- 2.图是
无序的
,即顶点和顶点之间的边不存在
顺序关系;- 3.图即可以使用
邻接矩阵
实现顺序存储,也可以使用邻接表
实现链式存储;
既然图的是无序的
,那么应该如何遍历图,使得所有的顶点和它们的边都被访问到呢?
一、图的深度优先遍历
下面我们来讲解一下深度优先遍历的思路:
1.第一步
首先需要知悉,顺序存储的内存是连续,链式存储的链表也是有指向的,总有先后关系,但是图的特性确定图是无序的
。我们从A开始遍历图,下面按步骤分析深度遍历的思路:
- 1.当访问到
A
时,标记A已经被访问过了。此时与A有连接的顶点是B和C,判断B(C)是否被访问过,如果没有,则访问B(C)
;- 2.当访问到
B
时,标记B已经被访问过了。此时与B有连接的顶点是C、I、G。判断C(I、G)是否被访问过,如果没有,则访问C(I、G)
;- 3.当访问到
C
时,标记C已经被访问过了。此时与C有连接的顶点是D和I,判断D(I)是否被访问过,如果没有,则访问D(I)
;- 4.当访问到
D
时,标记D已经被访问过了。此时与D有连接的顶点是E、H、G、I,判断E(H、G、I)是否被访问过,如果没有,则访问E(H、G、I)
;- 5.当访问到
E
时,标记E已经被访问过了。此时与E有连接的顶点是F和H,判断F(H)是否被访问过,如果没有,则访问F(H)
;- 6.当访问到
F
时,标记F已经被访问过了。此时与F有连接的顶点是A和G,由于A
已经被访问过
了,所以访问G
;- 7.当访问到
G
时,标记G已经被访问过了。此时与G有连接的顶点是B、D、H,由于B和D
已经被访问过
了,所以访问H
;- 8.当访问到
H
时,标记H已经被访问过了。此时与H有连接的顶点是D和E,由于D和H都被访问过了,没有可以访问的顶点了,退出循环
。
通过上面的8个步骤,我们访问到了图中的顶点A B C D E F G H,但是还有一个顶点I
没有被访问到
,怎么办呢?
2.第二步
为了
解决
顶点I
(或是图中的其它没有被访问到的顶点)没有
被访问到的问题,我们可以通过上图中红色箭头
的方式进行回溯
,也就是从最后一个
被访问过的顶点开始
,依次向上回退
,详细步骤如下:
- 1.与
H
有连接的顶点D和E已经被访问过了,回退
上一个访问过的顶点G
;- 2.遍历与
G
有连接的所有顶点B、D、H,发现都被访问过了,回退
到F
;- 3.遍历与
F
有连接的所有顶点F和G,发现都被访问过了,回退
到E
;- 4.遍历与
E
有连接的所有顶点F和H,发现都被访问过了,回退
到D
;- 5.遍历与
D
有连接的所有顶点E、H、G、I,发现I
没有被访问过了,于是访问I
,并将I标记
已经被访问过
了;- 6.遍历与
I
有连接的所有顶点B、C、D,发现都被访问过了,回退
到D
;- 7.遍历与
D
有连接的所有顶点E、H、G、I,发现都被访问过了,回退
到C
;- 8.遍历与
C
有连接的所有顶点D和I,发现都被访问过了,回退
到B
;- 9.遍历与
B
有连接的所有顶点C、G、I,发现都被访问过了,回退
到A
;- 10.遍历与
A
有连接的所有顶点B和F,发现都被访问过了,此时已经回退到了第一个
被访问的顶点,结束回溯
,遍历完成。
说明:
- 1.在
回溯
过程如果遇到有多个
未访问到的顶点,访问的逻辑是:依次访问并回溯
,直到全部都被访问后继续向上回溯
;- 2.在回溯过程中,如果有未被访问的顶点,访问该顶点后,发现它下面
还有未被访问
到的顶点,访问逻辑
和1
的逻辑一样;- 3.创建一个
visited数组
来标记
顶点是否被访问过
;- 4.每一次
遍历
到一个顶点或回溯
到一个顶点的时候都需要
遍历与之有连接
的所有顶点,判断其中是否有未遍历
过的顶点;- 5.
回溯
的逻辑我们可以使用递归的方式实现,递归
调用栈的入栈出栈
的逻辑刚好满足回溯
的逻辑。
对于第一步
和第二步
的逻辑,我们分别从邻接矩阵
和邻接表
两种存储方式来进行实现
二、邻接矩阵的深度优先遍历
如上图,通过前面的分析,邻接矩阵的深度优先遍历结果是
A B C D E F G H I
。
1.实现思路
- 1.将图的顶点和边的信息按
邻接矩阵
的逻辑顺序存储
到内存中;- 2.创建一个
visited
数组,用来标记
顶点是否被访问过
;- 3.初始化visited数组,所有值为
false
;- 4.选择顶点开始遍历(注意
非连通图
的情况);- 5.进入
递归
:打印顶点信息,并标记已经被访问过了;- 6.循环遍历边表,判断顶点间的
边
信息arc[i][j]是否为1
(顶点之间是否有连接
关系),并判断顶点是否被访问过
,如果没有,则继续。
2.代码实现
(1)实现邻接矩阵的顺序存储
1.状态值定义和数据类型定义
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status; //Status是函数的类型,其值是函数结果状态代码,如OK等
typedef int Boolean; //Boolean是布尔类型,其值是TRUE或FALSE
typedef char VertexType; //顶点数据类型
typedef int EdgeType; //边上的权值类型
#define MAXSIZE 9 //存储空间初始分配量
#define MAXEDGE 15
#define MAXVEX 9
#define INFINITYC 65535 //无穷大值,用来记录没有权重的情况
2.数据结构定义
typedef struct
{
VertexType vexs[MAXVEX]; // 顶点表
EdgeType arc[MAXVEX][MAXVEX];// 邻接矩阵,可看作边表
int numVertexes, numEdges; //图中的顶点数和边数
}MGraph;
3.实现图的顺序存储
void CreateMGraph(MGraph *G)
{
int i, j;
//1.确定图的顶点数以及边数
G->numVertexes=9;
G->numEdges=15;
//2.写入顶点信息,建立顶点表
G->vexs[0]='A';
G->vexs[1]='B';
G->vexs[2]='C';
G->vexs[3]='D';
G->vexs[4]='E';
G->vexs[5]='F';
G->vexs[6]='G';
G->vexs[7]='H';
G->vexs[8]='I';
//3.初始化图中的边表
for (i = 0; i < G->numVertexes; i++)
{
for ( j = 0; j < G->numVertexes; j++)
{
G->arc[i][j]=0;
}
}
//4.将图中的顶点之间的连接信息写入到边表中
G->arc[0][1]=1;
G->arc[0][5]=1;
G->arc[1][2]=1;
G->arc[1][8]=1;
G->arc[1][6]=1;
G->arc[2][3]=1;
G->arc[2][8]=1;
G->arc[3][4]=1;
G->arc[3][7]=1;
G->arc[3][6]=1;
G->arc[3][8]=1;
G->arc[4][5]=1;
G->arc[4][7]=1;
G->arc[5][6]=1;
G->arc[6][7]=1;
//5.无向图是对称矩阵,构成对称
for(i = 0; i < G->numVertexes; i++)
{
for(j = i; j < G->numVertexes; j++)
{
G->arc[j][i] =G->arc[i][j];
}
}
}
(2)实现深度优先遍历
1.递归遍历顶点信息
Boolean visited[MAXVEX]; //标记数组
void DFS(MGraph G,int i){
//1.标记顶点已经被访问过了
visited[i] = TRUE;
//2.输出顶点数据
printf("%c",G.vexs[i]);
//3.遍历顶点的边表信息
for(int j = 0; j < G.numVertexes;j++){
if(G.arc[i][j] == 1 && !visited[j]){
//如果有与之连接的顶点没被访问过,递归(递归回调时即实现了回溯的逻辑)
DFS(G, j);
}
}
}
2.遍历图
void DFSTravese(MGraph G){
//1.初始化标记数组
for(int i=0;i<G.numVertexes;i++){
visited[i] = FALSE;
}
//2.循环处理非连通图的情况
for(int i = 0;i<G.numVertexes;i++){
//从局部连通图的某一个顶点开始
if(!visited[i]){
//如果顶点未被访问过,递归遍历顶点信息
DFS(G, i);
}
}
}
(3)调试代码
printf("邻接矩阵的深度优先遍历!\n");
MGraph G;
CreateMGraph(&G);
DFSTravese(G);
printf("\n");
三、邻接表的深度优先遍历
按照邻接表的链式实现的结构如上图所示,1~16步画出了整个邻接表的深度优先遍历的逻辑,最终的遍历结果是:A F G H E D I C B
。可见在相同的遍历方式下,邻接矩阵和邻接表的遍历结果可能是不一样
。原因很简单--图是无序的
。
1.实现思路
与邻接矩阵
的实现思路一样
,不过代码上有些许差异,因为邻接表无法直接实现存储,需要借助邻接矩阵来实现,详情请看代码实现
。
2.代码实现
(1)实现邻接表的链式存储
借助“实现邻接矩阵的顺序存储”
的实现,我们来实现邻接表
的链式存储
1.结点和数据结构定义
//1.边表信息结点
typedef struct EdgeNode
{
int adjvex; //邻接点域,存储该顶点对应的下标
int weight; //用于存储权值,对于非网图可以不需要
struct EdgeNode *next; //指针域,指向下一个与顶点有连接的顶点的边信息
}EdgeNode;
//2.邻接表中的元素结点的数据结构
typedef struct VertexNode
{
int in; //顶点入度
char data; //顶点域,存储顶点信息
EdgeNode *firstedge;//边表头指针
}VertexNode, AdjList[MAXVEX];
//3.邻接表的数据结构
typedef struct
{
AdjList adjList;//邻接表
int numVertexes,numEdges; //图的顶点数和边数
}graphAdjList,*GraphAdjList;
2.利用邻接矩阵构建邻接表
void CreateALGraph(MGraph G,GraphAdjList *GL){
//1.创建邻接表,并且设计邻接表的顶点数以及弧数
*GL = (GraphAdjList)malloc(sizeof(graphAdjList));
(*GL)->numVertexes = G.numVertexes;
(*GL)->numEdges = G.numEdges;
//2.从邻接矩阵中将顶点信息输入
for (int i = 0; i < G.numVertexes; i++) {
//顶点入度为0
(*GL)->adjList[i].in = 0;
//顶点信息
(*GL)->adjList[i].data = G.vexs[i];
//顶点边表置空
(*GL)->adjList[i].firstedge = NULL;
}
//3.建立边表
EdgeNode *e;
for (int i = 0; i < G.numVertexes; i++) {
for (int j = 0; j < G.numVertexes; j++) {
if (G.arc[i][j] == 1) {
//1.创建顶点i与j的边表信息结点
e = (EdgeNode *)malloc(sizeof(EdgeNode));
//记录顶点i与j的边信息,j是顶点的下标值
e->adjvex = j;
//将顶点i与j的边信息接入邻接表中
e->next = (*GL)->adjList[i].firstedge;
(*GL)->adjList[i].firstedge = e;
//顶点j 上的入度++
(*GL)->adjList[j].in++;
//2.如果是无向图:创建顶点j与i的边表信息结点
e = (EdgeNode *)malloc(sizeof(EdgeNode));
//记录边表信息对应顶点的下标j
e->adjvex = i;
//将当前结点的指向adjList[i]的顶点边表上
e->next = (*GL)->adjList[j].firstedge;
(*GL)->adjList[j].firstedge = e;
//顶点j 上的入度++;
(*GL)->adjList[i].in++;
}
}
}
}
3.递归遍历顶点信息
Boolean visited[MAXSIZE]; //访问标志的数组
void DFS(GraphAdjList GL, int i)
{
//1.记录已经访问过了
visited[i] = TRUE;
//2.打印顶点数据
printf("%c ",GL->adjList[i].data);
//获取第一个边信息
EdgeNode *p;
p = GL->adjList[i].firstedge;
//3.遍历边表信息,如果有示访问过的结点,则递归访问
while (p) {
if(!visited[p->adjvex]){
DFS(GL,p->adjvex);
}
p = p->next;
}
}
4.遍历图
void DFSTraverse(GraphAdjList GL)
{
//1. 将访问记录数组默认置为FALSE
for (int i = 0; i < GL->numVertexes; i++) {
//初始化所有顶点状态都是未访问过的状态
visited[i] = FALSE;
}
//2. 选择一个顶点开始DFS遍历. 例如A
for(int i = 0; i < GL->numVertexes; i++)
//对未访问过的顶点调用DFS, 若是连通图则只会执行一次
if(!visited[i]){
DFS(GL, i);
}
}
5.调试代码
printf("邻接表的深度优先遍历!\n");
MGraph G;
GraphAdjList GL;
CreateMGraph(&G);
CreateALGraph(G,&GL);
DFSTraverse(GL);
printf("\n");
四、总结
- 1.图的深度遍历主要思路是
边访问边标记
的逻辑;- 2.经过一轮边访问边标记后,可能存在未访问到的顶点,所以需要进行
回溯
继续查找未访问到的顶点,直到回溯到最开始访问的顶点为止;- 3.
递归
的函数调用栈入栈出栈的逻辑刚好符合回溯的逻辑,所以我们在实现图的深度优先遍历时会使用递归。