📃博客主页: 小镇敲码人
💚代码仓库,欢迎访问
🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞
【ZZULI数据结构实验】之图的应用(C语言实现)
前言:本篇博客比较水,因为只涉及了一个求最短路径的算法,但是博主就是喜欢水代码及相关数据仓库自取。
这个实验需要图论算法-求最短路径的基础,详细可以看博主这篇博客。
🐠 实验内容与要求
🐠 实验前的准备工作
本次实验只考察了图论算法中的最短路径算法,一共有4个,博主在此篇博客已经详细叙述过,原理不再重复赘述。
看实验要求,我们需要求起点到终点的最短长度和最短时间,但是有一个问题就是时间可能有小数(假设路径是整数),这里博主的处理办法是:
将时间乘以10^6,在取整存入原先的数组中,这样我们保证了小数点后6位的精度,你甚至可以将时间乘以10的12次方,然后所有的数据类型都开long long
,long long
是长整型,范围可到10^18次方,然后最后打印的时候用浮点数打印,除以之前乘的那个数即可。
🐲 结构设计及函数声明
typedef struct node
{
int i;
int w;
struct node* next;
}Node;
typedef struct returnNode
{
int* pre;
int* dist;
}RN;
RN* Dijkstra2(int n, int** road, int start, int end);//最短路算法,Dijkstra算法实现,N^2
void Listpush_back(Node* Head, int i, int w);
void PrintLength(int* pre, int cur);//打印最短路径
这里对这两个结构体的作用做一下说明:
Node
类型的结构体,是一个带头的单链表,这里我们在邻接表部分会使用,链表不用我们考虑扩容,存入某个顶点的边时,我们需要把它指向的顶点和权值都存入,这时候直接头插即可。- RN类型的结构体,是我们用做返回值的一个类型,它帮助我们把最短路径(时间)数组和保存每个节点求出的最短路径(时间)的前驱顶点的数组返回到我们的测试函数中,以便于我们做进一步的操作。
🐠 函数具体实现
🐲 带头单链表尾插函数
不必多说,直接上代码。
void Listpush_back(Node* Head, int i, int w)//头插
{
//创建新节点,并初始化
Node* newnode = (Node*)malloc(sizeof(Node));
if (NULL == newnode)
{
printf("malloc failed\n");
exit(-1);
}
newnode->next = NULL;
newnode->i = i;
newnode->w = w;
//头插
newnode->next = Head->next;
Head->next = newnode;
}
🐲 求最短路径的Dijkstra算法(小堆优化版本)
这里我们使用的是优先队列(小堆)优化的版本,在图网最短连的博客中博主已经详细叙述过其原理,只不过在那里,我们是使用C++来实现的,今天我们来使用C语言实现一下:
RN* Dijkstra2(int n,int m,int** road, int start, int end)//最短路算法,Dijkstra算法实现,N^2
{
//首先我们需要初始化邻接表(每一个表使用链表来保存下一条边),因为road并不是,road只是每条边的信息
Node* G = (Node*)malloc(sizeof(Node)*n);
if (NULL == G)
{
printf("malloc failed\n");
exit(-1);
}
for (int i = 0; i < n; ++i)//初始化头节点
{
G[i].next = NULL;
G[i].i = -1;
G[i].w = -1;
}
for (int i = 0;i < m;++i)
{
int node1 = road[i][0], node2 = road[i][1],w = road[i][2];
//有向图
Listpush_back(&G[node1],node2,w);
}
RN* r1 = (int*)malloc(sizeof(RN));
if (NULL == r1)
{
printf("malloc failed\n");
exit(-1);
}
r1->dist = (int*)malloc(sizeof(int)*n);//dist数组存放起点start到每个节点之间的距离,一开始全部初始化为最大值的一半,为什么是一半呢?这里是防止溢出
if (NULL == r1->dist)
{
printf("malloc failed\n");
exit(-1);
}
for (int i = 0; i < n; ++i)
r1->dist[i] = INT_MAX / 2;
r1->pre = (int*)malloc(sizeof(int)*n);//dist数组存放起点start到每个节点之间的距离,一开始全部初始化为最大值的一半,为什么是一半呢?这里是防止溢出
if (NULL == r1->pre)
{
printf("malloc failed\n");
exit(-1);
}
for (int i = 0; i < n; ++i)
r1->pre[i] = -1;
Heap hp;//创建一个小根堆,来更快的求出每次的最短路径
HeapInit(&hp);//初始化小根堆
r1->dist[start] = 0;//start自己到自己的距离是0
r1->pre[start] = -1;//起点没有上一个节点
Pair x;//创建一个Pair变量
x.first = 0;//最短路径
x.second = start;//起点下标
HeapPush(&hp, x);
while (1)
{
int min_i = -1;
int dis_min = 0;
if (!HeapEmpty(&hp))
{
min_i = HeapTop(&hp).second;
dis_min = HeapTop(&hp).first;
HeapPop(&hp);//这个最短路径即将被用来更新其它节点,没有作用了,从优先队列中pop掉
}
if (min_i == -1)//求出起点到end的最短路径就满足要求了,退出程序
break;
if (dis_min > r1->dist[min_i])//可能出现这种情况,就是这个节点的信息之前就已经存入队列中了,但是再次更新之后没有把旧的信息删除,就会造成这种不匹配的现象,我们跳过这种不合法的情况
continue;
//用这个最短路径的点去更新其它的点
Node* cur = G[min_i].next;//让cur开始遍历min_i顶点指向的边
while(cur != NULL)
{
int newdis = cur->w + r1->dist[min_i];
if (newdis < r1->dist[cur->i])//如果比我现在的距离要小,就更新距离和上一个节点的下标
{
r1->dist[cur->i] = newdis;
r1->pre[cur->i] = min_i;
//更新pair的值,并把新的pair插入到小堆中
x.first = newdis;
x.second = cur->i;
HeapPush(&hp, x);
}
cur = cur->next;
}
}
HeapDestory(&hp);//释放堆中的空间
return r1;
}
我以画图的形式来解析一下这段代码的一些细节部分(注释也已经很详细了):
堆的数据类型声明:
🐲 打印最短路径函数
此函数使用递归实现,因为递归可以做到,从起点到终点的打印很方便。
void PrintLength(int* pre, int cur)//打印最短路径
{
if (cur == -1)
{
return;
}
PrintLength(pre, pre[cur]);//先去递归前驱节点,再回来打印
if (pre[cur] == -1)
printf("S%d", cur);
else
printf("->S%d", cur);
}
🐠 运行结果展示
🐲 测试函数
const int base = 1e6;
void Test_Dijkstra()
{
int n = 0;
printf("请输入节点个数:\n");
scanf("%d", &n);
int m = 0;
printf("请输入边的个数:\n");
scanf("%d", &m);
int start = 0, end = 0;
printf("请输入要求的最短路径的起点和终点:\n");
scanf("%d%d", &start,&end);
int** road = (int**)malloc(sizeof(int*) * m);//保存边数
if (NULL == road)
{
printf("malloc failed\n");
exit(-1);
}
for (int i = 0; i < m; ++i)//给一维数组开空间
{
road[i] = (int*)malloc(sizeof(int) * 4);
if (NULL == road[i])
{
printf("malloc failed\n");
exit(-1);
}
}
printf("输入%d个弧的起点、终点、长度(km)、以及平均速度(km/h):\n", m);
for (int i = 0; i < m; ++i)
for (int j = 0; j < 4; ++j)
scanf("%d", &road[i][j]);
RN* r1 = Dijkstra2(n,m,road, start, end);
//首先输出最短路径
printf("%d->%d的最短长度路径为:", start, end);
PrintLength(r1->pre, end);
printf("\n");
printf("最短路径长度为:%dkm\n", r1->dist[end]);
printf("dist长度表为:\n");
for (int i = 0; i < n; ++i)
printf("S%d->S%d: %dkm ", start, i, r1->dist[i]);
printf("\n");
for (int i = 0; i < m; ++i)
{
double t = 1.0 * road[i][2] / road[i][3];
printf("S%d->S%d的平均时间为:%lfmin\n", road[i][0], road[i][1], t*60);
road[i][2] = t * base;
}
printf("\n");
RN* r2 = Dijkstra2(n,m,road, start, end);
//首先输出最短路径
printf("S%d->S%d的最短时间路径为:", start, end);
PrintLength(r2->pre, end);
printf("\n");
printf("最短时间为%lfmin\n", 1.0*r2->dist[end]/base*60);
printf("dist时间表为:\n");
for (int i = 0; i < n; ++i)
printf("S%d->S%d: %lfmin ", start,i,1.0*(r2->dist[i])/base*60);
//释放空间,防止内存泄漏
free(r1->dist);
r1->dist = NULL;
free(r1->pre);
r1->pre = NULL;
free(r2->dist);
r2->dist = NULL;
free(r2->pre);
r2->pre = NULL;
}
int main()
{
Test_Dijkstra();
return 0;
}
🐲 测试结果
测试数据1:
输入:
第一行是顶点数
第二行是边数
第三行是起点到终点
第4~10行是边的数据,起点、终点、路径长度(单位km)、平均速度(单位km/h)
5
7
0 4
0 1 4 10
0 3 2 30
1 3 1 20
1 2 4 10
2 4 3 15
2 3 1 5
3 4 7 30
运行结果:
与预期结果一致。
测试数据2:
5
7
1 4
0 1 4 10
0 3 2 30
1 3 1 20
1 2 4 40
2 4 3 15
3 2 1 5
3 4 7 30
运行结果:
无穷大代表不连通。