数据结构与算法学习笔记10:图/深度广度遍历/最小生成树/最短路径算法/拓扑排序
图Graph
基础概念
有向图: 图中的边有次序性且有方向性
<v1,v3>和<v3,v1> 两条边
无向图: 图中的边无次序性且无方向性
(v1,v3)和(v3,v1) 同一条边
有向完全图: 图中所有顶点间所有可能的边均存在,其数量为 A n 2 A_n^2 An2,即为 n ( n − 1 ) n(n-1) n(n−1)
无向完全图: 图中所有顶点间所有可能的边均存在,其数量为 C n 2 C_n^2 Cn2,即为 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n−1)
有向连通图: 图中所有相异的成对顶点均有路径可通(1能到2,2能到1)
无向连通图: 图中所有顶点直接均有路径可通
极大连通子图(连通分量): 指的就是该图本身
强连通: 指的是有向顶点双向连通,所有顶点符合强连通的有向连通图,其强连通分量也是其本身。
依附: 边(v1,v2)依附于点v1和v2
简单路径: 如果路径上的各顶点均不互相重复,称这样的路径为简单路径。
回路: 如果路径上的第一个顶点与最后一个顶点重合,这样的路径称为回路(cycle)或环或圈。
如在图1中,回路有:
度: 指的是当前顶点关联了几条边,有向图中还分为出度(横向)和入度(纵向)(就是指向和被指向),出度+入度=度
存储方式
-
邻接矩阵
V1 V2 V3 V4 V5 V1 0 1 1 1 0 V2 1 0 1 1 1 V3 1 1 0 1 0 V4 1 1 1 0 0 V5 0 1 0 0 0 -
邻接链表(后面链表的结点顺序无所谓,只要和第一个点相关,后面可以随便放)
V1->V2->V3->V4 V2->V1->V3->V4->V5 V3->V1->V2->V4 V4->V1->V2->V3 V5->V2 -
邻接矩阵适合边多的图,其空间固定,反正5个点就是5*5,不管你用多少都是;
而邻接链表适合边少的,其空间不固定,除了存放和哪个点有关外还需要存放next点的指针变量
图的创建
-
过程:1、顶点个数;2、创建矩阵(赋初值);3、根据边关系和权值给矩阵赋值
-
代码:(简单无向版~)
#include <stdio.h> #include <stdlib.h> #include <string.h> #define VertexNumber 5 typedef struct node{ int nVertex;//点的数量 int nEdge; //边的数量 int pMatrix[VertexNumber][VertexNumber]; //矩阵(去宏定义里改,是写死的) //如果想要动态的可以用指针形式,如 //int *pMatrix; }Graph; Graph *CreateGraph(){ Graph *pGraph = NULL; pGraph = (Graph*)malloc(sizeof(Graph)); printf("输入顶点个数和边的数量:\n"); int nV,nE; scanf("%d%d",&nV,&nE); pGraph->nVertex = nV; pGraph->nEdge = nE; memset(pGraph->pMatrix,0,sizeof(int)*VertexNumber*VertexNumber);//初始化 //pGraph->pMatrix = (int*)malloc(sizeof(int)*nV*nV); //动态申请 //memset(pGraph->pMatrix,0,sizeof(int)*nV*nV); //给矩阵全部赋初值为 0 int v1,v2; //放入边 for(int i = 0;i < nE;i++){ printf("请输入两个顶点以确认一条边:\n"); scanf("%d%d",&v1,&v2); if (v1 >= 1 && v1 <= nV && v2 >= 1 && v2 <= nV && v1 != v2 && pGraph->pMatrix[v1-1][v2-1] == 0) { //输入的顶点要大于等于1且小于点的个数; //同时由于创建是简单图,所以顶点和顶点自己不存在关系,不能输入两个一样的顶点; //同时需要判断矩阵内相应位置是否已经存在关系,如果不存在才放。 pGraph->pMatrix[v1-1][v2-1] = 1; pGraph->pMatrix[v2-1][v1-1] = 1; //构建的是无向,所以矩阵应该沿对角线对称 //pGraph->pMatrix[(v1-1)*nV][(v2-1)*nV] = 1; //pGraph->pMatrix[(v2-1)*nV][(v1-1)*nV] = 1; //动态申请的话要算一下位置,前面的判断也需要算,pGraph->pMatrix[(v1-1)*nV][(v2-1)*nV] == 0 } else { i--; //放错了要把边的计数减回来噢 } } return pGraph; } int main(){ Graph *pGraph = CreateGraph(); int i; int j; //还不知道图的遍历,所以先用二维数组遍历啦 for (i = 0 ; i < pGraph->nVertex ; i++) { for (j = 0 ; j < pGraph->nVertex ; j++) { printf("%d ",pGraph->pMatrix[i][j]); } printf("\n"); } return 0; }
图的遍历
广度优先遍历(BFS)
分析:
V1 | V2 | V3 | V4 | V5 | |
---|---|---|---|---|---|
V1 | 0 | 1 | 1 | 1 | 0 |
V2 | 1 | 0 | 1 | 1 | 0 |
V3 | 1 | 1 | 0 | 1 | 1 |
V4 | 1 | 1 | 1 | 0 | 0 |
V5 | 0 | 1 | 0 | 0 | 0 |
- 假设以2为初始点,先打印2,然后看与2相关的所有顶点(1 3 4),打印1 3 4,然后看与1相关的所有顶点(2 3 4),2 3 4已经全部被打印了,然后看3相关的所有顶点(1 2 4 5),1 2 4被打印过了,所以打印5,然后看4相关的所有顶点(1 2 3)已经全部被打印了,看5相关的所有顶点(3)已经全部被打印了,至此结束。
过程:
- 1、标记数组
- 2、队列
- 3、起始顶点入队等待处理并标记(不要等打印再标记 这样的话 会重复处理的)
- 4、顶点处理
- 弹出 打印
- 遍历(找到有关且未处理的点排队等待处理,重复4的步骤)
代码:
void BFS(Graph *pGraph,int nBegin){ //传图和起始顶点
//标记数组
int *pMark = NULL;
pMark = (int *)malloc(sizeof(int*)*pGraph->nVertex);
memset(pMark, 0, sizeof(int*)*pGraph->nVertex);
//队列
queue<int>q;
//起始顶点 入队 标记
q.push(nBegin);
pMark[nBegin-1] = 1;
while (!q.empty()) {
nBegin = q.front();
q.pop();
printf("%d ",nBegin);
//遍历
for (int i = 0; i<pGraph->nVertex; i++) {
if (pGraph->pMatrix[nBegin-1][i] == 1 && pMark[i] == 0) {
q.push(i + 1);
pMark[i] = 1;
}
}
}
printf("\n");
//释放
free(pMark);
pMark = NULL;
}
深度优先遍历(DFS)
——经典回溯问题——
分析:
V1 | V2 | V3 | V4 | V5 | |
---|---|---|---|---|---|
V1 | 0 | 1 | 1 | 1 | 0 |
V2 | 1 | 0 | 1 | 1 | 0 |
V3 | 1 | 1 | 0 | 1 | 1 |
V4 | 1 | 1 | 1 | 0 | 0 |
V5 | 0 | 1 | 0 | 0 | 0 |
-
假设以4为初始点,先打印4,然后看与4相关的第一个顶点(1),打印1,然后看与1相关的第一个顶点(2),打印2,然后看与2相关的第一个顶点(1),1已经被打印,则继续看为(3),打印3,看与3相关的第一个顶点(1),1已经被打印,则继续看为(2),2也已经被打印,继续看为(4),4也已经被打印,继续看(5),打印(5),找5相关的第一个顶点,全部找完发现都已经被打印,则倒回上一级,查看3的第二个相关顶点,无,继续返回2,查看2的第二个相关顶点为(4),4也被打印,全部被打印,则倒回去看1的第二个相关顶点为(3),打印过了,第三个相关顶点(4),打印过了,倒回4,看4的第二个相关顶点(2),打印过了,4的第三个相关顶点(3),打印过了,至此彻底结束。
-
结果:4、1、2、3、5
过程:
- 1、标记数组(标记打印的点,数组下标01234对应着12345顶点,打印了4则在下标为3的位置上做标记)
- 2、处理顶点
- 输出;
- 标记;
- 遍历(找到第一个有关且未被处理过的点进行处理,处理过程重复2的步骤)。
代码:
void myDFS(Graph *pGraph,int nBegin,int *pMark){ //递归函数
//打印 标记
printf("%d ",nBegin);
pMark[nBegin-1] = 1;
//遍历
int i;
for (i = 0; i < pGraph->nVertex; i++) {
//有关的且未被打印过的第一个点
if (pGraph->pMatrix[nBegin-1][i] == 1 && pMark[i] == 0) {
//处理
myDFS(pGraph, i + 1, pMark); //i是下标但是遍历的点是1到n所以要i+1
}//用循环结束来控制递归结束
}
}
void DFS(Graph *pGraph,int nBegin){ //传图和起始顶点
//标记数组
int *pMark = NULL;
pMark = (int *)malloc(sizeof(int*)*pGraph->nVertex);
memset(pMark, 0, sizeof(int*)*pGraph->nVertex);
//直接写递归的话,每次都要来个标记数组,所以可以再写个递归函数
//顶点处理递归函数
myDFS(pGraph,nBegin,pMark);
//释放
free(pMark);
pMark = NULL;
}
DFS和BFS的其他应用
通过遍历,对比遍历的个数和图点的个数,可以判断该图是否为连通图,如果相等则连通,不相等则不连通。
当然DFS和BFS也可完成非连通图的遍历,当完成第一次遍历DFS或者BFS以后,找到标记数组中还没有遍历的点,由其再进行DFS或者BFS,两次遍历的个数相加看等不等于所有个数,不等于的话继续找标记数组继续遍历,直到相等,说明遍历完成。
最小生成树
克鲁斯卡尔-kruskal算法
连通,且无闭合回路。从所有边里最短的开始
- 以上图为例,从所有边中找到最短的一条,然后开始画,也就是边3及其两头的点2和点4,然后再找第二短的,也就是边4及其两头的点2和点6,继续,是边8,但边8画上去的话会有闭合回路,所以舍弃,找边9及其两头的点2和点1,然后是边12及其两头的点2和点3,然后边16、17由于闭合回路舍弃,然后是边19及其两头的点5和点6,至此结束。
普里姆-Prim算法
连通,且无闭合回路。选择任意一个点并从其最短边开始
- 以上图为例,随意选择一个点,假如选点3,其相关边最小的,也就是12,所以画边12和边12另一头的点2,然后看点3和点2辐射出去的所有边里最短的,为边3及点4,然后看点234的最短边,为边4及点6,然后看点2346里最小的边,为边9及点1,然后看12346里最小的边,为16和17(由于闭合回路所以舍弃),然后是边19和点5,至此结束。
最短路径算法
迪杰斯特拉(Dijkstra)
用于:有向的带正权值的图中,一个点到其他所有点之间的最短路径,使用的是贪心思想
V1 | V2 | V3 | V4 | V5 | |
---|---|---|---|---|---|
V1 | 0 | 12 | 1 | 14 | 30 |
V2 | 6 | 0 | 0 | 11 | 0 |
V3 | 17 | 3 | 0 | 14 | 3 |
V4 | 8 | 5 | 9 | 0 | 0 |
V5 | 0 | 0 | 12 | 8 | 0 |
PS:正常来说到不了应该放的是无穷∞,在代码中就放int类型最大值7FFFFFF
-
分析:
假设要找点1到其他所有点之间的最短路径,首先写出点1到其他点的初始长度
- V1{0,12,1,14,30}
然后找路径中最小的也就是点1至点3的1
- V1-V3{0,?,1,?,?}
点3至点2的路径为3,再加上刚开始点1至点3的路径1,也就是1-3-2路径为4,比原始的1-2路径的12小,因此进行更新;同理,1-3-5路径为1+3=4<30,进行更新;1-3-4路径为1+14=15>14,因此保持原始路径,得到:
- V1-V3{0,4,1,14,4}
然后选出除1和3外最短的,由于2和5都是4,所以选哪个都可以,此处选择2
- V1-V3-V2{0,4,1,?,?}
点2到点4路径为11,加上1-3-2的路径4,共为15,还是不更新,2-5路径不能走,也不更新,继续选择5
- V1-V3-V2-V5{0,4,1,?,4}
点5到点4路径为8,加上1-3-5的路径4,共为12小于14,可以更新,得到
- V1-V3-V2-V5{0,4,1,12,4} ,此时得到点1到其他所有点之间的最短路径
-
如果要求多元最短路径(任意两个点之间的最短距离),也可以使用迪杰斯特拉解决,起始就是以每个点作为起始点,按照上述方法进行,最后拼成一个矩阵就行了,当然也还有别的不那么麻烦的解法,用的是动态规划思想,后续再提。
拓扑排序(Topological Sort)
并不是一个纯粹的排序算法,只针对于DAG(即有向无环图),找到一个可执行的线性顺序,也就是拓扑序列。
拓扑序列:按照这个顺序,在每个项目开始时,能够保证它的前提活动都已经完成,从而使整个工程顺利进行。
(有环就死循环了,比如abc三个形成环,相互依赖,大家都没法加载)
DAG的无环指的是没有循环的意思,只要不是循环,也可以有闭合回路!!
如果不是DAG,不会有拓扑序;如果是DAG,至少有一个拓扑序;如果有一个图存在拓扑序,它一定是DAG。
AOV网
用顶点表示活动,用弧表示活动间的优先关系的有向图称为顶点表示活动的网(Activity On Vertex Network),简称AOV网,(属于DAG)
课程代号 | 先修课程 |
---|---|
C1 | 无 |
C2 | 无 |
C3 | C1,C2 |
C4 | C3,C5 |
C5 | C2 |
C6 | C4,C5 |
C7 | C4,C9 |
C8 | C1 |
C9 | C8 |
-
过程:
1、预处理每个点的入度
C1 C2 C3 C4 C5 C6 C7 C8 C9 0 0 2 2 1 2 2 1 1 2、执行入度为0的点(C1,C2),把可以执行的点,拿走放到容器,更新每个点的入度
C3 C4 C5 C6 C7 C8 C9 0 2 0 2 2 0 1 3、重复步骤2,执行入度为0的点(C3,C5,C8)
C4 C6 C7 C9 0 1 2 0 4、重复步骤2,执行入度为0的点(C4,C9)
4、重复步骤2,执行入度为0的点(C6,C7)
AOE网
AOV网的基础上,其边带权值(类似于活动持续的时间),(属于DAG)
拓扑排序的其他应用
可以用于判断该图是否为DAG,如果不是DAG,那么在某次拿走入度为0的点后会发现,剩下的点没有入度为0的了,全部都大于0,可能是1 1 1这样的循环,没有办法拿出,比较拿出点和所有点的个数,就可以判断出来。