数据结构与算法学习笔记10:图/深度广度遍历/最小生成树/最短路径算法/拓扑排序

图Graph

基础概念

有向图: 图中的边有次序性且有方向性

​ <v1,v3>和<v3,v1> 两条边

无向图: 图中的边无次序性且无方向性

​ (v1,v3)和(v3,v1) 同一条边

有向完全图: 图中所有顶点间所有可能的边均存在,其数量为 A n 2 A_n^2 An2,即为 n ( n − 1 ) n(n-1) n(n1)

无向完全图: 图中所有顶点间所有可能的边均存在,其数量为 C n 2 C_n^2 Cn2,即为 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n1)

有向连通图: 图中所有相异的成对顶点均有路径可通(1能到2,2能到1)

无向连通图: 图中所有顶点直接均有路径可通

极大连通子图(连通分量): 指的就是该图本身

强连通: 指的是有向顶点双向连通,所有顶点符合强连通的有向连通图,其强连通分量也是其本身。

依附: 边(v1,v2)依附于点v1和v2

简单路径: 如果路径上的各顶点均不互相重复,称这样的路径为简单路径

回路: 如果路径上的第一个顶点与最后一个顶点重合,这样的路径称为回路(cycle)或

如在图1中,回路有:
在这里插入图片描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QJ3kVOvt-1654757193459)(https://cdn.jsdelivr.net/gh/mozro0327/mynotes/images/20220607161404.png)]

度: 指的是当前顶点关联了几条边,有向图中还分为出度(横向)和入度(纵向)(就是指向和被指向),出度+入度=度

存储方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K051MHCT-1654757193460)(https://cdn.jsdelivr.net/gh/mozro0327/mynotes/images/20220607163458.png)]

  • 邻接矩阵

    V1V2V3V4V5
    V101110
    V210111
    V311010
    V411100
    V501000
  • 邻接链表(后面链表的结点顺序无所谓,只要和第一个点相关,后面可以随便放)

    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)
分析:
V1V2V3V4V5
V101110
V210110
V311011
V411100
V501000
  • 假设以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)

——经典回溯问题——

分析:
V1V2V3V4V5
V101110
V210110
V311011
V411100
V501000
  • 假设以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,两次遍历的个数相加看等不等于所有个数,不等于的话继续找标记数组继续遍历,直到相等,说明遍历完成。

最小生成树

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G97GrQyy-1654757193461)(C:\Users\Nothingserious\AppData\Roaming\Typora\typora-user-images\image-20220609133131653.png)]

克鲁斯卡尔-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)

用于:有向的带正权值的图中,一个点到其他所有点之间的最短路径,使用的是贪心思想

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wSfd1z8P-1654757193461)(C:\Users\Nothingserious\AppData\Roaming\Typora\typora-user-images\image-20220609133105737.png)]

V1V2V3V4V5
V101211430
V2600110
V31730143
V485900
V5001280

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的无环指的是没有循环的意思,只要不是循环,也可以有闭合回路!!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Osd5EYC8-1654757193462)(C:\Users\Nothingserious\AppData\Roaming\Typora\typora-user-images\image-20220609131603433.png)]

如果不是DAG,不会有拓扑序;如果是DAG,至少有一个拓扑序;如果有一个图存在拓扑序,它一定是DAG。

AOV网

用顶点表示活动,用弧表示活动间的优先关系的有向图称为顶点表示活动的网(Activity On Vertex Network),简称AOV网,(属于DAG)

课程代号先修课程
C1
C2
C3C1,C2
C4C3,C5
C5C2
C6C4,C5
C7C4,C9
C8C1
C9C8

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QPlsiTP8-1654757193463)(C:\Users\Nothingserious\AppData\Roaming\Typora\typora-user-images\image-20220609134353376.png)]

  • 过程:

    1、预处理每个点的入度

    C1C2C3C4C5C6C7C8C9
    002212211

    2、执行入度为0的点(C1,C2),把可以执行的点,拿走放到容器,更新每个点的入度

    C3C4C5C6C7C8C9
    0202201

    3、重复步骤2,执行入度为0的点(C3,C5,C8)

    C4C6C7C9
    0120

    4、重复步骤2,执行入度为0的点(C4,C9)

    4、重复步骤2,执行入度为0的点(C6,C7)

AOE网

AOV网的基础上,其边带权值(类似于活动持续的时间),(属于DAG)

拓扑排序的其他应用

可以用于判断该图是否为DAG,如果不是DAG,那么在某次拿走入度为0的点后会发现,剩下的点没有入度为0的了,全部都大于0,可能是1 1 1这样的循环,没有办法拿出,比较拿出点和所有点的个数,就可以判断出来。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

97Marcus

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

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

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

打赏作者

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

抵扣说明:

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

余额充值