本文会具体介绍邻接表,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.先找源点,源点到它自己的最短路径
顶点编号 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
visited是否已找到最短路径(初始为F) | T | ||||
dist最短路径的长度(初始为INF(很大的数)) | 0 | ||||
parent最短路径上的父节点(初始为-1) | -1 |
visited[start] = 1; //标记起始点已访问
dist[start] = 0; //初始点到初始点的距离为0
parent[start] = -1; //起始点 的父节点为-1
3.与源点相连的点可以确定距离(不一定是最短的,但是目前就能知道这个距离)
顶点编号 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
visited是否已找到最短路径(初始为F) | F | F | F | ||
dist最短路径的长度(初始为INF(很大的数)) | 100 | 20 | 50 | ||
parent最短路径上的父节点(初始为-1) | 0 | 0 | 0 |
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,整个图中,不会再有其他路能让它到源点的距离更近了。
我觉得这点挺重要的,一定要理解↑↑↑
顶点编号 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
visited是否已找到最短路径(初始为F) | F | T | F | ||
dist最短路径的长度(初始为INF(很大的数)) | 100 | 20 | 50 | ||
parent最短路径上的父节点(初始为-1) | 0 | 0 | 0 |
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,中转后距离变短就更新,不短就不变
顶点编号 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
visited是否已找到最短路径(初始为F) | F | F | T | F | |
dist最短路径的长度(初始为INF(很大的数)) | 50 | 30 | 20 | 40 | |
parent最短路径上的父节点(初始为-1) | 3 | 3 | 0 | 3 |
5.2 在所有未访问的节点中找dist最小的中间节点
顶点编号 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
visited是否已找到最短路径(初始为F) | F | T | F | ||
dist最短路径的长度(初始为INF(很大的数)) | 50 | 30 | 40 | ||
parent最短路径上的父节点(初始为-1) | 3 | 3 | 3 |
//找 起始点 经过中间节点 到其他可达点 的距离
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');//清空输入缓冲区
总结
之前没有重视数据结构的实验,都是草草写了就交了,这次实验助教没有给文档,反而想好好写了,从中发现自己确实有很多不足,之后要一一补回来。