一.前言:
学习这一节之前,我们需要弄懂一些基础的概念,不然会在学后面的时候有点迷糊。
1.顶点:
线性表中我们把数据元素叫元素,在树中叫结点,在图中叫顶点(Vertex)。
2.邻接矩阵:(无向图)
一种用于表示图的数据结构。它是一个二维数组,其中的元素表示图中顶点之间的连接关系。对于一个有n个顶点的图,邻接矩阵是一个大小为n×n的二维数组。要是图中有两个顶点(假设是第1个和第2个顶点)相连(存在一条边),那么他们对应在邻接矩阵中的第1行第2列和第2行第1列位置为1,其他没有相连的用0表示。
如下图表示:(下面都用以下图举栗子)
我们把有相连边的顶点拿出来:AB,AC,AD,BC,BD,DE,BE(不分先后)
因为顶点顺序为ABCDE,然后我们可以作出它的邻接矩阵:
邻接矩阵 | A | B | C | D | E |
A | 0 | 1 | 1 | 1 | 0 |
B | 1 | 0 | 1 | 1 | 1 |
C | 1 | 1 | 0 | 0 | 0 |
D | 1 | 1 | 0 | 0 | 1 |
E | 0 | 1 | 0 | 1 | 0 |
观察发现无向图邻接矩阵是关于主对角线对称的,这使得后面可以通过邻接矩阵的对称性了解和运用图的一些性质。
»»具体图的基本概念还不熟悉的同学可以看这位大佬的文章,了解了直接往下看代码举例
数据结构与算法(C语言) | 图的基本定义及存储结构https://blog.csdn.net/qq_35924276/article/details/81781022
二.代码部分:
//无向图图的创建
#include<stdio.h>
#include<stdlib.h>
#define MAX_SIZE 5
typedef struct graph
{
char* vexs;//存储图中所有顶点的标签或名称
int** arcs;//用来表示一个邻接矩阵(二维数组)
int vexNum;//顶点数
int arcNum;//边数
}Graph;
//以下所有队列的操作都是为BFS遍历做的准备,不可少
typedef struct queue
{
int front;//标记队首元素的位置
int rear;//标记队尾元素的下一个空位置
int data[MAX_SIZE];
}Queue;
Queue* initQueue()//分配队列空间
{
Queue* Q=(Queue*)malloc(sizeof(Queue));
Q->front=Q->rear=0;//处在同一位置时为空
return Q;
}
int isFull(Queue* Q)//判断队列是否已满
{
if((Q->rear+1)%MAX_SIZE==Q->front)
{
return 1;
}
else
{
return 0;
}
}
int isEmpty(Queue* Q)//判断是否为空
{
if(Q->front==Q->rear)//在同一位置时空
{
return 1;
}
else
{
return 0;
}
}
int enQueue(Queue* Q,int data)//入队(尾插)
{
if(isFull(Q))
{
return 0;
}
else
{
Q->data[Q->rear]=data;
Q->rear=(Q->rear+1)%MAX_SIZE;
return 1;
}
}
int deQueue(Queue* Q)//出队(头出)
{
if(isEmpty(Q))
{
return -1;
}
else
{
int data=Q->data[Q->front];
Q->front=(Q->front+1)%MAX_SIZE;//数据往后移一位,相当于后面一个元素往前移了
return data;
}
}
Graph* initGraph(int vexNum)//为图结构体分配空间
{
Graph* G=(Graph*)malloc(sizeof(Graph));
G->vexs=(char*)malloc(sizeof(char)*vexNum);
G->arcs=(int**)malloc(sizeof(int*)*vexNum);
for(int i=0;i<vexNum;i++)
{
G->arcs[i]=(int*)malloc(sizeof(int)*vexNum);
}
G->vexNum=vexNum;//初始化顶点数
G->arcNum=0;//初始化边数
return G;
}
void creatGraph(Graph* G,char* vexs,int* arcs)//创建一个图
{
for(int i=0;i<G->vexNum;i++)
{
G->vexs[i]=vexs[i];
for(int j=0;j<G->vexNum;j++)
{
//一种指针算术运算,通过指针运算可以模拟成二维数组。假设 arcs 的大小为 m 行 n 列,那么第 i 行、第 j 列的元素可以通过 *(arcs + i * n + j) 来访问。
G->arcs[i][j]=*(arcs+i*(G->vexNum)+j);
if(G->arcs[i][j]!=0)//更新边数,1有边,0无边
{
G->arcNum++;
}
}
}
G->arcNum/=2;//因为是无向图,这里的边数算了两次,只算一次即可
}
void DFS(Graph* G,int* flag,int index)//深度优先遍历
{
printf("%c\t",G->vexs[index]);
flag[index]=1;//已经访问过顶点标记为1,之后不会再访问
for(int i=0;i<G->vexNum;i++)
{
if(G->arcs[index][i]==1&&!flag[i])
{
DFS(G,flag,i);
}
}
}
void BFS(Graph* G,int* flag,int index)//广度优先遍历(队列)
{
Queue* Q=initQueue();
printf("%c\t",G->vexs[index]);
flag[index]=1;
enQueue(Q,index);
while(!isEmpty(Q))//队不空时
{
int i=deQueue(Q);
for(int j=0;j<G->vexNum;j++)//就将邻接矩阵每一行有1的顶点先输出,输出完了再转到刚刚有1的顶点的那一行,再看此行有没有1的顶点,接着输出
{
if(G->arcs[i][j]==1&&!flag[j])
{
printf("%c\t",G->vexs[j]);
flag[j]=1;
enQueue(Q,j);
}
}
}
}
int main()
{
Graph* G=initGraph(5);
int* flag=(int*)malloc(sizeof(int)*G->vexNum);//为每一个顶点都分配一块用来判断是非已输出的空间
for(int i=0;i<G->vexNum;i++)//首先赋值为0,表示未访问
{
flag[i]=0;
}
int arcs[5][5]=
{
0,1,1,1,0,
1,0,1,1,1,
1,1,0,0,0,
1,1,0,0,1,
0,1,0,1,0
};// 有边是1,无边是0
creatGraph(G,"ABCDE",(int*)arcs);
DFS(G,flag,0);
printf("\n");
for(int i=0;i<G->vexNum;i++)//首先赋值为0,表示未访问
{
flag[i]=0;
}
BFS(G,flag,0);
printf("\n");
return 0;
}
1.DFS遍历函数解释:
解释:DFS叫深度优先遍历,这种遍历方式主要思路是从图中一个未访问的顶点V开始,沿着一条路一直走到底,然后从这条路尽头的节点回退到上一个节点,再从另一条路开始走到底,不断递归重复此过程,直到所有的顶点都遍历完成。它的特点是不撞南墙不回头,先走完一条路,再换一条路继续走。还是没有理解的同学可以看下面的遍历的分解过程。
函数体:
void DFS(Graph* G,int* flag,int index)//深度优先遍历
{
printf("%c\t",G->vexs[index]);
flag[index]=1;//已经访问过顶点标记为1,之后不会再访问
for(int i=0;i<G->vexNum;i++)
{
if(G->arcs[index][i]==1&&!flag[i])
{
DFS(G,flag,i);
}
}
}
遍历过程如下:
<1> 此时由A到B,发现A有到B的边,则遍历到B处,B可以输出
<1> | A | B | C | D | E |
A | 0 | ① | 1 | 1 | 0 |
B | 1 | 0 | 1 | 1 | 1 |
C | 1 | 1 | 0 | 0 | 0 |
D | 1 | 1 | 0 | 0 | 1 |
E | 0 | 1 | 0 | 1 | 0 |
<2>由B继续往后遍历,B有到C的边,则C可以输出
<2> | A | B | C | D | E |
A | 0 | 1 | 1 | 1 | 0 |
B | 1 | 0 | ① | 1 | 1 |
C | 1 | 1 | 0 | 0 | 0 |
D | 1 | 1 | 0 | 0 | 1 |
E | 0 | 1 | 0 | 1 | 0 |
<3>到C顶点处,没有下一个未访问的顶点了,正常来说按顺序是到D的,结果C到D没有边,则递归返回到B顶点,由B继续走未走过的边遍历未访问的顶点,于是到了D顶点,D输出
<3> | A | B | C | D | E |
A | 0 | 1 | 1 | 1 | 0 |
B | 1 | 0 | 1 | ① | 1 |
C | 1 | 1 | 0 | 0 | 0 |
D | 1 | 1 | 0 | 0 | 1 |
E | 0 | 1 | 0 | 1 | 0 |
<4>D继续往后走,发现到E有路了,于是E输出
<4> | A | B | C | D | E |
A | 0 | 1 | 1 | 1 | 0 |
B | 1 | 0 | 1 | 1 | 1 |
C | 1 | 1 | 0 | 0 | 0 |
D | 1 | 1 | 0 | 0 | ① |
E | 0 | 1 | 0 | 1 | 0 |
总结:通过以上的图像分步遍历示意,同学们应该知道规律了。找下一个相邻顶点时看行,有没有边看行列相交处是否为1,有边是1,无边为0。
1.BFS遍历函数解释:
BFS叫广度优先遍历,它从图的起始节点开始,首先访问所有与起始节点直接相连的节点,然后再访问与这些节点直接相连的所有未被访问过的节点,这个过程会持续到所有的节点都被访问过。这种遍历方式类似于波纹在水面上扩散的过程。在BFS广度遍历中,通常使用队列来存储待访问的节点。还是没有理解的同学可以看下面的遍历的分解过程。
函数体:
void BFS(Graph* G,int* flag,int index)//广度优先遍历(队列)
{
Queue* Q=initQueue();
printf("%c\t",G->vexs[index]);//开始:那一个顶点出来(A)
flag[index]=1;//标记A列无顶点输出了,也就是A已经输出
enQueue(Q,index);//开始:下标0入队
while(!isEmpty(Q))//队不空
{
int i=deQueue(Q);//开始:0出队,i=0,所以到A行
for(int j=0;j<G->vexNum;j++)//就将邻接矩阵每一行有1的顶点先输出,输出完了再转到刚刚有1的顶点的那一行,再看此行有没有1的顶点,接着输出
{
if(G->arcs[i][j]==1&&!flag[j])
{
printf("%c\t",G->vexs[j]);
flag[j]=1;
enQueue(Q,j);
}
}
}
}
遍历过程如下:
<1>先输出顶点A,此时flag[0]=1,表示第一列(j=0)已经有顶点A输出,这里保证每个顶点都输出的话,就要做这个标记,之后就不会再来这一列(j=0)找顶点了
i\j | →(j) | 0 | 1 | 2 | 3 | 4 |
↓(i) | <1> | A | B | C | D | E |
0 | A | 0 flag[0]=1 | 1 | 1 | 1 | 0 |
1 | B | 1 | 0 | 1 | 1 | 1 |
2 | C | 1 | 1 | 0 | 0 | 0 |
3 | D | 1 | 1 | 0 | 0 | 1 |
4 | E | 0 | 1 | 0 | 1 | 0 |
<2>输出顶点A后,继续在A行里面找1,B,C,D都有1,所以顶点都输出,并将其顶点对应的下标j值存入队列中,之后会访问这些j下标的行。另外还flag[1]=flag[2]=flag[3]=1,标志着BCD列都没有顶点输出了。
i\j | →(j) | 0 | 1 | 2 | 3 | 4 |
↓(i) | <2> | A | B | C | D | E |
0 | A | 0 flag[0]=1 | 1 flag[1]=1 | 1 flag[2]=1 | 1 flag[3]=1 | 0 |
1 | B | 1 | 0 | 1 | 1 | 1 |
2 | C | 1 | 1 | 0 | 0 | 0 |
3 | D | 1 | 1 | 0 | 0 | 1 |
4 | E | 0 | 1 | 0 | 1 | 0 |
<3>之后出队的是1,i=1,就来到了B行,B中ABCD列顶点都标记输出过了,E列有1,未标记,表示有边,输出E,之后j=4这个下标入队。到C行之后每一行就没有顶点了输出,因为列全标记了,遍历才结束。
i\j | →(j) | 0 | 1 | 2 | 3 | 4 |
↓(i) | <3> | A | B | C | D | E |
0 | A | 0 flag[0]=1 | 1 flag[1]=1 | 1 flag[2]=1 | 1 flag[3]=1 | 0 |
1 | B | 1 | 0 | 1 | 1 | 1 flag[4]=1 |
2 | C | 1 | 1 | 0 | 0 | 0 |
3 | D | 1 | 1 | 0 | 0 | 1 |
4 | E | 0 | 1 | 0 | 1 | 0 |
总结:flag是用来标记列的,可以这样认为:一列代表一个顶点,要是全部的列标记了就可以证明遍历结束了。另外,行只是用来看是否有到达下一个顶点的条件(有1就有边),这里行的执行顺序是按入队出队顺序决定的。
NO.33 【已修改代码注释漏洞】
如果这篇文章有帮助到你,希望给个免费的点赞支持,感谢🌹