数据结构与算法05----图

图的基本概念(多对多的逻辑结构)

  • 假设ABCDEFG是7个电话,之间的连线表示修有通信线路
  • 电话就是图的顶点 vi∈V,通信线路是 ei∈E,G = {V,E}就是一个图
  • 只要两个电话间有线路,就可以相互通话 => 无向图
  • 电话(顶点)连接的线路(边)数量:
  • ABCDE和GF之间消息无法传递:不连通
  • ABCDE和GF是两个连通分量
    在这里插入图片描述
  • 假设ABCDE是五个电话,之间的连线表示修有通信线路,数字表示该线路的电话费
  • 不同通信线路上的电话费不同:加权图
    在这里插入图片描述
  • 假设ABCDE是五个城市,带箭头连线表示该方向上有航班运营、
  • 例如航班A→D只能支持A飞往D,边是单向的 => 有向图
  • 飞来某地的航班数量:入度
  • 从某地起飞的航班数量: 出度
    在这里插入图片描述
  • 图并非只能表示地理数据,只要数据元素间满足多对多关系即可
  • 可表示几个学生之间的朋友关系 => 无向无权图
  • 可表示社交媒体的关注 / 被关注关系 => 有向无权图
  • 可表示多个耗时不同的任务之间的依赖关系 => 有向加权图

Key 1:在一个无向图中,所有顶点的度数之和为边数数量的2倍
Key 2:在一个有向图中,所有顶点的出度之和 == 所有顶点的入度之和

图的存储结构:邻接矩阵和邻接表

邻接矩阵

  • 设|V| = n,图可以用一个n * n的方阵表示
  • 即一个二维数组AdjMat[n][n] => Adjacent Matrix
  • AdjMat[i][j]表示vi和vj的连接情况

无向无权图:

  • AdjMat[i][j]为1表示有边相连,为0表示无边
  • AdjMat是对称的
    在这里插入图片描述

有向加权图:

  • AdjMat[i][j]表示vi → vj的权重
  • AdjMat[i][j]为+∞表示不通在这里插入图片描述

邻接表

  • 每个顶点用一个链表存下自己的邻居
  • |V| = n,有n个链表,即图可用一个链表的数组AdjList[n]存储
  • AdjList[i]表示顶点vi的链表(头)
  • 从AdjList[i]开始可以遍历所有以vi的邻居
    在这里插入图片描述

邻接矩阵和邻接表的比较

  • 设G = {V,E},|V| = n.

  • 邻接矩阵无论如何都需要一个二维数组[n][n],而邻接表中每条链表长度取决于它有多少邻居

  • 邻接矩阵访问AdjMat[i][j]是O(1)的,但邻接表访问特定边需要顺着起点的链表向后查找

  • 邻接表的优点:

    • 在边较少 (邻居少即边较少) 时节省许多空间 => 适用于稀疏图
  • 邻接表的缺点:

    • 无法直接获得某条边信息,需要vi链表进行从头顺序存取,最坏情况下O(n)

图DFS和BFS遍历

DFS(Depth First Search):深度优先遍历

  • 遇到新的邻居就进去…直到没有可以进的邻居了再返回
  • 优先进入后来遇到的邻居 => 递归 / 栈
    在这里插入图片描述

DFS代码实现

#include <stdio.h>
#include <stdlib.h>

typedef struct EdgeNode     //边表结点
{
    int adjvex;     //存储顶点对应的下标    存储的是一个位置,而非具体元素,为了以后改变数据方便操作
    struct EdgeNode *next;      //链域指向下一个邻接点
    int weight;     //权值(问题中有权值再用)
} EdgeNode;

typedef struct VertexNode       //顶点表结点
{
    char data;      //存放顶点信息
    EdgeNode *firstedge;        //指向边表中第一个结点
} VertexNode;

typedef struct 
{
    VertexNode adjlist[20];
    int n, e;
} GraphAdjlist;     //声明图的邻接表类型

int visited[10];       //访问标志数组(访问过赋值为1,反之为0)

void create(GraphAdjlist *G)        //创建邻接表
{
    int i, j, k;
    EdgeNode *e;

    printf("ding dian shu he bian shu:");
    scanf("%d %d", &G->n, &G->e);

    getchar();      //清除缓冲

    printf("diang dian bian hao:\n");

    for (i = 0; i < G->n; i++)
    {
        scanf("%c", &G->adjlist[i].data);       //输入顶点编号
        
        G->adjlist[i].firstedge = NULL;     //将边表置空
        getchar();
    }

    for (k = 0; k < G->e; k++)
    {
        printf("shu ru bian(vi, vj)shang de ding dian xu hao:\n");
        scanf("%d %d", &i, &j);     //头插法方便,快速  如果用尾插法需要指针遍历到尾部,太慢
        /*使用头插法加入边表结点*/
        e = (EdgeNode *)malloc(sizeof(EdgeNode));
        e->adjvex = j;
        e->next = G->adjlist[i].firstedge;
        G->adjlist[i].firstedge = e;

        e = (EdgeNode *)malloc(sizeof(EdgeNode));       //因为是无向图,一条边对应两个顶点
        e->adjvex = i;
        e->next = G->adjlist[j].firstedge;
        G->adjlist[j].firstedge = e;
    }

    printf("\n");
}

void DFS(GraphAdjlist *G, int i)
{
    EdgeNode *p;

    visited[i] = 1;

    printf("%c ", G->adjlist[i].data);

    p = G->adjlist[i].firstedge;
    while (p != NULL)
    {
        if (visited[p->adjvex] == 0)
        {
            DFS(G, p->adjvex);
        }

        p = p->next;
    }
}

void DFSTraverse(GraphAdjlist *G)
{
    int i;
    for (i = 0; i < G->n; i++)
    {
        visited[i] = 0;
    }

    for (i = 0; i < G->n; i++)
    {
        if (visited[i] == 0)
        {
            DFS(G, i);
        }
    }
}

int main()      //A B C D E
{
    GraphAdjlist G;
    
    create(&G);

    printf("DFS:");

    DFSTraverse(&G);

    return 0;
}

BFS(Breath First Search):广度优先遍历

  • 先把当前结点的邻居都遍历完,再按先来后到遍历邻居的邻居们,逐层向外扩张
  • 优先进入先访问的邻居的邻居 => 队列
    在这里插入图片描述

BFS代码实现

#include <stdio.h>
#include <stdlib.h>
#define max 20

typedef struct EdgeNode     //边表结点
{
    int adjvex;     //存储顶点对应的下标    存储的是一个位置,而非具体元素,为了以后改变数据方便操作
    struct EdgeNode *next;      //链域指向下一个邻接点
    int weight;     //权值(问题中有权值再用)
} EdgeNode;

typedef struct VertexNode       //顶点表结点
{
    char data;      //存放顶点信息
    EdgeNode *firstedge;        //指向边表中第一个结点
} VertexNode;

typedef struct 
{
    VertexNode adjlist[max];
    int n, e;
} GraphAdjlist;     //声明图的邻接表类型

int visited[max];       //访问标志数组(访问过赋值为1,反之为0)

void create(GraphAdjlist *G)        //创建邻接表
{
    int i, j, k;
    EdgeNode *e;

    printf("ding dian shu he bian shu:");
    scanf("%d %d", &G->n, &G->e);

    getchar();      //清除缓冲

    printf("diang dian bian hao:\n");

    for (i = 0; i < G->n; i++)
    {
        scanf("%c", &G->adjlist[i].data);       //输入顶点编号
        
        G->adjlist[i].firstedge = NULL;     //将边表置空
        getchar();
    }

    for (k = 0; k < G->e; k++)
    {
        printf("shu ru bian(vi, vj)shang de ding dian xu hao:\n");
        scanf("%d %d", &i, &j);     //头插法方便,快速  如果用尾插法需要指针遍历到尾部,太慢
        /*使用头插法加入边表结点*/
        e = (EdgeNode *)malloc(sizeof(EdgeNode));
        e->adjvex = j;
        e->next = G->adjlist[i].firstedge;
        G->adjlist[i].firstedge = e;

        e = (EdgeNode *)malloc(sizeof(EdgeNode));       //因为是无向图,一条边对应两个顶点
        e->adjvex = i;
        e->next = G->adjlist[j].firstedge;
        G->adjlist[j].firstedge = e;
    }

    printf("\n");
}

void BFS(GraphAdjlist *G, int v)
{
    EdgeNode *p;
    int queue[max], front = 0, rear = 0;        //定义循环队列并初始化
    int w, i;

    for (i = 0; i < G->n; i++)      //标志数组初始化
        visited[i] = 0;
    
    printf("%2c", G->adjlist[v].data);

    visited[v] = 1;
    rear = (rear + 1) % max;
    queue[rear] = v;
    while (front != rear)
    {
        front = (front + 1) % max;
        w = queue[front];
        p = G->adjlist[w].firstedge;
        while (p != NULL)
        {
            if (visited[p->adjvex] == 0)
            {
                printf("%2c", G->adjlist[p->adjvex].data);

                visited[p->adjvex] = 1;
                rear = (rear + 1) % max;
                queue[rear] = p->adjvex;
            }
            
            p = p->next;
        }
    }
    printf("\n");
}

