如要查看本人写的有关图的全部代码,可以在本人的gitee中的C语言学习仓库里找MyGraph文件。我的Gitee 顺手点个收藏吧
本文用到了图的邻接矩阵存储、图的邻接表存储、图的边集数组存储,具体实现可以参考我的上一篇有关图存储结构的文章图的五种存储结构及其C语言实现
本文参考了《大话数据结构》 清华大学出版社 程杰著,个人认为这本书讲的还不错,大家有兴趣的话可以去看看。
🌅一、图的遍历
从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历。
🛕1.深度优先遍历
深度有限遍历的思想就是先从一个结点v出发,我先尽力的沿着一个方向跑,跑不下去了或者回到原来的结点,那么我们就回到上一层然后沿着另一个方向跑,依次递归,直到图中与v有路径连通的结点都被访问,如果此时图中还有结点未被访问,那么我们就选取图中另一个未被访问过的顶点作为起始点,重复上述过程,直到图中所有顶点都被访问位置。
🎡1.1 邻接矩阵法的深度优先遍历
//邻接矩阵的深度有限遍历
bool visited[MAXSIZE];//标记这个顶点有没有被访问过
void MGDFS(MGraph G, int i);//深度优先算法的递归函数
void MGDFSTravese(MGraph G);//深度优先遍历函数
extern bool vistied[MAXSIZE];
void MGDFSTravese(MGraph G)
{
for (int i = 0; i < G.numNodes; i++)
{
visited[i] = false;
}
printf("深度优先遍历的结果为:\n");
for (int i = 0; i < G.numNodes; i++)
{
if (visited[i] != true)
{
MGDFS(G, i);
}
}
printf("\n");
}
void MGDFS(MGraph G, int i)
{
int j;
visited[i] = true;
printf("%s ", G.vexs[i]);
for (int j = 0; j < G.numNodes; j++)
{
if (G.arr[i][j] == 1 && visited[j] != true)
{
MGDFS(G, j);
}
}
}
void test1()
{
MGraph G;
createMGraphwithw(&G);
MGDFSTravese(G);
}
🌇1.2 邻接表法的深度优先遍历
//邻接表法的深度优先遍历
bool visited1[MAXSIZE];
void ADJLDFS(GraphAdjList G, int i);
void ADJDFSTraverse(GraphAdjList G);
extern bool visited1[MAXSIZE];
void ADJDFSTraverse(GraphAdjList G)
{
for (int i = 0; i < G.numNode; i++)
{
visited1[i] = false;
}
printf("深度优先遍历的结果是:");
for (int i = 0; i < G.numNode; i++)
{
if (visited1[i] != true)
{
ADJLDFS(G, i);
}
}
printf("\n");
}
void ADJLDFS(GraphAdjList G, int i)
{
EdgeNode* p;
visited1[i] = true;
p = G.adjlist[i].firstedgenode;
printf("%s ", G.adjlist[i].str);
while (p != NULL)
{
if (visited1[p->adjvex] != true)
{
ADJLDFS(G, p->adjvex);
}
p = p->next;//回到这个结点后走下一个方向
}
}
void test2()
{
GraphAdjList G;
createAdjlistGraph(&G);
printAdjlistGraph(G);
ADJDFSTraverse(G);
}
对比这两种存储结构,如果是邻接矩阵法,那么最坏情况需要遍历整个矩阵,时间复杂度是O(N^2);如果是邻接表法,时间复杂度是O(N+e),其中e是边的个数,可见邻接表法的存储结构还是优化了很多的。
🕍2.广度优先遍历
广度优先遍历的思想类似于树的层序遍历,先访问一个结点,然后访问所有和他相连的结点,然后访问和他相连的结点中第一个子结点,然后对这个结点重复上述过程,直到所有结点都被访问为止。
这种思想很适合借助队列这种只能在头出只能在尾入的结构来实现,首先第一个结点的下标入队列,然后把它打印一下,记录我们已经访问过这个结点,进行一个循环,把Q中首个元素弹出到i中,然后访问所有和i相连的结点,并且把他们标记为访问过了,然后把他们都入队,然后重复循环,直到队列为空位置。直观的理解这一套过程下来会把这个结点所在的连通图都进行广度有限遍历,但是图不一定都是连通图,我们可以再在外层套一个循环,遍历每个结点,找到下一个连通分量中的未被标记的结点,然后进行广度优先遍历。
为了实现广度有限遍历函数,我们需要一些有关队列的知识。
🏰2.1 队列的定义
队列是一种先进先出的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。
🎢2.2 队列的顺序存储——循环队列
普通的顺序存储队列要么涉及出队时要频繁移动队列,要么要空出数组位置导致空间浪费,这种情况称为假溢出。
把队列的这种头尾相接的顺序存储结构称为循环队列。
为解决循环队列的头结点和尾结点相等时不知道是满还是空的问题,我们空出一个数组元素不用,也就是说当
(
r
e
a
r
+
1
)
m
o
d
(
M
A
X
S
I
Z
E
)
=
=
f
r
o
n
t
(rear+1)mod(MAXSIZE)==front
(rear+1)mod(MAXSIZE)==front
时,队列就满了,这样就解决了rear==front的时候判断不了是空还是满的问题了。
通用的计算循环队列长度的公式如下:
(
r
e
a
r
−
f
r
o
n
t
+
M
A
X
S
I
Z
E
)
m
o
d
(
M
A
X
S
I
Z
E
)
(rear-front+MAXSIZE)mod(MAXSIZE)
(rear−front+MAXSIZE)mod(MAXSIZE)
#include <stdio.h>
#include <stdlib.h>
//循环队列
#define ARRSIZE 100
typedef int QElemType;
typedef struct {
QElemType data[ARRSIZE];
int front;
int rear;
}SqQueue;
//初始化队列
void InitSqQueue(SqQueue* Q);
//返回队列中的元素个数 也就是当前队列长度
int SqQueueLength(SqQueue Q);
//入队操作
void EnSqQueue(SqQueue* Q, QElemType e);
//出队操作
void DeSqQueue(SqQueue* Q, QElemType* e);
#include "Queue.h"
void InitSqQueue(SqQueue* Q)
{
Q->front = Q->rear = 0;
}
int SqQueueLength(SqQueue Q)
{
return (Q.rear - Q.front + ARRSIZE) % ARRSIZE;
}
void EnSqQueue(SqQueue* Q, QElemType e)
{
if ((Q->rear + 1) % ARRSIZE == Q->front)
{
printf("队列已满\n");
return;
}
Q->data[Q->rear] = e;//队尾进入
Q->rear = (Q->rear + 1) % ARRSIZE;//尾指针移动
}
void DeSqQueue(SqQueue* Q, QElemType* e)
{
if (Q->rear == Q->front)
{
printf("队列已空\n");
return;
}
*e = Q->data[Q->front];
Q->front = (Q->front + 1) % ARRSIZE;
}
🪂2.3 队列的链式存储
//链式队列
typedef struct Node {
QElemType data;
struct Node* next;
}QNode,*Queueptr;
typedef struct {
Queueptr front, rear;
}LinkQueue;
//初始化队列
void InitLinkQueue(LinkQueue* Q);
//插入队列元素
void EnLinkQueue(LinkQueue* Q, QElemType e);
//出队列操作
void DeLinkQueue(LinkQueue* Q, QElemType* e);
//销毁队列
void destroyLinkQueue(LinkQueue* Q);
//队列长度
int LinkQueueLength(LinkQueue Q);
void InitLinkQueue(LinkQueue* Q)
{
QNode* s = (QNode*)malloc(sizeof(QNode));
if (s == NULL)
{
printf("内存溢出\n");
exit(-1);
}
Q->front = Q->rear = s;
}
void EnLinkQueue(LinkQueue* Q, QElemType e)
{
QNode* s = (QNode*)malloc(sizeof(QNode));
if (s == NULL)
{
printf("内存溢出\n");
exit(-1);
}
s->data = e;
s->next = NULL;
Q->rear->next = s;
Q->rear = s;//移动尾指针
}
void DeLinkQueue(LinkQueue* Q, QElemType* e)
{
Queueptr p;
if (Q->front == Q->rear)
{
printf("队列为空\n");
return;
}
p = Q->front->next;
*e = p->data;
Q->front->next = p->next;
if (Q->rear == p)
{
Q->rear = Q->front;
}
free(p);
}
void destroyLinkQueue(LinkQueue* Q)
{
QNode* p = Q->front->next;
QNode* q;
while (p != NULL)
{
q = p;
p = p->next;
free(q);
}
free(Q->front);
}
🎇2.4 邻接矩阵法的广度优先遍历
extern bool visited2[MAXSIZE];
void MGBFSTraverse(MGraph G)
{
LinkQueue Q;
for (int i = 0; i < G.numNodes; i++)
{
visited2[i] = false;
}
InitLinkQueue(&Q);
printf("广度优先遍历的结果是:");
for (int i = 0; i < G.numNodes; i++)
{
if (visited2[i] != true)
{
visited2[i] = true;
printf("%s ", G.vexs[i]);
EnLinkQueue(&Q, i);//访问过的元素入队列 以待未来出列来访问这个结点的下一层结点
while (LinkQueueLength(Q) != 0)
{
DeLinkQueue(&Q, &i);//出队列的顶点找它的相关联的顶点
for (int j = 0; j < G.numNodes; j++)
{
if (G.arr[i][j] == 1 && visited2[j] != true)
{
printf("%s ", G.vexs[j]);
visited2[j] = true;
EnLinkQueue(&Q, j);
}
}
}
}
}
printf("\n");
}
void test1()
{
MGraph G;
createMGraphwithw(&G);
MGBFSTraverse(G);
}
🚇2.5 邻接表法广度优先遍历
extern bool visited3[MAXSIZE];
void ADJBFSTraverse(GraphAdjList G)
{
for (int i = 0; i < G.numNode; i++)
{
visited3[i] = false;
}
SqQueue Q;
InitSqQueue(&Q);
printf("广度优先遍历的结果是:");
for (int i = 0; i < G.numNode; i++)
{
if (visited3[i] != true)
{
visited3[i] = true;
printf("%s ", G.adjlist[i].str);
EnSqQueue(&Q, i);
while (SqQueueLength(Q) != 0)
{
DeSqQueue(&Q, &i);
EdgeNode* p = G.adjlist[i].firstedgenode;
while (p != NULL)
{
if (visited3[p->adjvex] != true)
{
visited3[p->adjvex] = true;
printf("%s ", G.adjlist[p->adjvex].str);
EnSqQueue(&Q, p->adjvex);
}
p = p->next;
}
}
}
}
}
void test2()
{
GraphAdjList G;
createAdjlistGraph(&G);
ADJBFSTraverse(G);
}
深度优先遍历适合目标比较明确,以找到目标为主要目的的情况;广度优先更适合在不断扩大遍历范围时找到相对最优解的情况。
🌁二、最小生成树问题
构造连通网的最小代价(最小权重和)生成树的问题叫做最小生成树问题。
🕋1. Prim算法
prim的算法的原理是我们首先从一个顶点出发,把这个结点入树,把与这个点相连所有顶点的权值中选最小权值的节点,然后让它入树,然后找这个树与树外其他顶点的所连边的最小权值边,然后把那个点也入树,重复上述过程,直到所有结点都入树为止。
注意,我们这个过程通过把顶点入树,并且搜索权值过程中入树的顶点是不会再参与最小权值搜索的过程了,这避免了找树中的结点构成的边导致形成环的问题。
prim算法更加关心从点出发的解决最小生成树的问题。
void MinSpanTree_PrimMGraph(MGraph G)
{
int min;//用来在每轮循环中存当前最小值
int lowweight[MAXSIZE];
//用来存每轮循环中 每个顶点和当前生成树中的结点的一堆边的权值的最小值
//当lowweight[i]=0的时候,表面这个结点已经加入我们的生成树了
int adjvex[MAXSIZE];
//用来表示这个最小边是和哪个结点相连的(下标代表边的一个顶点,数组值代表边的另一个顶点
lowweight[0] = 0;//表明从v0开始构建生成树
adjvex[0] = 0;
for (int i = 1; i < G.numNodes; i++)
{
lowweight[i] = G.arr[0][i];//赋予权值
adjvex[i] = 0;//初始化成每个结点现在与生成树v0的连线
}
printf("构成此最小生成树的边有:");
for (int i = 1; i < G.numNodes; i++)
{
min = INF;
int j = 1;
int k;
//找出与当前树最近的点的下标
while (j < G.numNodes)
{
if (lowweight[j] != 0 && lowweight[j] < min)
{
min = lowweight[j];
k = j;//k等于当前与树相连边中的最小权值的点,并且这个点还不在树中保证不会形成环
}
j++;
}
printf("(%s,%s) ", G.vexs[adjvex[k]], G.vexs[k]);
//当前最小权值数组中,adjvex[k]代表与k相连的点
lowweight[k] = 0;//k入树
//因为k入树了 所以要更新树和所有顶点的权值与最小权值数组
//也就是看看经过k能不能使路径更近
//更新adjvex数组和lowweight数组
for (j = 1; j < G.numNodes; j++)
{
//对没入树的进行遍历
if (lowweight[j] != 0 && G.arr[k][j] < lowweight[j])
{
lowweight[j] = G.arr[k][j];
adjvex[j] = k;
}
}
}
}
void test1()
{
MGraph G;
createMGraphwithw(&G);
MinSpanTree_PrimMGraph(G);
}
上面代码的时间复杂度是O(n^2),此算法还可以优化,参考算法导论第六部分图算法的23.2节。
🏖️2. Kruskal算法
Kruskal算法是更加关注从边集的角度生成最小生成树的一种算法,它会在每一轮选择最小权值的边进行入树,并且利用parent数组和find函数来判断这个边会不会使得我们已有的树形成环,然后遍历所有的边,就形成了最小生成树。
这其中真正核心的算法就是如何判断新加的边是否会形成树,这里我们的考虑方法是利用一个数组parent,首先parent数组全部初始化成0,表明任何边都还没有入树,当我们存其他边入树的时候,首先用find函数找到起点在树中的根,如何判断这个点是树的根呢?我们把起点放到find函数里头找,如果找到parent[f]=0的时候就返回,parent[f]=0表示我们找到了我们这个树的边集合到达了端点暂时结束。如果两个端点在这个它所在的边集合的端点不相同,说明他们来自不同的树,所以可以把这两个点加入我们的树,打印这条边,并且让parent[n]=m,把起点的树的端点值改为m,表明这个边入树;否则说明它们现在在同一个树中,则跳过这个端点。
void MGraphchangeEdge(MGraph G,Edge* edge)
{
int k = 0;
for (int i = 0; i < G.numNodes; i++)
{
for (int j = i; j < G.numNodes; j++)
{
if (G.arr[i][j] != INF && G.arr[i][j] != 0)
{
edge[k].weight = G.arr[i][j];
edge[k].start = i;
edge[k].end = j;
k++;
}
}
}
}
int cmpbyweight(Edge* x, Edge* y)
{
return x->weight - y->weight;
}
void MinSpanTree_KruskalMGraph(MGraph G)
{
Edge edge[MAXSIZE];
MGraphchangeEdge(G, edge);
qsort(edge, G.numEdges, sizeof(Edge), cmpbyweight);
int parent[MAXSIZE] = { 0 };
printf("最小生成树的组成边如下:\n");
for (int i = 0; i < G.numEdges; i++)
{
int m = findparent(edge[i].start, parent);
int n = findparent(edge[i].end, parent);
if (m != n)
{
parent[m] = n;
printf("边(%s,%s) 对应权值%d\n", G.vexs[edge[i].start], G.vexs[edge[i].end], edge[i].weight);
}
}
}
int findparent(int f, int* parent)
{
while (parent[f] != 0)
{
f = parent[f];
}
return f;
}
void test1()
{
MGraph G;
createMGraphwithw(&G);
MinSpanTree_KruskalMGraph(G);
}
🏟️三、最短路径
对于网图来说,最短路径,是指两顶点之间经过的边上权值之和最小的路径,并且我们称路径上的第一个顶点是源点,最后一个顶点是终点。
🗻1.Dijkstra算法
每次都找离我们已经形成的最短路最近的点加入最短路,然后更新其他点到起始点的距离(通过经过我们新加的点走),然后再找最近的点加入最短路,是一种动态规划的思想。
//求解最短路径的Dijkstra算法
typedef int Patharc[MAXSIZE];//这个数组用来存储vo到v最短路径p[v]表示v的前继
typedef int ShortPath[MAXSIZE];//存到各点的最短路径的权值和
void ShortestPath_Dijkstra(MGraph G, int v0, Patharc P, ShortPath D);
void ShortestPath_Dijkstra(MGraph G, int v0, Patharc P, ShortPath D)
{
int final[MAXSIZE] = { 0 };
final[v0] = 1;
for (int v = 0; v < G.numNodes; v++)
{
D[v] = G.arr[v0][v];
P[v] = -1;//最短路径数组初始化为-1
}
int nums = 0;
for (int v = 0; v < G.numNodes; v++)
{
int min = INF;
int k = 0;
for (int w = 0; w < G.numNodes; w++)//找离v0最近的顶点
{
if (final[w] == 0 && D[w] < min)
{
min = D[w];
k = w;
}
}
final[k] = 1;
if (nums == 0)
{
P[k] = v0;
}
for (int w = 0; w < G.numNodes; w++)//因为vk加入了v0
//接下来我们比较其他点通过vk走到v0的距离与原本v0走到vk的距离
//看哪个小 更新D数组
{
if (final[w] == 0 && (min + G.arr[k][w] < D[w]))
{
D[w] = min + G.arr[k][w];
P[w] = k;
nums++;
}
}
}
}
void test1()
{
MGraph G;
createMGraphwithw(&G);
Patharc p;
ShortPath D;
ShortestPath_Dijkstra(G, 0, p, D);
for (int i = 1; i < G.numNodes; i++)
{
int k = i;
while (k != 0)
{
printf("%s->%s ",G.vexs[k], G.vexs[p[k]]);
k = p[k];
}
printf("\n权值为%d\n",D[i]);
}
}
int main()
{
test1();
}
这个算法的时间复杂度是o(n^2).
🧆2.Floyd算法
Floyd算法可以帮助我们找到每个顶点到任何顶点的最短距离,他的算法思想十分简洁,最外层循环作为一个中转循环,看看我们当前两个点的距离再经过新的中转点的距离是否比直接从我们的顶点到那个点近,如果近,则更新距离矩阵D且把前继矩阵P的那个点更为中转点在矩阵中前继元素。
//求解最短路的Floyd算法
typedef int Patharc1[MAXSIZE][MAXSIZE];//最短距离矩阵
typedef int ShortPath1[MAXSIZE][MAXSIZE];//存储最短路径的前继矩阵
void ShortestPath_Floyd(MGraph G, Patharc1 p, ShortPath1 D);
void printshortestpath_Floyd(MGraph G, Patharc1 p, ShortPath1 D);
void ShortestPath_Floyd(MGraph G, Patharc1 p, ShortPath1 D)
{
//初始化 把距离矩阵D初始化为邻接矩阵G 把最短路矩阵p的每个前继都写成他自己
int v, w, k;
for (v = 0; v < G.numNodes; v++)
{
for (w = 0; w < G.numNodes; w++)
{
D[v][w] = G.arr[v][w];
p[v][w] = w;
}
}
for (k = 0; k < G.numNodes; k++)
{
for (v = 0; v < G.numNodes; v++)
{
for (w = 0; w < G.numNodes; w++)
{
//如果经过中转距离更近
if (D[v][w] > D[v][k] + D[k][w])
{
D[v][w] = D[v][k] + D[k][w];//更新距离
p[v][w] = p[v][k];//路径设计为要经过下标为k的结点
}
}
}
}
}
void printshortestpath_Floyd(MGraph G, Patharc1 p, ShortPath1 D)
{
int k = 0;
printf("各点的最短路径如下:\n");
for (int v = 0; v < G.numNodes; v++)
{
for (int w = v+1; w < G.numNodes; w++)
{
printf("%s->%s weight %d\n", G.vexs[v], G.vexs[w], D[v][w]);
k = p[v][w];
printf("path: %s", G.vexs[v]);
while (k != w)
{
printf("->%s", G.vexs[k]);
k = p[k][w];
}
printf("->%s\n", G.vexs[w]);
}
printf("\n");
}
}
🍔四、拓扑排序
🥙1.拓扑排序介绍
🍝1.1 AOV网
在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为AOV网。
AOV网中不能存在回路,否则优先关系就被破坏了。
🎂1.2 拓扑序列
设G=(V,E)是一个具有n个顶点的有向图,V中的一个顶点序列v1,v2,…,vn,若满足从顶点vi到vj有一条路径,顶点序列中vi必须在vj的前面,我们称这样的一个顶点序列叫做拓扑序列。
🥧1.3 拓扑排序
所谓拓扑排序就是对一个有向图构造他的拓扑序列的过程。构造过程中如果拓扑序列中有全部的顶点,那么说明此有向图无回路,是AOV图,否则说明此有向图有回路。
🥛2.栈
我们求解拓扑排序的基本思路是首先找一个入度为0的顶点,然后输出这个顶点,然后去掉这个顶点和以这个顶点为起点的弧,然后再次寻找度为0的顶点。
栈是仅限在表尾进行插入和删除的线性表,我们把允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈。栈又称为后进先出的线性表,简称LIFO结构。
为了防止出现我们每次寻找入度为0的顶点都要遍历一遍所有顶点的问题,我们可以使用栈这种结构,每次把入度为0的节点都先入栈,然后从栈中取出栈顶元素,打印此元素,然后遍历它的邻接点,让他们的入度都减1,然后看看他们的入度如果为0了就让他们入栈,然后重复上述过程,直到栈中无元素为止。
🧊2.1 栈的顺序存储
🍛2.1.1 静态顺序栈
//静态的顺序栈
typedef struct {
StackElement data[MAXNUMS];
int top;
}Sqstack;
void InitSqstack(Sqstack* s);
void pushSqstack(Sqstack* stack, StackElement e);
void popSqstack(Sqstack* stack, StackElement* e);
void InitSqstack(Sqstack* s)
{
s->top = -1;
}
void pushSqstack(Sqstack* stack, StackElement e)
{
if (stack->top == MAXNUMS - 1)
{
printf("栈满了 请调整MAXNUMS\n");
return;
}
stack->data[++stack->top] = e;
}
void popSqstack(Sqstack* stack, StackElement* e)
{
if (stack->top == -1)
{
printf("栈为空\n");
return;
}
*e = stack->data[stack->top--];
}
🍿2.1.2 动态顺序栈
//动态顺序栈
typedef struct {
StackElement* data;
int top;
int size;
}SqStack;
void InitSqStack(SqStack* s);
void checkcapcity(SqStack* s);
void pushSqStack(SqStack* s, StackElement e);
void popSqStack(SqStack* s, StackElement* e);
void InitSqStack(SqStack* s)
{
StackElement* tmp = (StackElement*)malloc(M * sizeof(StackElement));
if (tmp == NULL)
{
printf("malloc fault\n");
exit(-1);
}
s->data = tmp;
s->top = -1;
s->size = M;
}
void checkcapcity(SqStack* s)
{
if (s->top + 1 == s->size)
{
StackElement* tmp = (StackElement*)realloc(s->data, 2 * s->size * sizeof(StackElement));
if (tmp == NULL)
{
printf("realloc fault\n");
exit(-1);
}
s->data = tmp;
s->size = 2 * s->size;
}
}
void pushSqStack(SqStack* s, StackElement e)
{
checkcapcity(s);
s->data[++s->top] = e;
}
void popSqStack(SqStack* s, StackElement* e)
{
if (s->top == -1)
{
printf("empty stack\n");
return;
}
*e = s->data[s->top--];
}
🍙2.1.3 静态两栈共享空间
//静态双栈共享空间
typedef struct {
StackElement data[MAXNUMS];
int top1;
int top2;
}doubleSqstack;
void InitdoubleSqstack(doubleSqstack* s);
void PushdoubleSqstack(doubleSqstack* s, StackElement e, int stacknumber);
void PopdoubleSqstack(doubleSqstack* s, StackElement* e, int stacknumber);
void InitdoubleSqstack(doubleSqstack* s)
{
s->top1 = -1;
s->top2 = MAXNUMS;
}
void PushdoubleSqstack(doubleSqstack* s, StackElement e, int stacknumber)
{
if (s->top1 + 1 == s->top2)
{
printf("stack full\n");
return;
}
if (stacknumber == 1)
{
s->data[++s->top1] = e;
}
else if (stacknumber == 2)
{
s->data[--s->top2] = e;
}
}
void PopdoubleSqstack(doubleSqstack* s, StackElement* e, int stacknumber)
{
if (stacknumber == 1)
{
if (s->top1 == -1)
{
printf("stack1 empty\n");
return;
}
*e = s->data[s->top1--];
}
else if (stacknumber == 2)
{
if (s->top2 == MAXNUMS)
{
printf("stack2 empty\n");
return;
}
*e = s->data[s->top2++];
}
}
🍲2.2 栈的链式存储
以单链表的头结点作为栈的top指针,入栈等价于对单链表做头插,出栈等价于对单链表做头删。
//栈的链式存储
typedef struct StackNode {
StackElement data;
struct StackNode* next;
}StackNode;
typedef struct {
StackNode* top;
int count;
}LinkStack;
void InitLinkStack(LinkStack* s);
void PushLinkStack(LinkStack* s, StackElement e);
void PopLinkStack(LinkStack* s, StackElement* e);
void InitLinkStack(LinkStack* s)
{
s->top = NULL;
s->count = 0;
}
StackNode* createnewnode(StackElement e)
{
StackNode* ret = (StackNode*)malloc(sizeof(struct StackNode));
if (ret == NULL)
{
printf("malloc fault");
exit(-1);
}
ret->data = e;
ret->next = NULL;
return ret;
}
void PushLinkStack(LinkStack* s, StackElement e)
{
StackNode* newnode = createnewnode(e);
newnode->next = s->top;
s->top = newnode;
s->count++;
}
void PopLinkStack(LinkStack* s, StackElement* e)
{
if (s->top == NULL)
{
printf("stack empty\n");
return;
}
*e = s->top->data;
StackNode* p = s->top;
s->top = s->top->next;
s->count--;
free(p);
}
🍦3.拓扑序列算法
我们求解拓扑排序的基本思路是首先找一个入度为0的顶点(所以要先计算入度),然后输出这个顶点,然后去掉这个顶点和以这个顶点为起点的弧,然后再次寻找度为0的顶点。
这个思路中用到了去掉顶点,用更加注重顶点的邻接表法来存储图是比较好的。
为了防止出现我们每次寻找入度为0的顶点都要遍历一遍所有顶点的问题,我们可以使用栈这种结构,每次把入度为0的节点都先入栈,然后从栈中取出栈顶元素,打印此元素,然后遍历它的邻接点,让他们的入度都减1,同时检查他们的入度如果为0了就让他们入栈,然后重复上述过程,直到栈中无元素为止。
有入度的邻接表的存储结构
//邻接表法
typedef struct EdgeNode{
int adjvex;//确定这个点的下标
EdgeType w;//储存头结点到这个结点的邻接边的权值
struct EdgeNode* next;
}EdgeNode;//每个头结点的邻接链表的节点
typedef struct {
VertexType str[MAXSIZE];//结点名
int in;
EdgeNode* firstedgenode;//指向第一个邻接点
}VertexNode,AdjList[MAXSIZE];
//定义结点数组的元素,并直接把大小为MAXSIZE的结点数组这种类型定义为AdjList
typedef struct {
AdjList adjlist;
int numNode;//结点个数
int numEdge;//边的个数
}GraphAdjList;
void createAdjlistGraph(GraphAdjList* G);//以邻接表法创无向权图
void createAdjlistGraph_withdirection(GraphAdjList* G);//以邻接表法创建有向权图
void printAdjlistGraph(GraphAdjList G);
void createAdjlistGraph_withdirection(GraphAdjList* G)
{
printf("请输入图G的结点数和边数,以a空格b的形式输入:");
scanf("%d %d", &(G->numNode), &(G->numEdge));
for (int i = 0; i < G->numNode; i++)
{
printf("请输入序号为%d的结点:", i);
scanf("%s", G->adjlist[i].str);
G->adjlist[i].firstedgenode = NULL;
}
for (int k = 0; k < G->numEdge; k++)
{
int i, j;
EdgeNode* e = (EdgeNode*)malloc(sizeof(EdgeNode));
printf("请输入边(vi,vj)上的序号i和j和权值w,以i,j,w的形式输入:");
scanf("%d,%d,%d", &i, &j, &(e->w));
e->adjvex = j;
e->next = G->adjlist[i].firstedgenode;
G->adjlist[i].firstedgenode = e;
}
}
计算入度
void calculatain(GraphAdjList* G)
{
for (int i = 0; i < G->numNode; i++)
{
G->adjlist[i].in = 0;
}
for (int i = 0; i < G->numNode; i++)
{
EdgeNode* p;
for (p = G->adjlist[i].firstedgenode; p != NULL; p = p->next)
{
G->adjlist[p->adjvex].in++;
}
}
}
拓扑排序算法
int topologicalsort(GraphAdjList G)
{
calculatain(&G);
EdgeNode* p;
LinkStack stack;
int e;
int count = 0;
InitLinkStack(&stack);
printf("该图的拓扑排序为\n");
for (int i = 0; i < G.numNode; i++)
//把所有度为0的借点的下标入栈
{
if (G.adjlist[i].in == 0)
{
PushLinkStack(&stack, i);
}
}
while (stack.top != NULL)
//弹出栈中的元素,打印此结点,然后把这个结点的邻接点的入度都减1 并且如果此时这个点入度是0了就入栈
//后入栈的先出保证了打印出的下一个结点严格在前面这个节点的“后面”
{
PopLinkStack(&stack, &e);
printf("%s -> ", G.adjlist[e].str);
count++;
for (p = G.adjlist[e].firstedgenode; p != NULL; p = p->next)
{
int k = p->adjvex;
if ((--G.adjlist[k].in) == 0)
{
PushLinkStack(&stack, k);
}
}
}
//如果count等于结点数 说明所有的结点都在拓扑排序中 说明这个图是AOV网
if (count == G.numNode)
{
return 0;
}
else
{
return -1;
}
}
出栈的操作进行了n次,入度减1的操作进行了e次,所以这个算法的时间复杂度为O(n+e)。
🏟️五、关键路径
🏖️1.AOE网
在AOV网的基础上,给每个边都赋予权值,这样的网就称为AOE网,它的顶点可以代表事件(如开始、发动机完成等),它的弧代表活动,弧上的权值代表这个活动需要的时间。
🪂2.关键路径
我们把路径上各个活动的持续时间之和称为路径长度,从源点到汇点具有最大长度的路径我们称为关键路径,在关键路径上的活动称为关键活动。
关键路径的命名来源如下:因为关键路径是所有头尾相连路径中用时最长的路径,所以它是最后决定整个生产什么时候结束的关键,如果能缩短关键路径上的关键活动用时,那么就可以缩短整个生产的用时。
🏟️3.关键路径算法的原理
因为AOE网继承了AOV网的性质,所以这个网络可以一定程度上分成好多层,入度为0的一层,去掉这些入度为0的点和弧,剩下的入度为0的一层,循环往复,直到点都被去掉为止,这就完成了网络的分层,那么持续时间最长的路径一定是从上一层到下一层用时最长的活动所组成的路径,那么怎么判断这个活动是用时最长的呢?可以用活动的最早开始时间和活动的最晚开始时间的概念,如果最早开始时间和最晚开始时间相等,那么就意味着这个活动在所有到达下一层的活动中是刻不容缓的路径,如果不是用时最长的活动,那么最早开始时间可以是这层开始的时间,最晚开始时间可以往后拖,只要剩下的时间足够完成这个活动就行,但是由于这一层到下一层的用时就是持续时间最长的路径的用时,所以最早开始时间和最晚开始时间相等的活动就是关键活动。
但是因为我们要打印的是关键路径,也就是要打印边,所以我们应该更加关心每个活动的最早开始时间和最晚开始时间是否相等,为了求解活动的最早开始时间与最晚开始时间,我们要求解每个顶点的最早开始时间和最晚开始时间,由此,引入以下四个量
e
t
v
(
e
a
r
l
i
s
t
t
i
m
e
o
f
v
e
r
t
e
x
)
事
件
的
最
早
发
生
时
间
l
t
v
(
l
a
t
e
s
t
t
i
m
e
o
f
v
e
r
t
e
x
)
事
件
的
最
晚
发
生
时
间
e
t
e
(
e
a
r
l
i
s
t
t
i
m
e
o
f
e
d
g
e
)
活
动
的
最
早
开
始
时
间
l
t
e
(
e
a
r
l
i
s
t
t
i
m
e
o
f
e
d
g
e
)
活
动
的
最
晚
开
始
时
间
etv\quad(earlist\quad time\quad of\quad vertex)\quad 事件的最早发生时间\\ ltv\quad(latest\quad time\quad of\quad vertex)\quad 事件的最晚发生时间\\ ete\quad(earlist\quad time\quad of\quad edge)\quad 活动的最早开始时间\\ lte\quad(earlist\quad time\quad of\quad edge)\quad 活动的最晚开始时间\\
etv(earlisttimeofvertex)事件的最早发生时间ltv(latesttimeofvertex)事件的最晚发生时间ete(earlisttimeofedge)活动的最早开始时间lte(earlisttimeofedge)活动的最晚开始时间
-
计算etv
怎么计算事件的最早发生时间呢,想一想,我们可以这样,申请一个数组etv来保存每个顶点的最早发生时间,入度为0的第一层结点的最早发生时间就等于0,下一层结点的最早发生时间是上一层与他相连的所有结点的最早发生时间的值加上活动所用时间(边上的权值)的最大值,因为你一定要等前继的活动都做完了才能开工嘛,所以etv的求法如下:
-
计算ltv
计算最晚开始时间我们要从最后一层的结点倒着遍历,申请一个数组ltv来保存每个顶点的最晚发生时间,先把这个数组都初始化成最后一个结点的最早开始时间,然后前面每一层的最晚开始时间等于与他相连的下一层的所有顶点的最晚开始时间减去活动用时(他们两个弧的权值)的最小值,因为你得保证你最晚开工时间能让下一层最早要开始的工作能按最晚开始时间开始,所以ltv的求法如下:
这里计算ltv的时候要从末尾往前面计算,怎么实现从后往前计算呢,想到了什么!拓扑序列,我们可以设计一个栈stack2,修改原有的拓扑排序函数,stack2把拓扑序列压栈,然后弹出的时候就是从最后一层往前走的了,并且,我们可以在这个函数里完成etv[k]的计算,只要在每次遍历出栈元素e的邻接表时,如果出栈元素的最早开始时间加上权值大于当前遍历到的这个点的最早开始时间,那么就更新最早开始时间,具体实现如下:
extern int* etv; extern int* ltv; extern LinkStack stack2; int TopologicalSort(GraphAdjList G) { calculatain(&G); EdgeNode* p; LinkStack stack; int e; int count = 0; InitLinkStack(&stack); InitLinkStack(&stack2); etv = (int*)malloc(G.numNode * sizeof(int)); for (int i = 0; i < G.numNode; i++) { etv[i] = 0;//初始化顶点的最早发生时间为0 } for (int i = 0; i < G.numNode; i++) //把所有度为0的借点的下标入栈 { if (G.adjlist[i].in == 0) { PushLinkStack(&stack, i); } } while (stack.top != NULL) //弹出栈中的元素,打印此结点,然后把这个结点的邻接点的入度都减1 并且如果此时这个点入度是0了就入栈 //后入栈的先出保证了打印出的下一个结点严格在前面这个节点的“后面” { PopLinkStack(&stack, &e); PushLinkStack(&stack2, e);//stack2存拓扑序列 count++; for (p = G.adjlist[e].firstedgenode; p != NULL; p = p->next) { int k = p->adjvex; if ((--G.adjlist[k].in) == 0) { PushLinkStack(&stack, k); } //如果当前p所指的顶点的最早开始时间小于当前节点e的最早开始时间加边上的权值 //那么更新etv[k] if (etv[k] < etv[e] + p->w) { etv[k] = etv[e] + p->w; } } } //如果count等于结点数 说明所有的结点都在拓扑排序中 说明这个图是AOV网 if (count == G.numNode) { return 0; } else { return -1; } }
可以看到我们仅仅是在原有基础上把打印stack出栈元素更换为了此元素入栈stack2,新增了初始化stack2、etv和如果当前p所指的顶点的最早开始时间小于当前节点e的最早开始时间加边上的权值,那么更新etv[k]。
-
计算ete和lte
对于每个活动来说,活动的最早开始时间就等于弧左顶点的最早发生时间,活动的最晚开始时间lte等于弧右顶点的最晚发生时间减去活动的用时,因为你不能耽误下一个事件的开始,所以得最晚在事件的最晚发生时间减去活动时间的时候开始活动。
然后比较每个活动的ete和lte,如果相等则说明此活动为关键路径,打印之
我们可以把计算ltv和计算ete和lte并且打印关键路径放到一个函数里来实现
void CriticalPath(GraphAdjList G) { int e; EdgeNode* p; int k; int ete, lte; ltv = (int*)malloc(G.numNode * sizeof(int)); for (int i = 0; i < G.numNode; i++) { ltv[i] = etv[G.numNode - 1];//初始化ltv } //求解ltv 利用stack2从最后一层倒着求 while (stack2.top) { PopLinkStack(&stack2, &e); for (p = G.adjlist[e].firstedgenode; p != NULL; p = p->next) { k = p->adjvex; //如果e结点的当前最晚发生时间大于后续所连的p所指结点的最晚发生时间减去权值,则更新ltv[e] if (ltv[e] > ltv[k] - p->w) { ltv[e] = ltv[k] - p->w; } } } printf("关键路径为\n"); for (int i = 0; i < G.numNode; i++) { for (p = G.adjlist[i].firstedgenode; p; p = p->next) { ete = etv[i];//最早发生时间等于活动的左端点的时间 lte = ltv[p->adjvex] - p->w; if (ete == lte) { printf("<%s,%s> length:%d\n", G.adjlist[i].str, G.adjlist[p->adjvex].str, p->w); } } } }
🎇4.关键路径算法与测试用例
//关键路径算法
int* etv;//顶点的最早发生时间
int* ltv;//顶点的最晚发生时间
LinkStack stack2;//用来存储拓扑序列方便后续求ltv
int TopologicalSort(GraphAdjList G);//获取拓扑序列方便后续求ltv且计算etv
void CriticalPath(GraphAdjList G);//计算ltv并且比较ete和lte以输出关键路径
extern int* etv;
extern int* ltv;
extern LinkStack stack2;
int TopologicalSort(GraphAdjList G)
{
calculatain(&G);
EdgeNode* p;
LinkStack stack;
int e;
int count = 0;
InitLinkStack(&stack);
InitLinkStack(&stack2);
etv = (int*)malloc(G.numNode * sizeof(int));
for (int i = 0; i < G.numNode; i++)
{
etv[i] = 0;
}
for (int i = 0; i < G.numNode; i++)
//把所有度为0的借点的下标入栈
{
if (G.adjlist[i].in == 0)
{
PushLinkStack(&stack, i);
}
}
while (stack.top != NULL)
//弹出栈中的元素,打印此结点,然后把这个结点的邻接点的入度都减1 并且如果此时这个点入度是0了就入栈
//后入栈的先出保证了打印出的下一个结点严格在前面这个节点的“后面”
{
PopLinkStack(&stack, &e);
PushLinkStack(&stack2, e);//stack2存拓扑序列
count++;
for (p = G.adjlist[e].firstedgenode; p != NULL; p = p->next)
{
int k = p->adjvex;
if ((--G.adjlist[k].in) == 0)
{
PushLinkStack(&stack, k);
}
//如果当前p所指的顶点的最早开始时间小于当前节点e的最早开始时间加边上的权值
//那么更新etv[k]
if (etv[k] < etv[e] + p->w)
{
etv[k] = etv[e] + p->w;
}
}
}
//如果count等于结点数 说明所有的结点都在拓扑排序中 说明这个图是AOV网
if (count == G.numNode)
{
return 0;
}
else
{
return -1;
}
}
void CriticalPath(GraphAdjList G)
{
int e;
EdgeNode* p;
int k;
int ete, lte;
ltv = (int*)malloc(G.numNode * sizeof(int));
for (int i = 0; i < G.numNode; i++)
{
ltv[i] = etv[G.numNode - 1];//初始化ltv
}
//求解ltv 利用stack2从最后一层倒着求
while (stack2.top)
{
PopLinkStack(&stack2, &e);
for (p = G.adjlist[e].firstedgenode; p != NULL; p = p->next)
{
k = p->adjvex;
//如果e结点的当前最晚发生时间大于后续所连的p所指结点的最晚发生时间减去权值,则更新ltv[e]
if (ltv[e] > ltv[k] - p->w)
{
ltv[e] = ltv[k] - p->w;
}
}
}
printf("关键路径为\n");
for (int i = 0; i < G.numNode; i++)
{
for (p = G.adjlist[i].firstedgenode; p; p = p->next)
{
ete = etv[i];//最早发生时间等于活动的左端点的时间
lte = ltv[p->adjvex] - p->w;
if (ete == lte)
{
printf("<%s,%s> length:%d\n", G.adjlist[i].str, G.adjlist[p->adjvex].str, p->w);
}
}
}
}
void test9()
{
GraphAdjList G;
createAdjlistGraph_withdirection(&G);
TopologicalSort(G);
CriticalPath(G);
}
参考文献:《大话数据结构》清华大学出版社