简介:这份《数据结构课程设计-校园导航系统》文档面向计算机专业学生及数据结构课程设计指导者,围绕校园地图建模与最短路径查询这一典型题目,给出可直接参考的完整设计报告。压缩包内仅含1个doc文件,约187KB,篇幅紧凑却覆盖任务书、详细设计、调试与测试、时间复杂度分析、源程序清单与执行结果、C程序设计总结、致谢及参考文献等模块,结构贴合高校课程设计报告的常规评分要求。文档以邻接矩阵存储无向图,用顺序表保存景点名称、代号与简介,并采用Floyd算法求解任意两景点间的最短路径,同时说明景点信息查询与问路查询功能的设计思路;调试部分还记录了最短路径只能正序输出等实际问题的排查过程。目前已有67人学习,适合需要快速搭建选题框架、对照实现代码与报告写法,或查漏补缺的读者参考。
1. 从一份 .doc 拆起:校园导航系统到底能跑出什么
拿到一份《数据结构课程设计-校园导航系统》的 .doc,多数人先翻目录看章节全不全;我一般直接跳到源程序清单,看它把图存成什么、最短路怎么回溯。这份文档交付的不是能上线的导航 App,而是一套可复现的课程设计骨架:邻接矩阵存 10 个景点、28 条无向边,Floyd 算任意两点最短路径,外加按名称查景点简介的线性扫描。适合两类人:正在做数据结构课设、需要一份能编译能演示的底稿;以及想拿它改成本地地图、练手图算法的开发者。它最大的价值不在界面,而在把“图建模—最短路—路径还原”这条链路完整摊开,连调试时踩过的逆序输出坑都写进了报告。血泪经验是,这类文档真正能省时间的地方,是它把错误样本也留下来了。
2. 邻接矩阵存校园图:10 个景点、28 条边怎么落表
2.1 为什么这份设计选邻接矩阵
校园导航的图规模很小,10 个景点、28 条边,用邻接矩阵开 20×20 的 int 数组,内存占用不到 2KB,完全不需要纠结空间。邻接矩阵的好处是判断两点之间有没有边、取边权、做 Floyd 初始化都很直接,Edge[i][j]一个下标就到位。邻接表在稀疏图上更省空间,但 Floyd 本身就要一张cost[n][n]的距离矩阵,用邻接表最后还得转成矩阵,课程设计这个体量反而多一层转换。
真正要留意的是“无向图”三个字。校园道路两个方向都能走,邻接矩阵必须对称:Edge[0][1]=20的同时要有Edge[1][0]=20。原文档的边数组把两个方向都列了一遍,所以矩阵看起来没问题;如果只列单向,Floyd 算出来的路径会出现“去得了、回不来”的翻车现场,后面第 5 章会细说。
另一个选型理由是顶点信息的组织。景点不只是编号,还有名称、代号、简介,这些放在顺序表SeqList里,图只负责存边。查询景点信息时扫顺序表,查最短路时用矩阵,职责分得比较清楚。
2.2 三个结构体:顶点、边、图
原文档给了三个核心结构体,我按可编译的写法整理了一遍,字段含义没有变:
#define MaxSize 20 // 顺序表最大容量 #define MaxVertices 20 // 最大顶点数 #define MaxWeight 1000 // 无穷大边权,表示不可达 typedef struct { char name[20]; // 景点名称 int code; // 景点代号 char introduction[50]; // 景点简介 } DataType; typedef struct { DataType list[MaxSize]; int size; // 当前顶点数 } SeqList; typedef struct { SeqList vertices; // 顶点表 int Edge[MaxVertices][MaxVertices]; // 邻接矩阵 int numOfEdge; // 边数 } AdjMGraph; typedef struct { int row; int col; int weight; } RowColWeight;DataType把景点当一条记录,name用来做查询匹配,code是给用户看的短编号,introduction是简介。SeqList是典型的顺序表,size必须和实际顶点数一致,否则后面遍历会读到脏数据。AdjMGraph把顶点表和边矩阵绑在一起,传参时只传一个图对象,比散着传三个数组省心。RowColWeight是建图时的“边描述”,row、col是顶点下标,weight是路长,它只负责把平面图翻译成矩阵,不参与最短路计算。
参数上有一个容易忽略的点:MaxWeight取 1000 是课程设计里常见的“无穷大”。10 个点、单边最大 35 的场景够用,但一旦把边权改大或者景点增多,1000 可能被真实路径超过,Floyd 会把“不可达”误判成“可达但很远”。更稳的写法是0x3f3f3f3f,同时在松弛时判断加法的两个操作数是不是无穷大,避免整数溢出。
2.3 把校园平面图写成 RowColWeight 数组
顶点表按顺序放 10 个景点,下标 0 到 9 就是它们在矩阵里的位置。下面这份数据与原文档一致,只把容易引起歧义的地名换成了通用描述:
DataType a[] = { {"学生食堂", 10, "学生的主要就餐点"}, {"男生宿舍", 11, "一共有八栋"}, {"操场", 12, "塑胶跑道和人工草坪"}, {"物理楼", 13, "拥有各种实验器材"}, {"博学楼", 14, "学生上课集中地"}, {"草地", 15, "休闲娱乐的好地方"}, {"老教", 16, "学校最早的教学楼"}, {"女生宿舍", 17, "一共七栋"}, {"小门", 18, "可以通往周边生活区"}, {"图书馆", 19, "藏书五十万册"} }; RowColWeight rcw[] = { {0,1,20}, {0,2,30}, {0,3,35}, {1,0,20}, {1,3,20}, {2,0,30}, {2,3,15}, {2,4,30}, {3,0,35}, {3,1,20}, {3,2,15}, {3,4,30}, {4,2,30}, {4,3,30}, {4,5,10}, {5,4,10}, {5,6,35}, {5,8,8}, {6,5,35}, {6,7,20}, {6,8,25}, {6,9,5}, {7,6,20}, {7,8,10}, {8,5,8}, {8,6,25}, {8,7,10}, {9,6,5} }; int n = 10; // 顶点数 int e = 28; // 边描述条数这张表里每个三元组就是一条边。{0,1,20}表示下标 0 的“学生食堂”到下标 1 的“男生宿舍”路长 20。因为是无向图,{1,0,20}也列了一遍,矩阵两边都写。n=10和e=28必须和数组实际长度对上,e写多了会读越界,写少了会漏边。
提示:景点名称不要带空格。后面用
scanf("%s")读名称时,空格会被当成输入结束,带空格的景点名永远查不到。要么名称里不用空格,要么改用fgets读整行再去掉换行。
2.4 建图函数的入口参数与边界检查
建图函数负责三件事:把顶点数组拷进顺序表、把矩阵初始化成“对角线 0、其余无穷大”、按边数组写矩阵。原文档的Creat没有做边界检查,下标越界会直接踩内存,我补了一版:
void Creat(AdjMGraph *G, DataType v[], RowColWeight e[], int n, int eCount) { int i, j; if (n < 0 || n > MaxVertices) { return; } G->vertices.size = n; for (i = 0; i < n; i++) { G->vertices.list[i] = v[i]; } for (i = 0; i < MaxVertices; i++) { for (j = 0; j < MaxVertices; j++) { G->Edge[i][j] = (i == j) ? 0 : MaxWeight; } } G->numOfEdge = 0; for (i = 0; i < eCount; i++) { int r = e[i].row; int c = e[i].col; int w = e[i].weight; if (r < 0 || r >= n || c < 0 || c >= n || w < 0) { continue; } G->Edge[r][c] = w; G->Edge[c][r] = w; // 无向图补反向 G->numOfEdge++; } }入口参数G是输出图,v是顶点数组,e是边数组,n是顶点数,eCount是边描述条数。函数不返回错误码,非法边直接跳过,课程设计里够用;如果要做成可复用模块,建议返回int表示成功或失败,把越界的边打印出来。
这里有一个计数上的小坑:原文档的rcw已经把双向边各列了一次,建图时再补反向,矩阵值不变,但numOfEdge会翻倍。如果只是演示,边数不参与算法,影响不大;如果报告里要写“边数为 28”,就要么让rcw只列单向、建图补反向,要么保留双向数组、建图时不再补反向。两种约定选一种,别混着用。
3. Floyd 求任意两点最短路:三重循环与 path 回溯的逆序坑
3.1 Floyd 的状态定义与初始化
Floyd 解决的是“任意两点之间”的最短路,正好对应校园导航“游客问任意两个景点怎么走”的需求。它的状态定义不复杂:weight[i][j]表示从顶点 i 到顶点 j 的当前最短距离,path[i][j]记录这条路径上的一个中间分界点。初始化时,weight直接拷贝邻接矩阵,path全部置-1,表示 i 到 j 没有中间点。
for (i = 0; i < n; i++) { for (j = 0; j < n; j++) { weight[i][j] = cost[i][j]; path[i][j] = -1; } }cost是建图后的邻接矩阵,n是实际顶点数。注意只初始化前 n 行 n 列,不要拿MaxVertices去跑 Floyd,否则会把未使用的顶点当成真实景点,算出无意义的路径。path置-1不是随便选的,后面回溯时用它判断“还有没有中间点”。
空间上,weight和path都是 n×n 的 int 矩阵,10 个点就是 100 个 int,约 400 字节一个矩阵,两个加起来不到 1KB。原报告写 Floyd 空间复杂度 O(1),这是把辅助数组漏掉了;准确说法是 O(n²),第 6 章会把这个账重新算一遍。
3.2 三重循环里 path[i][j]=k 到底记了什么
Floyd 的核心是三层循环,最外层k是“允许经过的中间点”,内两层i、j枚举起点和终点:
for (k = 0; k < n; k++) { for (i = 0; i < n; i++) { for (j = 0; j < n; j++) { if (weight[i][k] == MaxWeight || weight[k][j] == MaxWeight) { continue; // 防止无穷大相加溢出 } int through = weight[i][k] + weight[k][j]; if (through < weight[i][j]) { weight[i][j] = through; path[i][j] = k; } } } }path[i][j] = k记录的是“i 到 j 的最短路在 k 处断开更短”,它不是前驱节点,而是分界点。这一点是理解回溯的关键:知道path[i][j]=k后,要分别去查path[i][k]和path[k][j],把两段路径拼起来,而不是顺着path一路往前找前驱。
松弛条件里的through < weight[i][j]用严格小于,不用小于等于。等于的时候换不换中间点都不影响距离,但频繁改写path会让路径还原变得不稳定,同一个查询可能两次输出不同走法。课程设计演示时,稳定输出比“多解”更重要。
那个MaxWeight判断也别省。如果weight[i][k]是 1000,weight[k][j]是 30,相加得 1030,虽然没溢出,但会把一条“不可达”的边算成 1030 的假路径;如果MaxWeight取更大的值,还可能整数溢出变成负数,直接污染最短路。先判断再加,是这类矩阵算法的基本习惯。
3.3 路径回溯:正序和逆序为什么输出不一样
原文档在调试部分专门写了这个问题:从 A 到 B 能正序输出,从 B 到 A 逆向输出就乱。根因不在 Floyd,而在回溯代码用“下标大小”判断方向。原代码里有一段if (j < k) k = m; else j = m;,它假设起点下标小于终点下标时走一套逻辑,大于时走另一套。这个假设只在部分数据上成立,换一对景点就翻车。
path记录的是分界点,正确回溯应该做分治:要打印 i 到 j 的路径,先看path[i][j]是不是-1;如果是,说明 i 和 j 直达,没有中间点;如果不是,设k=path[i][j],先打印 i 到 k 的路径,再打印 k,再打印 k 到 j 的路径。这个过程和下标谁大谁小完全无关。
void PrintPath(int i, int j, int path[][MaxVertices], AdjMGraph G) { if (path[i][j] == -1) { return; } int k = path[i][j]; PrintPath(i, k, path, G); printf("%s -> ", G.vertices.list[k].name); PrintPath(k, j, path, G); }调用时先打印起点,再调PrintPath(s, t, path, G),最后打印终点。递归深度最多等于路径上的顶点数,10 个点最多 10 层,不会栈溢出。参数i、j是起点和终点下标,path是 Floyd 算出的分界点矩阵,G用来取景点名称。这个写法正反两个方向都成立,因为分治不依赖下标顺序。
注意:如果
weight[s][t]仍然是MaxWeight,说明两点不连通,这时不要调PrintPath,否则会打印出只有起点和终点的“假路径”。先判断距离,再还原路径。
3.4 把递归回溯改成迭代的写法
递归版本好懂,但有些课程设计环境对递归不友好,或者报告里要求“非递归实现”。可以用显式栈把递归展开。思路是模拟两次递归调用:先压右段,再压左段,保证左段先处理。
void PrintPathIter(int s, int t, int path[][MaxVertices], AdjMGraph G) { int stack[MaxVertices * 2]; int top = 0; stack[top++] = s; stack[top++] = t; while (top > 0) { int j = stack[--top]; int i = stack[--top]; int k = path[i][j]; if (k == -1) { continue; } // 先压右段 k->j,再压左段 i->k,出栈时左段先处理 stack[top++] = k; stack[top++] = j; stack[top++] = i; stack[top++] = k; } }这段代码只演示“展开顺序”,真正打印时还需要在合适的时机输出景点名,否则只是把分界点走了一遍。更实用的做法是先用数组收集路径节点,再统一输出:从s开始,用递归或栈把中间点按顺序塞进route[],最后把s、中间点、t依次打印。课程设计里递归版本足够,迭代版本作为扩展思路,报告里写清楚“递归深度不超过顶点数”即可。
4. 查询与菜单落地:景点信息扫描和名称到下标映射
4.1 景点信息查询:线性扫描的边界
景点信息查询的逻辑很直白:用户输入景点名称,程序从顺序表头扫到尾,用strcmp比较,命中就输出代号和简介,扫完没命中就提示不存在。
void InfoQuery(AdjMGraph G, const char name[]) { int i; for (i = 0; i < G.vertices.size; i++) { if (strcmp(G.vertices.list[i].name, name) == 0) { printf("景点名称:%s\n", G.vertices.list[i].name); printf("景点代号:%d\n", G.vertices.list[i].code); printf("景点简介:%s\n", G.vertices.list[i].introduction); return; } } printf("未找到该景点,请检查名称。\n"); }时间复杂度 O(n),n 是景点数。10 个点扫一遍几乎无感,但边界要处理干净:G.vertices.size必须是实际顶点数,不能拿MaxSize去循环,否则会读到未初始化的name,strcmp可能越界。找到后直接return,不要继续扫,避免同名景点被后面的记录覆盖输出。
strcmp对大小写、全半角、空格都敏感。控制台输入“图书馆”和源文件里的“图书馆”看起来一样,编码不同就可能不相等。Windows 控制台默认 GBK,源文件如果是 UTF-8,中文比较会失败。课程设计演示时,最稳的办法是统一源文件和控制台编码,或者给每个景点一个英文/拼音代号,查询时用代号匹配,名称只用来显示。
4.2 名称到下标:别让 j、k 留在未初始化状态
最短路径查询要先根据两个名称找到它们在顶点表里的下标,再拿下标去查 Floyd 矩阵。原文档的Path函数用两个变量j、k接下标,但没有初始化,如果用户输入的名称不存在,j或k就是随机值,后面Floyd用这个随机下标去索引矩阵,轻则输出乱码路径,重则数组越界崩溃。
把“找下标”抽成独立函数,找不到返回-1,调用方先判断再往下走:
int FindVertex(AdjMGraph G, const char name[]) { int i; for (i = 0; i < G.vertices.size; i++) { if (strcmp(G.vertices.list[i].name, name) == 0) { return i; } } return -1; } void PathQuery(AdjMGraph G, const char from[], const char to[]) { int s = FindVertex(G, from); int t = FindVertex(G, to); if (s == -1 || t == -1) { printf("起点或终点不存在,请重新输入。\n"); return; } int weight[MaxVertices][MaxVertices]; int path[MaxVertices][MaxVertices]; Floyd(G.Edge, G.vertices.size, weight, path); if (weight[s][t] >= MaxWeight) { printf("两点之间不连通。\n"); return; } printf("最短距离:%d\n", weight[s][t]); printf("%s", G.vertices.list[s].name); PrintPath(s, t, path, G); printf("%s\n", G.vertices.list[t].name); }FindVertex的入口是图和名称,出口是下标或-1。PathQuery先做两次查找,任何一次失败都直接返回,不进入 Floyd。weight和path是局部数组,每次查询重新算,避免上一次查询的残留数据影响下一次。weight[s][t] >= MaxWeight用来判断不连通,因为 Floyd 初始化时不可达就是MaxWeight,只要没有更短的路径把它替换掉,它就还是这个值。
这里还有一个性能上的小问题:每查一次最短路都重跑一遍 Floyd,10 个点无所谓,但如果景点增加到几百个,每次查询 O(n³) 就会明显卡顿。常见做法是建图后先跑一次 Floyd,把weight和path存成全局或图结构的一部分,查询时只做下标查找和路径还原。课程设计里为了代码简单,每次重算也能接受,报告里可以把这点写成“可优化项”。
4.3 菜单驱动:递归调 menu 的栈增长问题
原文档的菜单用递归:menu()里调用Information1(),Information1()做完查询又调用menu(),Path1()也一样。用户每操作一次,调用栈就多一层,连续查询几十次后可能栈溢出。演示时不容易触发,但这是一个典型的“能跑但不可靠”的写法。
改成while循环加switch,每次操作完回到循环开头,栈深度恒定:
void Menu(AdjMGraph G) { int choice; char from[20], to[20], name[20]; while (1) { printf("\n1. 景点信息查询\n"); printf("2. 最短路径查询\n"); printf("0. 退出\n"); printf("请选择:"); if (scanf("%d", &choice) != 1) { while (getchar() != '\n'); // 清掉非法输入 continue; } if (choice == 0) { break; } switch (choice) { case 1: printf("输入景点名称:"); scanf("%19s", name); InfoQuery(G, name); break; case 2: printf("输入起点和终点:"); scanf("%19s %19s", from, to); PathQuery(G, from, to); break; default: printf("无效选项,请重新选择。\n"); } } }scanf("%19s", name)里的19是宽度限制,name数组大小是 20,留一个位置给字符串结束符\0,防止输入过长导致缓冲区溢出。scanf("%d", &choice)返回 1 表示成功读到整数,返回 0 表示输入了非数字,这时要清空缓冲区,否则下一次循环还会读到同一个非法字符,形成死循环。
菜单循环里不要混用scanf和fgets。scanf("%d")会把换行符留在缓冲区,后面如果接fgets,可能读到一个空行。要么统一用scanf,要么在切换前用getchar()把换行吃掉。课程设计里输入格式固定,统一用scanf最省事。
4.4 输入缓冲区与中文名称的处理
中文景点名在控制台里输入,最容易出的问题不是算法,而是编码。源文件用 UTF-8 保存,Windows 控制台按 GBK 解释,strcmp比较的字节序列不一样,明明看起来一样的两个字就是不相等。排查时可以在FindVertex里把每个名称的字节打印出来,对比输入和源数据的十六进制,很快就能确认是不是编码问题。
另一个问题是scanf("%s")遇到空格就停。如果景点名里有空格,比如“学生 食堂”,用户输入“学生 食堂”,程序只读到“学生”,后面“食堂”留在缓冲区,下一次查询会读错。解决办法有两个:景点名不用空格,或者改用fgets(name, sizeof(name), stdin)读整行,再用strcspn(name, "\n")把换行替换成\0。
fgets(name, sizeof(name), stdin); name[strcspn(name, "\n")] = '\0';fgets会把换行一起读进来,strcspn找到换行位置并替换成结束符。这样能读带空格的名称,但也要注意缓冲区里可能残留上一次scanf的换行,第一次调用fgets可能直接读到空行。输入逻辑尽量统一,别在一个菜单里两种读法来回切。
5. 避坑排查:这份课程设计最容易翻车的 5 个点
5.1 路径逆序输出错乱
现象:查 A 到 B 时路径节点齐全,查 B 到 A 时少一个中间点,或者顺序变成“起点—终点—中间点”。原因:回溯代码用j < k判断方向,把“下标大小”当成了“路径方向”,而path记录的是分界点,不是前驱,方向判断从根上就不成立。解决:删掉下标比较,改用递归分治或显式栈,按path[i][j]拆成i→k和k→j两段分别处理。验证时正反各查一次,把两条路径倒过来比对,节点集合应该一致。
5.2 无向图只录了单向边
现象:A 到 B 有最短路,B 到 A 却显示不连通,或者绕一大圈。原因:RowColWeight只写了{0,1,20},没写{1,0,20},邻接矩阵不对称。Floyd 虽然会尝试用中间点绕行,但绕不回来就是绕不回来。解决:建图时统一补反向G->Edge[c][r] = w,边数组只维护单向描述;如果边数组已经双向列了,建图里就不要再补,避免边数重复计数。排查时直接打印邻接矩阵,看Edge[i][j]和Edge[j][i]是否相等。
5.3 无穷大设 1000 导致路径被“截胡”
现象:改大某条边权后,最短路结果变成 1000 或一个明显偏大的数,甚至出现一条根本不存在的路径。原因:MaxWeight取 1000,Floyd 松弛时weight[i][k] + weight[k][j]可能超过 1000,如果没判断无穷大就直接相加,会把不可达的边算成“1030 的假路径”;如果MaxWeight取INT_MAX,相加还会溢出成负数,负权路径直接把最短路带偏。解决:无穷大用0x3f3f3f3f,松弛前先判断两个加数是否等于无穷大,等于就跳过。改完数据后重新跑一遍所有点对,看有没有距离大于等于无穷大的异常值。
5.4 查不到景点时下标未初始化
现象:输入一个不存在的景点名,程序有时输出一条随机路径,有时直接崩溃。原因:Path函数里j、k没有初始化为-1,strcmp没命中时它们保持随机值,Floyd 拿随机下标去索引weight[j][k],越界读写。解决:查找函数找不到就返回-1,调用方先判断s == -1 || t == -1,命中失败直接提示并返回,不进入 Floyd。测试时专门输入一个不存在的名称,看程序是提示还是继续算。
5.5 菜单递归导致多次输入后栈溢出
现象:连续查询几十次后程序卡死或异常退出,重启后又正常。原因:menu()调用查询函数,查询函数末尾又调用menu(),每操作一次栈上多一帧,长时间演示必然翻车。解决:改成while(1)循环,查询函数只负责查询,返回后由循环决定下一次操作。顺带把scanf的返回值检查加上,输入非数字时清缓冲区,避免死循环。排查时可以在菜单循环里打印一个计数器,连续操作 100 次看计数是否稳定增长,栈溢出通常伴随计数到某个值后崩溃。
6. 验收与二次扩展:测试用例、复杂度修正和换图不改算法
6.1 一组能复现的测试用例
课程设计验收不是“能跑就行”,而是“每条功能都有可复现的输入输出”。下面这组用例按功能覆盖,拿过去可以直接对着敲:
| 编号 | 输入 | 预期 | 检查点 |
|---|---|---|---|
| T1 | 查询“图书馆” | 输出代号 19 和简介 | 线性表扫描命中 |
| T2 | 查询“不存在的景点” | 提示未找到 | 查找返回 -1,不越界 |
| T3 | 最短路径 学生食堂→图书馆 | 输出有限距离和完整路径 | 路径首尾正确,节点不重复 |
| T4 | 反向 图书馆→学生食堂 | 距离与 T3 相同,路径逆序 | 回溯不依赖下标大小 |
| T5 | 把某条边权改大后重跑 T3 | 距离变大或路径绕行 | 数据与算法一致 |
| T6 | 连续查询 50 次 | 程序不崩溃 | 菜单不用递归 |
T3 和 T4 要成对跑。只测正向,回溯的逆序坑很容易漏过去;正反一比对,路径集合不一致立刻就能发现。T5 用来验证“改数据不用改算法”,把rcw里某条边的weight从 20 改成 200,重新编译运行,最短路应该跟着变。如果没变,说明改的不是实际参与建图的那份数据,或者建图函数把边写到了别的下标。
6.2 复杂度写错的地方要改
原报告里有两处复杂度写法需要修正。景点信息查询是线性扫描,时间复杂度 O(n),额外空间只用了几个循环变量,空间复杂度 O(1),不是 O(n)。Floyd 是三重的for循环,时间复杂度 O(n³);空间上weight和path各是一个 n×n 矩阵,空间复杂度 O(n²),不是 O(1)。报告里写错不致命,但答辩时被问到“辅助数组算不算空间”,答不上来就很被动。
把账算清楚还有一个好处:知道瓶颈在哪。10 个点跑 Floyd 是 1000 次内层操作,瞬间完成;景点增加到 100 个,就是 100 万次,仍然能跑;增加到 1000 个,就是 10 亿次,演示时会明显卡。真要到那个规模,要么换 Dijkstra 做单源查询,要么把 Floyd 结果缓存起来,别每次查询重算。
6.3 二次扩展:换图、换算法、加文件读入
这份代码的算法部分和景点数据是分开的,换一张校园图不需要动 Floyd。把DataType a[]和RowColWeight rcw[]抽到一个单独的data.h或者文本文件里,main只负责读数据、建图、进菜单。想加景点,就在顶点数组末尾追加一条,再把与它相连的边补进rcw,n和e跟着改。想加“景点增删”,就要维护顶点表和边矩阵的一致性:删顶点不能只把size减一,还要把矩阵里对应行和列清成MaxWeight,否则残留的边会指向一个已经不存在的下标。
算法扩展上,如果只查“从一个固定入口到所有景点”的最短路,Dijkstra 更合适,O(n²) 就能出结果,不用每次跑三重循环。如果边权可能出现负数,Floyd 本身能处理负权,但不能有负权环;校园路长不会为负,这点不用太担心。再进一步,可以把路径输出改成带距离累加的形式,每经过一条边打印“当前路段长度 / 累计距离”,演示时比只给一个总数更直观。
我自己的习惯是,每次改完图数据,先打印邻接矩阵,再跑一遍所有点对的距离,最后正反各查一条路径手工核对。从那以后每次碰 Floyd,我都强制先看path矩阵再信输出——路径还原这东西,黑匣子的时候最容易被一个下标骗过去。希望帮到你。
本文还有配套的精品资源,点击获取