int main()
{
    GraphAdjlist G;
    
    create(&G);

    printf("BFS:");

    BFS(&G, 0);

    return 0;
}

Key 1:DFS每步操作:进入当前结点的下一个未访问邻居,如无则返回
Key 2:BFS每步操作:进入当前队首结点并让其出队,将其未访问邻居入队

最小生成树(Prim算法)

  • 对于含n个结点的一个无向连通图,其边数最多为 n(n-1)/2 条,最少为n - 1条
  • 保持连通性的情况下,选 n - 1条边出来,剔除其他边,它就变成了一棵树
  • 生成树里没有环
  • n结点 + n - 1条边 + 连通 => 生成树
    在这里插入图片描述

最小生成树 MST(Minimum Spanning Tree)

  • 在加权图中选出n - 1条边来构成其生成树,且这些边的权值之和最小
  • MST不唯一
  • 如果结点是电话,权值是线路造价
  • MST的意义是什么?
    在这里插入图片描述

求最小生成树:Prim算法 “加点法”

  • 在加权图中选出n - 1条边来构成一颗生成树,且它们权值之和最小,用什么策略选?
  • 每次在连接已完成结点和未完成结点的边中,选一条权值最小的,重复n - 1遍
  • 算法利用了贪心思想:选择局部最优
    在这里插入图片描述

Key 1:Prim算法是 加点法,逐步增加n - 1个点来形成MST
Key 2:Prim算法每次加点满足 1)这个点所属边的权值最小 2)加点不会形成环

迪杰斯特拉算法

单源点最短路径:Dijkstra算法

  • 加权图中求从一个顶点s出发到图上其他各点的最短距离
  • |V| = n,算法循环n - 1次,每次循环中:
    • 找到未完成结点中,s -> t距离最短的t,将t标注为已完成
    • 以t为中转更新s至t的邻居们的距离
      在这里插入图片描述

AOV图求拓扑排序

  • 假设以下有向图中的顶点表示不同课程,边表示课程之间的依赖关系,请问能否顺利毕业?

  • 能,因为顺着箭头走,不会产生环 => 有向无环图(DAG)

  • 一个可行的选课顺序:1,2,5,4,3,6,7,8,9
    在这里插入图片描述

  • 上面这种网络就叫做AOV网络:

    • Activity on vertices
    • 图中的结点表示活动
  • 如何求拓扑排序?

  • 简而言之,把当前能上 (没有前序依赖, 入度为0) 的课上了,然后将此与之有关的依赖关系删掉

  • 如果当前还有剩余未上的课,然而没有能上的课了 => 无法拓扑排序 => 有向图存在环

步骤:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最后得到一个可行的选课顺序:1,2,5,4,3,6,7,8,9

AOE网络求解关键路径

AOE网络、关键活动、关键路径

  • 设计师、架构师,前端、后端和测试五人共同开发一个网站:
    • 设计师设计好前端才能开工,架构师搭好框架后端才能开工,前后端都完成了测试才能开工
    • 设计师需要三天,架构师需要五天,前端需要四天,后端需要八天,测试需要两天
  • 至少需要多少天?哪些人比较关键?
    • 至少需要15天
    • 架构师、后端和测试很关键,如果他们拖延了,整个项目必定延期,他们 必须在前序工作完成后立即开始
    • 设计师和前端就算拖延了也不一定导致项目延期,可以先摸摸鱼再开工
  • 因此架构师、后端和测试就称为关键活动s -> 2 -> 3 -> e就称为关键路径
    在这里插入图片描述
  • AOE网络:
    • Activity on edges
    • 边表示活动
    • 结点表示状态
    • 有一个起点和一个终点
  • 考试会给一个AOE网络,求关键活动和关键路径
  • 方法:求每个结点的 最早 / 最晚发生时间 vE和vL,以及每个活动的 最早 / 最晚开始时间 eE和eL
    • 按照拓扑排序,求出所有结点的vE
    • 令终点的vL = vE,按照拓扑排序的逆序,求出所有结点的vL
    • 所有活动(边)的eE等于其起点的vE (一个活动最早在起点状态达成后即可开始)
    • 所有活动(边)的eL等于其终点的vL - wi(也可以拖到最后一刻再开始)
      在这里插入图片描述
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Len1Mi

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

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

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

打赏作者

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

抵扣说明:

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

余额充值