校园导游-c语言-邻接表-迪杰斯特拉算法

本文会具体介绍邻接表,Dijkstra算法。


前言

不是大作业,只是一次普通的实验作业,而且也没有要求,所以全凭自己发挥,我只写了最短路径。
部分代码,实验课还没结束,就不放全部代码了,学会最重要!
采用邻接表的存储方式,邻接表也不是正经的邻接表。↓↓↓

typedef struct didian {
	int id;					//地点编号
	int juli;			//到顶点的距离
	struct didian *next;	//指针
}node;

一、邻接表

图的一种存储结构,书上定义了三个结构体:顶点,边和图结构体
本来是为了使结构更清晰的,但是让我这个小白看了老半天也没怎么看明白。我在这里就以自己的理解说一下。

图的邻接表是 所有 顶点串起来的链表 的数组。
【顶点结构体】是头结点,存储顶点的信息,
【边结构体】存储的是与顶点相连的边和点的信息,
【图结构体】是将所有链表封装在一起。
在这里插入图片描述

图不是非常准确,但基本能代表我的理解,于是在本次实验中,我只采取了一个结构体(只说图的结构体),如图
在这里插入图片描述

只包含顶点编号,和其他点到该点的距离,这样把顶点和边都概括到一起。
图是定义了全局变量:

node *Map[didian_num];// 地点 数组

二、迪杰斯特拉算法

算法是真的难,我几乎把b站第一页搜到的讲迪杰斯特拉算法的视频都看了一遍,才勉强理解了那么一丢丢。
在这里插入图片描述

我的理解:
1.初始化三个数组

int visited[didian_num], dist[didian_num];
	for (int i = 0; i < didian_num; i++)
	{
		visited[i] = 0;	//是否访问过,就是是否已经找到它到初始点的最短距离
		dist[i] = INF;	//每个点到初始点的最短距离
		parent[i] = -1;	//找到最短距离后,该点到初始点的路上 经过的第1个点
	}

2.先找源点,源点到它自己的最短路径

顶点编号01234
visited是否已找到最短路径(初始为F)T
dist最短路径的长度(初始为INF(很大的数))0
parent最短路径上的父节点(初始为-1)-1
	visited[start] = 1;		//标记起始点已访问
	dist[start] = 0;		//初始点到初始点的距离为0
	parent[start] = -1;		//起始点 的父节点为-1

3.与源点相连的点可以确定距离(不一定是最短的,但是目前就能知道这个距离)

顶点编号01234
visited是否已找到最短路径(初始为F)FFF
dist最短路径的长度(初始为INF(很大的数))1002050
parent最短路径上的父节点(初始为-1)000
node *p = Map[start];
	while (p) {
		dist[p->id] = p->juli;		//其他点到初始点的距离
		parent[p->id] = start;		//走最短距离的父节点
		p = p->next;		//遍历 其他点
	}

4.在所有未访问的节点中找dist最小的中间节点(在1 3 4中找3)
为什么要找中间节点:我觉得这里并不是为了找中间节点而找中间节点,而是找到这个点(找到最短距离的点)后,它就成了中间节点,因为之后要在它的基础上找其他点的最短距离)
为什么在1 3 4 中找:我们是从一个点出发,找其他点到这个点的最短路径,所以在这个点能直接到达的点中,一定有一个最短距离。)==(如果在直达点之外有个中转点,能让源点到该点的距离更短,那么这个中转点一定是源点的直达点)

所以找3,整个图中,不会再有其他路能让它到源点的距离更近了。
我觉得这点挺重要的,一定要理解↑↑↑

顶点编号01234
visited是否已找到最短路径(初始为F)FTF
dist最短路径的长度(初始为INF(很大的数))1002050
parent最短路径上的父节点(初始为-1)000
	int middle = 0;	//中间节点
	int min_dist = INF;//中间节点:dist最小-距离起始点最近

	for (int i = 0; i < didian_num; i++)
	{
		//在所有未访问的节点中找中间节点:dist最小-距离起始点最近
		min_dist = INF;
		for (int j = 0; j < didian_num; j++)//寻找未被访问节点中 距离起始点最近的节点
		{
			if (visited[j] == 0 && dist[j] < min_dist)
			{
				min_dist = dist[j];//找到最小dist
				middle = j;			//找到中间节点
			}
		}
		visited[middle] = 1;	//标记 中间节点 已访问

5.理解了上一点之后,就很容易了,从中间点3往外找其他点的最短距离,还是先找 可以与中间点直接相连的点(与3.类似),再在所有未访问的节点中找dist最小的中间节点(与4.类似),如果又懵了,就回去看看3.4.。

5.1  与3直接相连的点有(0)1 2 4,中转后距离变短就更新,不短就不变
顶点编号01234
visited是否已找到最短路径(初始为F)FFTF
dist最短路径的长度(初始为INF(很大的数))50302040
parent最短路径上的父节点(初始为-1)3303
5.2  在所有未访问的节点中找dist最小的中间节点
顶点编号01234
visited是否已找到最短路径(初始为F)FTF
dist最短路径的长度(初始为INF(很大的数))503040
parent最短路径上的父节点(初始为-1)333
	//找 起始点 经过中间节点 到其他可达点 的距离
		for (int j = 0; j < didian_num; j++)//经过 中间节点 ,找(最短)距离
		{
			if (visited[j] == 0 && dist[middle] + getJuli(middle,j) < dist[j])//dist[middle] + 从中间点到其他点的距离 < dist[j]
			{
				dist[j] = dist[middle] + getJuli(middle, j);
				parent[j] = middle;
			}
		}

代码里的注释很详细,可以看看。

三、从文件读入邻接表

文档内容如下:
源点 距离 id 距离 id 距离 id 距离在这里插入图片描述

//先创建 地图的邻接表  通过读取文件
void InitMap() {

	FILE *fp;
	fp = fopen("ditu.txt", "r"); //r 打开一个已有的文本文件,允许读取文件。
	if (fp == NULL) {
		printf("文件打开错误!\n");
		exit(1);		//表示异常退出
	}

	int num = 0;	//记录文件里有几行,与didian_num相同//链表的总数,即文件的行数,即有几个顶点
	int first = 0;	//标记是不是头结点	0是 1否

	while (!feof(fp)) {
		node *V;
		V = (node*)malloc(sizeof(node));      //申请头结点空间 V指向这个空间

		char c = fgetc(fp);	//读取一个字符
		if (feof(fp))break;	//到文件结尾就要及时退出循环
		V->id = c - '0';	//将char型变为int型
		//printf(" id:%d ", V->id);
		V->next = NULL;

		c = fgetc(fp);
		if (feof(fp))break;
		if ((int)c == 32)	//如果是空格,说明该数据结束
		{
			c = fgetc(fp);
			if (feof(fp))break;
			V->juli = c - '0';//将char型变为int型
		}
		else {	//未遇见空格,id未结束
			while ((int)c != 32) {	//id若不是个位数,字符就会被分开读取
				V->id = V->id * 10 + (c - '0');	//遇到空格之前的字符,都是该id的字符

				c = fgetc(fp);
				if (feof(fp))break;
				//读取到空格,说明id数据结束,跳出循环
			}
			c = fgetc(fp);
			if (feof(fp))break;
			V->juli = c - '0';//将char型变为int型
		}
		printf(" id:%d \n", V->id);

		c = fgetc(fp);
		if (feof(fp))break; 
		while ((int)c != 32){	//距离若不是个位数,字符就会被分开读取
			V->juli = V->juli * 10 + (c - '0');	//遇到空格之前的字符,都是该距离的字符
			//printf(" juli:%d \n", V->juli);

			c = fgetc(fp);
			if (feof(fp))break;
			if ((int)c == 10)break;//读取到回车 或 空格,说明juli数据结束,跳出循环
		}
		printf(" juli:%d ", V->juli);//该节点结束,插入链表

		//将其他点插入 初始点之后 且逆序
		if (first == 1) { //非头结点
			V->next = Map[num]->next;
			Map[num]->next = V;
		}
		else {//头结点插入的时候 Map[num]=V

			Map[num] = V; 
			first = 1;	//头结点标记 为1 ,之后的都不是头结点
		}

		if (c == '\n')//如果是回车,说明该链表结束
		{
			num++;		//链表的总数,即文件的行数,即有几个顶点
			first = 0;	//头结点标记 为0 ,下一个数据是下一个链表的头结点
			printf("|\n");
		}
 
		if (feof(fp))break;	//到文件结尾就要及时退出循环
	}

	printf(" num:%d\n ", num);
	fclose(fp);	//关闭文件


	printf(" 输出邻接表:\n");

	for (int i = 0; i < didian_num; i++) {
		printf("id:%d ", Map[i]->id);
		printf("juli:%d ", Map[i]->juli);
		node *t = Map[i]->next;
		while (t) {

			printf(" -id:%d juli:%d ", t->id, t->juli);
			t = t->next;
		}
		printf("\n\n");
	}	
}

这么一看代码好长,还是邻接矩阵方便。

四、获取中间结点到其他节点的距离

/*
*	获取中间结点到其他节点的距离
*/
int getJuli(int middle, int j) {
	node *p = Map[middle];

	while (p) {
		if (p->id == j)  return p->juli; 	//中间点和其他点 有路,返回距离
		p = p->next;
	}
	//printf("noway!\n");
	return INF;	//中间点和其他点 没有路,返回INF
}

五、将最短路径的正序放入path

/*
*	得到最短路径
*	通过迪杰斯特拉算法得到的parent数组,能得到最短路径的倒序
*	本函数将最短路径的正序放入path
*/
void Path(int start, int end, int parent[didian_num], int* path) {

	int t;	//临时,存储父节点的id
	int n=0;	//记录最短路径上有几个点,用来动态开辟数组空间

	//先判断两点间是否有通路
	if (parent[end] != -1) {	//该点有父节点,说明它找到了到初始点的最短路径,即有通路
	
		n++;
		t = parent[end];	//t = 它的父节点
		while (t != -1) {	//它的父节点不为-1,即不是初始点 就一直往回追溯
			n++;
			t = parent[t];	//t = 它的父节点
		}

	}
	else {
		printf("No Way!");	//没有通路
		return;
	}

	int *nixu_path = (int*)malloc(sizeof(int) * n);	//动态开辟空间-逆序

	/*上边是计算 n ,下边将倒序路径 变 正序*/
	int i = 1;

	nixu_path[0] = end;	//先将终止点存入路径
	t = parent[end];	//t = 终止点的父节点
	while (t != -1) {	//它的父节点不为-1,即不是初始点 就一直往回追溯

		nixu_path[i++] = t;	//所以t都是最短路径上的点,都需要存
		t = parent[t];//t = 它的父节点
	}

	path = (int*)malloc(sizeof(int)*n);			//动态开辟空间-正序
	for (i = 0; i < n; i++) {
		path[i] = nixu_path[n-i-1];		//将nixu_path数组逆序
	}

	free(nixu_path);	//释放逆序空间


	printf("最短路径:");
	for (i = 0;  i < n; i++) {
		printf(" %d ", path[i]);
		if(i == n-1) printf("\n");
		else printf("->");
	}
	free(path);//释放空间
}

六、码代码过程中遇到的各种问题

以下都凭记忆写的,可能会有错

1.typedef 结构体 链表

typedef struct 可有可无 {
	【	
		数据域,可以写很多的信息
	】
	【
		指针域,可以到处指,只要你自己清楚,
		由指针串起来的一串结构体就是链表
	】
}struct 可有可无的)别称;

2.Malloc free,堆溢出

动态开辟空间
指针 = (强制类型转换)malloc(开辟空间的大小);

node *V;
V = (node*)malloc(sizeof(node));      
//申请头结点空间 V指向这个空间

//释放链表空间
void Free(node *head)
{
	node *p;
	while (head != NULL) {
		p = head;
		head = head->next;
		free(p);
	}
}

若没有正确释放空间,
单次运行程序可能不会出错,因为程序结束后就释放了堆区,
若有循环,则会出错。

3.warning C4172: 返回局部变量或临时变量的地址

之前在函数里这样写

int* ShortestPath_DIJ(int start) {
	int parent[didian_num]
...
	return parent;
}

提示错误,warning C4172: 返回局部变量或临时变量的地址.: parent
所以改成传参的形式

4.重定义,多次初始化变量

情况是在循环内可以这样写

while (!feof(fp)) {
		node *V;
		V = (node*)malloc(sizeof(node)); 
		...赋初值...
	}

在循环外写

node *V;
V = (node*)malloc(sizeof(node)); 
...赋初值...
node *V;
V = (node*)malloc(sizeof(node)); 
...赋初值...

会报错:重定义,多次初始化变量

5.读取文件

	FILE *fp;
	fp = fopen("ditu.txt", "r"); //r 打开一个已有的文本文件,允许读取文件。
	if (fp == NULL) {
		printf("文件打开错误!\n");
		exit(1);		//表示异常退出
	}
	while (!feof(fp)) {
		if (feof(fp))break;	//到文件结尾就要及时退出循环
	}

	fclose(fp);	//关闭文件

5.1读取一行

//函数 fgets() 从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf,并在最后追加一个 null 字符来终止字符串。
char *buff;
fgets(buff, 100, fp);  //读取一行(读取99个字符,遇到换行就结束)
//为什么嘞? fgets函数既不自带回车,还会将回车 读成单独一行???

5.2读取一字符

char c = fgetc(fp);	

将char型变为int型

id = c - '0';	//将char型变为int型

清空输入缓冲区

while ((c = getchar()) != '\n');//清空输入缓冲区

总结

之前没有重视数据结构的实验,都是草草写了就交了,这次实验助教没有给文档,反而想好好写了,从中发现自己确实有很多不足,之后要一一补回来。

  • 2
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

tfnmdmx

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

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

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

打赏作者

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

抵扣说明:

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

余额充值