【ZZULI数据结构实验】之图的应用(C语言实现)

在这里插入图片描述

📃博客主页: 小镇敲码人
💚代码仓库,欢迎访问
🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞


前言:本篇博客比较水,因为只涉及了一个求最短路径的算法,但是博主就是喜欢水代码及相关数据仓库自取
这个实验需要图论算法-求最短路径的基础,详细可以看博主这篇博客

🐠 实验内容与要求

在这里插入图片描述

🐠 实验前的准备工作

本次实验只考察了图论算法中的最短路径算法,一共有4个,博主在此篇博客已经详细叙述过,原理不再重复赘述。

看实验要求,我们需要求起点到终点的最短长度和最短时间,但是有一个问题就是时间可能有小数(假设路径是整数),这里博主的处理办法是:
将时间乘以10^6,在取整存入原先的数组中,这样我们保证了小数点后6位的精度,你甚至可以将时间乘以10的12次方,然后所有的数据类型都开long longlong 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);//打印最短路径

这里对这两个结构体的作用做一下说明:

  1. Node类型的结构体,是一个带头的单链表,这里我们在邻接表部分会使用,链表不用我们考虑扩容,存入某个顶点的边时,我们需要把它指向的顶点和权值都存入,这时候直接头插即可。
  2. 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

运行结果:

无穷大代表不连通。

在这里插入图片描述

  • 28
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小镇敲码人

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

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

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

打赏作者

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

抵扣说明:

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

余额充值