图的介绍
图是用多个顶点及其组成的边构成的非线性数据结构,用来表示多对多的关系,以机场举例子,每个机场就是一个顶点,机场与机场之间构成飞行线路,整个飞行线路网就是一个图。
- 按照方向的话可以把图分为有向图和无向图
对于无向图,图的某个顶点相连的边数就这个顶点的度。
对于有向图,指向顶点自己的边数为入度,顶点自己指出的边数为该顶点的出度,出度加入度就是这个顶点的总度数。
- 按照有无权重的话可以分为有权图和无权图
在程序中主要有两种方式存储图这种数据结构。
图的存储------邻阶矩阵
有向图

以这个有五个顶点的有向图举例,由于有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;
}
结果如下: