图的存储与拓扑排序

图的介绍

图是用多个顶点及其组成的边构成的非线性数据结构,用来表示多对多的关系,以机场举例子,每个机场就是一个顶点,机场与机场之间构成飞行线路,整个飞行线路网就是一个图。

  • 按照方向的话可以把图分为有向图和无向图

对于无向图,图的某个顶点相连的边数就这个顶点的度。

对于有向图,指向顶点自己的边数为入度,顶点自己指出的边数为该顶点的出度,出度加入度就是这个顶点的总度数。

  • 按照有无权重的话可以分为有权图和无权图

在程序中主要有两种方式存储图这种数据结构。

图的存储------邻阶矩阵

有向图

以这个有五个顶点的有向图举例,由于有5的顶点,所以其邻阶矩阵的大小是5×5,第一行第二列元素为1表示顶点V1指向顶点V2,第一行第四列为1表示顶点V1指向顶点V4,第二行第五列为1表示顶点V2。这样一来当我们想知道顶点V1有多少给出度时,只需要看矩阵的第一行有多少个1即可,当我们想知道顶点V1有多少个入度时,只需要看第一列有多少个1即可。

在实际编程中,用二维数组表示矩阵就可以了,如果要实现有权图的话,将数组的元素修改成权重,若没有对应线段的话就修改成无穷大。

无向图

无向图其实可以看成双向图,顶点V1指向V2,顶点V2也指向V1,所以其对应矩阵为对称矩阵。

邻阶矩阵还有有一些很明显的缺点的,比如空间复杂度比较大,若有一个由n个节点组成的图用矩阵表示的话,那就需要开辟的空间了。

图的存储------邻阶表

有向图

用一个一维数组就能实现这种数据结构,数组的下标表示顶点,下标对应的元素存储着一个单链表,其单链表存储的下标表示该顶点指向的其他顶点对应的下标,例如V1就指向V2和V4,所以V1的下标元素存储的单链表就存储着下标为1的V2和下标为3的V4.

无向图

同理可以很简单的理解无向图了,每个顶点的对应的单链表存储着其顶点连接的顶点的下标。

拓扑排序介绍

拓扑排序是有向无环的专属排序,可以把大学课程看成是一个有向无环图,有向性体现在课程的学习上是有顺序的,必须学完课程A才有能力学课程B,无环表示不会出现课程的学习路径为A到B到C再到A的循环。

拓扑排序实现

程序参考豆包

常量与类型定义

cpp 复制代码
#define NumVertex 100  // 最大顶点数
#define ERROR(msg) printf("Error: %s\n", msg); exit(1)

// 顶点类型(简化为整型)
typedef int Vertex;
// 邻接表边节点
typedef struct EdgeNode {
    Vertex adjvex;          // 邻接顶点下标
    struct EdgeNode* next;  // 下一条边
} EdgeNode;
// 邻接表顶点节点
typedef struct VexNode {
    Vertex data;            // 顶点数据
    EdgeNode* firstedge;    // 第一条边
} VexNode;
// 图结构(邻接表)
typedef struct {
    VexNode adjlist[NumVertex];
    int vexnum, edgenum;    // 实际顶点数、边数
} Graph;

// 队列结构(数组实现)
typedef struct {
    Vertex data[NumVertex];
    int front, rear;
} Queue;

首先拓扑排序是对有向无环图中的每个顶点进行排序,所以先得实现有向图这种数据结构,这里用邻接表实现,用数组实现队列,队列实现邻接表,邻接表的实质就是一个Graph结构体,这个结构体包含一个由顶点组成的数组,以及用来记录顶点数和边数的变量。其中顶点的结构为结构体EdgeNode实现的链表。

队列操作实现

cpp 复制代码
Queue CreateQueue(int maxSize) {
    Queue Q;
    Q.front = Q.rear = 0;
    return Q;
}

void MakeEmpty(Queue* Q) {
    Q->front = Q->rear = 0;
}

bool IsEmpty(Queue* Q) {
    return Q->front == Q->rear;
}

void Enqueue(Vertex V, Queue* Q) {
    if ((Q->rear + 1) % NumVertex == Q->front) {
        ERROR("Queue is full");
    }
    Q->data[Q->rear] = V;
    Q->rear = (Q->rear + 1) % NumVertex;
}

Vertex Dequeue(Queue* Q) {
    if (IsEmpty(Q)) {
        ERROR("Queue is empty");
    }
    Vertex V = Q->data[Q->front];
    Q->front = (Q->front + 1) % NumVertex;
    return V;
}

void DisposeQueue(Queue* Q) {
    // 数组实现的队列无需额外释放内存,空函数即可
}

队列的主要操作是入队和出队,具体实现逻辑前面有专门的文章讲过,这里就不多赘述了。

图的创建

cpp 复制代码
/ 初始化图
void InitGraph(Graph* G, int vexnum) {
    G->vexnum = vexnum;
    G->edgenum = 0;
    for (int i = 0; i < vexnum; i++) {
        G->adjlist[i].data = i;
        G->adjlist[i].firstedge = NULL;
    }
}

// 添加有向边 V→W
void AddEdge(Graph* G, Vertex V, Vertex W) {
    EdgeNode* p = (EdgeNode*)malloc(sizeof(EdgeNode));
    p->adjvex = W;
    p->next = G->adjlist[V].firstedge;
    G->adjlist[V].firstedge = p;
    G->edgenum++;
}

