在# 10--图和图的存储中已经讲解过了图的基本知识和图的存储:
邻接矩阵和邻接表。总结图的特征如下:
- 1.图是由
有限个顶点和顶点之间连接的边组成的一种逻辑结构;- 2.图是
无序的,即顶点和顶点之间的边不存在顺序关系;- 3.图即可以使用
邻接矩阵实现顺序存储,也可以使用邻接表实现链式存储;
既然图的是无序的,那么应该如何遍历图,使得所有的顶点和它们的边都被访问到呢?
一、图的广度优先遍历
如上图,将右边的树进行适当的变形,使之变为如右图那样有一定层次关系,以方便我们查看。
图的广度优先遍历需要借助队列来实现,下面分析一下实现的逻辑:
- 1.标记
A已经访问过了,同时将A入队;- 2.将
A出队,并且没有标记过的顶点B和F入队,标记B和F已经访问过了;- 3.将
B出队,同时将与B有连接的顶点C、I、G入队,将顶点C、I、G入队标记已经访问过了;- 4.将
F出队,同时将与F有连接的,并且还没有入队的顶点E入队,标记E已经访问过了;A和G标记访问过了,不用重复入队;- 5.将
C出队,此时与C连接的B和I已经标记,所以它们不用重复入队,只需要将D入队,并且标记D访问过了;- 6.将
I出队,此时与I连接的B、C和D都已经标记过了,所以与I有连接了B、C、D都不用重复入队;- 7.将G出队,此时与G有连接的B、F和D已经标记过了,所以只需要将H入队,标记H已经访问过了;
- 8.将
E出队,此时与E有连接的F和D已经标记过了,所以F和D都不用重复入列;- 9.将
D出队,此时与D有连接的C、I、G、E和H已经标记过了,所以C、I、G、E、H都不用重复入队;- 10.将
H出队,此时与H连接的D、G、E已经标记过了,不用重复入队。并且此时,队列为空,遍历结束。
总结起来广度优先遍历的特点如下:
- 1.开始遍历时,将任意一个
顶点入队;- 2.每次从队头
出队一个顶点,并且标记访问过了。同时将与这个顶点连接的,没有标记过的顶点入队;- 3.当
队列为空时,结束遍历。
二、邻接矩阵的广度优先遍历
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];
}
}
}
4.构建队列
//1.循环队列的顺序存储结构
typedef struct
{
int data[MAXSIZE];
int front; //头指针
int rear; //尾指针,若队列不空,指向队列尾元素的下一个位置
}Queue;
//2.初始化一个空队列Q
Status InitQueue(Queue *Q)
{
Q->front=0;
Q->rear=0;
return OK;
}
//3.队列判空
Status QueueEmpty(Queue Q)
{
// 队列空的标志
if(Q.front==Q.rear)
{
return TRUE;
}
else{
return FALSE;
}
}
//4.入队
Status EnQueue(Queue *Q,**int** e)
{
//队列满的判断
if ((Q->rear+1)%MAXSIZE == Q->front){
return ERROR;
}
//将元素e入队
Q->data[Q->rear]=e;
//移动队尾指针
Q->rear=(Q->rear+1)%MAXSIZE;
return OK;
}
//出队
Status DeQueue(Queue *Q,int *e)
{
//队列空的判断
if (Q->front == Q->rear){
return ERROR;
}
//获取队头元素
*e = Q->data[Q->front];
//出队:移动队头指针
Q->front = (Q->front+1)%MAXSIZE;
return OK;
}
5.邻接矩阵的广度优先遍历实现
Boolean visited[MAXVEX]; //标志数组
void BFSTraverse(MGraph G){
int temp = 0;
//1.创建队列
Queue Q;
InitQueue(&Q);
//2.将访问标志数组全部置为"未访问状态FALSE"
for (int i = 0 ; i < G.numVertexes; i++) {
visited[i] = FALSE;
}
//3.对遍历邻接表中的每一个顶点(对于连通图只会执行1次,这个循环是针对非连通图)
for(int i = 0 ; i < G.numVertexes; i++) {
//判断顶点是否访问过
if(!visited[i]){
//访问并标记
visited[i] = TRUE;
printf("%c ",G.vexs[i]);
//4.将顶点入队
EnQueue(&Q, i);
while (!QueueEmpty(Q)) {
//5.将顶点出队
DeQueue(&Q, &i);
//6.遍历与之相连的顶点
for (int j = 0; j < G.numVertexes; j++) {
//7.有连接,并且没有访问标记
if(G.arc[i][j] == 1 && !visited[j])
{
//8.访问并标记,同时入列
visited[j] = TRUE;
printf("%c ",G.vexs[j]);
EnQueue(&Q, j);
}
}
}
}
}
}
6.调试代码
printf("邻接矩阵广度优先遍历!\n");
MGraph G;
CreateMGraph(&G);
BFSTraverse(G);
printf("\n");
三、邻接表的广度优先遍历
1.构建邻接矩阵顺序存储下的图的存储
在
二、邻接矩阵的广优先遍历已经实现了
2.结点和数据结构定义
//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;
3.利用邻接矩阵构建邻接表
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++;
}
}
}
}
4.构建队列
在
二、邻接矩阵的广优先遍历已经实现了
5.邻接表的广度优先遍历实现
Boolean visited[MAXSIZE]; //标志数组
void BFSTraverse(GraphAdjList GL){
//1.定义结点
EdgeNode *p;
//2.创建队列
Queue Q;
InitQueue(&Q);
//3.将访问标志数组全部置为"未访问状态FALSE"
for(int i = 0; i < GL->numVertexes; i++)
visited[i] = FALSE;
//4.对遍历邻接表中的每一个顶点(对于连通图只会执行1次,这个循环是针对非连通图)
for(int i = 0 ;i < GL->numVertexes;i++){
//4.判断当前结点是否被访问过.
if(!visited[i]){
//5.标记访问过了
visited[i] = TRUE;
//打印顶点
printf("%c ",GL->adjList[i].data);
//6.将顶点入队
EnQueue(&Q, i);
//7.判断队列是否为空
while (!QueueEmpty(Q)) {
//8.出队
DeQueue(&Q, &i);
//9.遍历边表链表
p = GL->adjList[i].firstedge;
while (p) {
//10.判断是否已经标记过了
if(!visited[p->adjvex]){
//11.标记访问过了
visited[p->adjvex] = TRUE;
printf("%c ",GL->adjList[p->adjvex].data);
//12.入队
EnQueue(&Q, p->adjvex);
}
//13.遍历下一结点
p = p->next;
}
}
}
}
}
6.调试代码
printf("邻接表广度优先遍历\n");
MGraph G;
GraphAdjList GL;
CreateMGraph(&G);
CreateALGraph(G,&GL);
BFSTraverse(GL);
四、总结
图的广度遍历借助了队列来实现,性能较好,但是实现复杂。图的深度优先遍历那样使用了递归,实现简单,但是性能较差,实现开发中可以按需求和自己的喜好来选择。
关于文章中关于队列的实现使用的是顺序存储的逻辑,关于队列的顺序存储,请看文章# 06--队列的顺序实现。