// 统计每个顶点的入度
void GetIndegree(Graph* G, int Indegree[]) {
    for (int i = 0; i < G->vexnum; i++) Indegree[i] = 0;
    for (int i = 0; i < G->vexnum; i++) {
        EdgeNode* p = G->adjlist[i].firstedge;
        while (p) {
            Indegree[p->adjvex]++;
            p = p->next;
        }
    }
}

初始化图就是创建多个顶点,此时这些顶点之间没有指向关系,然后通过AddEdge函数实现不同顶点之间的指向关系,假设现在想让顶点V指向顶点W,实际上要做的就是在顶点V的链表中新增一个指向W的边节点。这里用头插法让V的链表中加上了W,p可以看成是一个W节点,只不过p的next是V的链表,这样的p就相当于W+V的链表,再让p成为V的链表就相当于在原来V的链表的第一个位置插入了W。每多一个指向关系就相当于图多一条边,这样边数就得+1。

对应每个顶点的入度(即指向这个顶点的边数)统计,先将每个顶点的入度初始化为0,然后外层遍历每个顶点,while循环统计该顶点的出边情况,将每个出边的终顶点的入度+1.

拓扑排序

cpp 复制代码
int TopNum[NumVertex];  // 存储拓扑编号(伪代码中的TopNum数组)
void Topsort(Graph G) {
    Queue Q;
    int Counter = 0;
    Vertex V, W;
    int Indegree[NumVertex];  // 入度数组

    // 1. 初始化队列
    Q = CreateQueue(NumVertex);
    MakeEmpty(&Q);

    // 2. 统计入度,入度为0的顶点入队
    GetIndegree(&G, Indegree);
    for (V = 0; V < G.vexnum; V++) {
        if (Indegree[V] == 0) {
            Enqueue(V, &Q);
        }
    }

    // 3. 处理队列中的顶点
    while (!IsEmpty(&Q)) {
        V = Dequeue(&Q);
        TopNum[V] = ++Counter;  // 分配拓扑编号

        // 遍历V的所有邻接顶点W
        EdgeNode* p = G.adjlist[V].firstedge;
        while (p) {
            W = p->adjvex;
            if (--Indegree[W] == 0) {
                Enqueue(W, &Q);
            }
            p = p->next;
        }
    }

    // 4. 检测环
    if (Counter != G.vexnum) {
        ERROR("Graph has a cycle");
    }

    // 5. 释放队列
    DisposeQueue(&Q);
}

拓扑排序就是根据指向将顶点排序出来,如果一个顶点的入度为0,那它就一个排在最前面,因为没有顶点指向它。所以先初始化一个队列,将入度为0的顶点先入队,这些顶点是排序的起点。

然后让这些入度为0的顶点出队,每出队一个顶点,先给这些顶点分别编号放入TopNum数组中,再遍历这个顶点的所有邻阶顶点,再让每一个邻阶顶点为0,此时这些邻阶顶点就是**新的起点,将它们入队重复进行刚才的操作。**这样排序就完成了,遍历结果存储在TopNum数组中。

一个实例

cpp 复制代码
int main() {
    Graph G;
    int vexnum = 5;  // 示例:5个顶点
    InitGraph(&G, vexnum);

    // 添加有向边(示例:0→1, 0→2, 1→3, 2→3, 3→4)
    AddEdge(&G, 0, 1);
    AddEdge(&G, 0, 2);
    AddEdge(&G, 1, 3);
    AddEdge(&G, 2, 3);
    AddEdge(&G, 3, 4);

    // 执行拓扑排序
    Topsort(G);

    // 输出拓扑编号
    printf("拓扑编号结果:\n");
    for (int i = 0; i < vexnum; i++) {
        printf("顶点%d → 拓扑编号%d\n", i, TopNum[i]);
    }

    return 0;
}

结果如下:

相关推荐
挖矿大亨2 小时前
C++中深拷贝与浅拷贝的原理
开发语言·c++·算法
Bruce_kaizy2 小时前
c++图论——生成树之Kruskal&Prim算法
c++·算法·图论
LYFlied3 小时前
【每日算法】LeetCode 5. 最长回文子串(动态规划)
数据结构·算法·leetcode·职场和发展·动态规划
老赵聊算法、大模型备案3 小时前
《人工智能拟人化互动服务管理暂行办法(征求意见稿)》深度解读:AI“拟人”时代迎来首个专项监管框架
人工智能·算法·安全·aigc
雪花desu3 小时前
【Hot100-Java中等】/LeetCode 128. 最长连续序列:如何打破排序思维,实现 O(N) 复杂度?
数据结构·算法·排序算法
松涛和鸣3 小时前
41、Linux 网络编程并发模型总结(select / epoll / fork / pthread)
linux·服务器·网络·网络协议·tcp/ip·算法
鹿角片ljp3 小时前
力扣26.有序数组去重:HashSet vs 双指针法
java·算法
XFF不秃头3 小时前
力扣刷题笔记-合并区间
c++·笔记·算法·leetcode
巧克力味的桃子4 小时前
学习笔记:查找数组第K小的数(去重排名)
笔记·学习·算法