简介:杭电数据结构课程设计(已通过验收)的完整实践资料,包含停车场管理问题和校园导航咨询系统两个项目。资源面向正在完成数据结构课程设计的高校学生,尤其适合需要参考完整实现与实验报告格式的读者;两个项目分别涉及栈、队列、链表、图等核心结构,并应用最短路径等算法,体现理论与实践结合。压缩包共十一个文件,大小约九百三十千字节,涵盖源码文件、头文件、实验报告、地图数据、矩阵表格、示意图以及可执行程序,覆盖代码、文档、数据与演示的完整链路。目前已有四百六十人学习,读者可获得可运行的停车场管理模拟程序与校园导航系统、完整的课程设计报告,以及各模块的代码组织与算法实现思路,便于对照验收标准优化自己的作品。
1. 这不是算法题,是“带着代码去答辩”
HDU 的数据结构课程设计,跟平时刷杭电 OJ 那道坎完全是两码事。OJ 题错了就错了,改到 AC 为止,没人问你为什么这么写;课程设计要过验收,你得拿着一个能跑的 .exe,当着老师的面演示功能,然后回答“为什么用邻接表不用邻接矩阵”“这个排序在最坏情况下是什么复杂度”“输入一万条数据会不会崩”。很多同学代码写完了,一进验收教室被问三句话就卡壳,最后得了个合格但心里没底。这篇笔记就是按“能跑、能讲、能过验收”的标准来讲的,方向是数据结构与算法落地里最常见的那套:图的遍历与最短路径、二叉树操作、排序与查找,以及一套完整的交互界面。
标题里的“通过验收”才是真正的主菜。代码只是入场券,验收看的是你的选题够不够粒度、需求分析有没有边界、测试数据能不能自圆其说、答辩时原理讲不讲得清。下面我会把我做这个课设的完整思路拆给你:怎么选题目、怎么定数据结构、代码怎么从主函数往下长出来、以及验收前那个晚上到底该检查什么。新手能照着一路做到提交,熟手可以直接跳到第 5 章看踩坑清单和答辩问法。
2. 选题与需求分析:决定你后面两个星期过得好不好
2.1 常见选题的分类与能力匹配
杭电数据结构课设的题目池每年都在换,但翻来覆去就是那几类。我按“数据结构的核心程度”帮你分个类,你看自己适合哪类。
第一类是“图的算法型”,比如校园导航、城市交通查询、迷宫求解。核心是图的存储与遍历,Dijkstra 或者 Floyd 必考其一。这类题目数据结构感最强,答辩时能讲的东西多,但代码量也最大。
第二类是“线性结构应用型”,比如学生信息管理、图书借阅管理、员工工资查询。核心是结构体数组或链表,配合多关键字排序和查找。这类题目好写,但要过得漂亮,得在“查找算法对比”和“数据量增大时的性能变化”上做文章,不能只会个冒泡排序。
第三类是“树型应用”,比如哈夫曼编码/译码、表达式求值、二叉排序树。哈夫曼是经典中的经典,因为编码过程能可视化出哈夫曼树,答辩时有图可看;表达式求值则是栈和二叉树两个知识点一起考了。
我不建议选那种“系统管理”味太重的题,比如“超市收银系统”“图书馆管理系统”。这些题目听着实用,但其实数据结构浓度不够——你说你是用顺序表还是链表?老师一问“那顺序表和链表各自的适用场景是什么”,你只能说“都可以”,这就很被动。
我当时的选题是一个“基于图的最短路径查询系统”,图的顶点是校园建筑,边是路径距离。为什么选它?因为图这个数据结构在课设里表现力最强:邻接矩阵存起来直观,Dijkstra 讲起来有条理,还能顺手做一个 DFS 遍历把“可达性查询”也囊括进来,一个题目覆盖多个考点,验收时老师可问的点多,你展示的空间也大。
2.2 拿到题目的第一个小时:把需求写成一页纸
别急着开 IDE。你拿到题目后,最该做的是把“人话需求”翻译成“数据结构语言”。我给你一个模板,照着填就行。
系统名称:校园建筑最短路径查询系统
输入:源建筑编号、目的建筑编号
输出:最短路径长度、途经建筑序列
额外功能:显示所有建筑列表、查询某建筑可直达哪些建筑
边界情况:源和目的相同、图不连通、非法编号输入
这一页纸就是你的需求说明书初稿。后面写中期报告、课程设计报告的时候,直接把这份内容扩写就行,不用再绞尽脑汁想“我做了什么”。
然后做一件事:画出数据的组织方式。用表格列出来,建筑信息放哪、边信息放哪、路径输出时用到的辅助数组放哪。这一步能帮你发现很多问题——比如你想用 Floyd 而不是 Dijkstra,那就要多一个二维路径矩阵;如果想支持按距离排序输出全部路线,那就要考虑怎么在多条路径里做比较。这些都是后面代码里绕不开的决策,现在想清楚,比写完了再推倒重来省力得多。
2.3 验收视角的需求补充:老师想在你的演示里看到什么
前面那页纸是“功能需求”,但课设验收还有一层“展示需求”。老师一天要看几十个组,不会仔细读你的代码,他主要通过几分钟的演示来判断你这个课设“做没做到位”。
所以你的需求分析里,至少要包含三个能“看得见”的点。第一,初始化过程可视化——启动程序时打印出读入了多少个顶点、多少条边,让老师确定你的数据是加载进来的,不是写死在代码里的。第二,一个足够复杂的测试用例——选一条明显不是最短的路径,让程序算出来后老师能肉眼验证结果是对的。第三,异常输入的优雅处理——故意输一个不存在的编号,程序不能崩溃,不能死循环,要输出一句人能看懂的提示。
这三条就是验收现场的高光时刻。很多同学代码没问题,败就败在演示时输入了“5”结果编号只有 0 到 3,程序直接数组越界崩了,老师眉头一皱,后面答辩你心里就慌了。这些都是你在需求分析阶段就该写进“边界情况”里的东西。
3. 核心数据结构与算法选型:把“要用什么”焊死在设计文档里
3.1 邻接矩阵还是邻接表:别只看教材怎么说
图的最短路查询系统,第一个决策就是图的存储结构。教材上会告诉你稀疏图用邻接表,稠密图用邻接矩阵。这个结论没错,但课设场景下你要多想一层。
课设的数据量就那么大,你录入 20 个建筑、40 条路,邻接矩阵也就是一个 20×20 的二维数组,400 个 int,占 1600 字节。这个量级谈性能优化没有意义。此时你应该优先考虑“哪个写起来不容易错、答辩时更好讲”。
我用的邻接矩阵。原因有三个:第一,Dijkstra 算法用邻接矩阵写,代码形态跟教材伪代码一一对应,你背得住也讲得清;第二,答辩时老师让你“把图结构画出来”,邻接矩阵直接在纸上画个方格子填 0 和 ∞ 就行,特别直观;第三,检查图的连通性、查一条边是否存在,矩阵是 O(1) 的,你演示“查询两栋建筑是否直接相连”这个功能时,代码不用绕弯子。
邻接表当然也可以。如果你选的题目是“微博关注关系分析”,那必须用邻接表——因为十万个顶点、百万条边,用矩阵就是灾难。选哪个不是看哪个高级,而是看数据长什么样。我一般建议:顶点数预期超过 100 或者边特别稀疏(不到完全图的 10%),考虑邻接表;否则就邻接矩阵,省心。
3.2 Dijkstra 的课设版本:记录路径而不只是距离
教材上的 Dijkstra 通常只求最短路径长度,用一个 dist[] 数组搞定。但课设演示需要输出完整路径,所以你还得维护一个 prev[] 数组,记录每个顶点的前驱顶点。这是课设版 Dijkstra 和算法题版 Dijkstra 最大的区别。
#define MAXVEX 20 #define INF 65535 typedef struct { char name[20]; // 建筑名称 int id; // 建筑编号 } Vertex; typedef struct { int edges[MAXVEX][MAXVEX]; // 邻接矩阵,INF 表示不连通 int numVertexes, numEdges; // 实际顶点数和边数 Vertex vexs[MAXVEX]; // 顶点数组 } MGraph; // Dijkstra 算法:求 start 到所有点的最短路径 // dist: 输出最短路径长度 // prev: 输出前驱顶点数组,用于回溯路径 void Dijkstra(MGraph *G, int start, int dist[], int prev[]) { int final[MAXVEX] = {0}; // final[i]=1 表示顶点 i 已确定最短路径 int i, j, k, min; for (i = 0; i < G->numVertexes; i++) { dist[i] = G->edges[start][i]; prev[i] = (dist[i] < INF) ? start : -1; } final[start] = 1; dist[start] = 0; for (i = 1; i < G->numVertexes; i++) { min = INF; k = -1; for (j = 0; j < G->numVertexes; j++) { if (!final[j] && dist[j] < min) { min = dist[j]; k = j; } } if (k == -1) break; // 剩余顶点不可达,提前结束 final[k] = 1; for (j = 0; j < G->numVertexes; j++) { if (!final[j] && min + G->edges[k][j] < dist[j]) { dist[j] = min + G->edges[k][j]; prev[j] = k; } } } }这段代码里最关键的设计是prev数组的初始化:prev[i] = (dist[i] < INF) ? start : -1。这里把“直接与起点相连”和“不与起点相连”两种情况区分开,不然后面回溯路径时会把不相连的顶点错误地串进来。k == -1那个 break 也是实际测试逼出来的——如果图本身不连通,没有这个判断,内层循环会把k留在 -1,下一轮final[-1]就是数组越界。你写代码时一定要加上这种防御性判断,尤其是课设这种“演示时输入不可控”的场景。
回溯路径的代码也要单独写一个函数,不要在 Dijkstra 里顺手打印,否则你的 Dijkstra 就不纯净了——它既要算距离又要做 IO,答辩时不好讲清楚。
void PrintPath(int start, int end, int prev[]) { // 用栈逆序输出前驱链,避免递归深度不确定 int stack[MAXVEX], top = 0; int cur = end; while (cur != -1) { stack[top++] = cur; if (cur == start) break; cur = prev[cur]; } // 此时栈顶是 start,依次出栈即为路径 while (top > 0) { printf("%s", vexs[stack[--top]].name); if (top > 0) printf(" -> "); } printf("\n"); }为什么用栈不用递归?因为课设程序的路径长度最长可能等于顶点数,递归层数虽然不多,但老师可能会问“如果有一千个顶点,这个递归会不会爆栈”。你用栈写,一方面展示了“用数据结构解决问题”的意识,另一方面彻底规避了递归深度的争议。答辩时这句话可以直接说:我用栈来逆序前驱链,是为了不依赖递归,避免最坏情况下顶点数较大时的栈溢出风险。
3.3 排序与查找:第二考点不能瘸腿
只做最短路,课设撑不起来。老师会问:你这个系统里还有没有别的数据结构知识点?所以你得主动在系统里塞一个“排序”功能。我当时的做法是:支持按建筑名称的字典序输出全部建筑列表,以及按距离排序输出“从一个建筑出发的所有可达路径”。
// 按距离从小到大排序所有从 start 出发的边 // 用结构体数组 + qsort,避免手写冒泡的尴尬 typedef struct { int adjvex; // 邻接顶点编号 int weight; // 边权(距离) } EdgeInfo; int cmpEdge(const void *a, const void *b) { return ((EdgeInfo*)a)->weight - ((EdgeInfo*)b)->weight; } void SortEdgesByWeight(MGraph *G, int start) { EdgeInfo edges[MAXVEX]; int n = 0; for (int i = 0; i < G->numVertexes; i++) { if (G->edges[start][i] != INF) { edges[n].adjvex = i; edges[n].weight = G->edges[start][i]; n++; } } qsort(edges, n, sizeof(EdgeInfo), cmpEdge); printf("从 %s 出发的路径,按距离排序:\n", G->vexs[start].name); for (int i = 0; i < n; i++) { printf(" %s (%d 米)\n", G->vexs[edges[i].adjvex].name, edges[i].weight); } }这里刻意用了qsort而不是自己写快排,是留了答辩余地的:如果老师问你“你自己实现过排序吗”,你就说“课设里我用了 C 标准库的 qsort 来处理数据排序,因为它的比较器接口清晰、性能稳定;我在平时练习时也手写过快速排序和归并排序,能讲清楚分治思路。” 这比你在课设里贴一个自己写的、边界都处理不对的快排要安全得多。课设不是算法竞赛,不要在核心业务里冒险。
查找方面,因为顶点数量少,顺序查找就够。但答辩可能会问“为什么不用二分查找”,你得能答上来:顺序查找适用于无序列表,而建筑名称列表如果允许增删就不保持有序性,所以二分查找不适用;如果想要 O(log n) 查找,可以在初始化时维护一个按名称排序的索引数组。你看,这个问题你一答,老师就知道你是真懂查找的适用条件,而不是背了个二分模板。
4. 从主函数长出来的完整代码:可复现的骨架与七个必经步骤
4.1 先写 main 的骨架:菜单循环是课设的脸面
课程设计不是写一个被调用的库函数,而是一个能交互的程序。你的 main 要是一个循环打印菜单、接收输入、分发任务的结构。这一步别写在最后,第一步就写。
int main() { MGraph G; int choice; int start, end; InitGraph(&G); // 从文件读入数据,初始化图 PrintWelcome(); // 打印系统名称和作者信息 while (1) { PrintMenu(); // 打印功能菜单 scanf("%d", &choice); while (getchar() != '\n'); // 清空输入缓冲,防止残留换行 switch (choice) { case 1: ListAllBuildings(&G); break; case 2: QueryShortestPath(&G); break; case 3: SortEdgesByWeight(&G); break; case 4: DFS_Traverse(&G); break; case 0: printf("感谢使用,再见!\n"); return 0; default: printf("输入错误,请重新选择!\n"); break; } } return 0; }这段骨架里有几个细节是血泪经验。第一,while (getchar() != '\n')这一行必须有,不然你输入完数字按回车,残留的换行会被下一个 %c 或 %s 吃掉,然后你的菜单就乱跳了。第二,case 0和default必须分开:0 是正常退出,其他数字是错误输入,这两件事不能用同一个分支,否则老师会觉得你程序的控制流不清晰。第三,每个 case 末尾都要有 break,这不用我说你也知道,但漏 break 恰恰是课设代码里最常见的问题,因为 Ctrl+C 复制上一段的时候很容易连着 break 一起删掉。
菜单函数本身要打印清楚每个数字对应什么功能,别用“1.xxx 2.xxx”这种紧凑格式,要把边界情况也写上去,比如“0. 退出”,让使用者不困惑。
4.2 数据的正确打开方式:文件读取而不是交互录入
用 scanf 一个顶点一个顶点地录入图数据是一种自虐行为——演示时你光输入数据就要两三分钟,老师早就失去耐心了。正确做法是把数据放在文本文件里,程序启动时一次性读取进来。
#define MAX_NAME 20 void InitGraph(MGraph *G) { FILE *fp = fopen("graph_data.txt", "r"); if (fp == NULL) { printf("错误:找不到 graph_data.txt 文件!\n"); printf("请确认数据文件与程序在同一目录下。\n"); exit(1); } fscanf(fp, "%d %d", &G->numVertexes, &G->numEdges); for (int i = 0; i < G->numVertexes; i++) { fscanf(fp, "%d %s", &G->vexs[i].id, G->vexs[i].name); } // 初始化邻接矩阵 for (int i = 0; i < G->numVertexes; i++) { for (int j = 0; j < G->numVertexes; j++) { if (i == j) G->edges[i][j] = 0; else G->edges[i][j] = INF; } } // 读取边 int v1, v2, weight; for (int i = 0; i < G->numEdges; i++) { fscanf(fp, "%d %d %d", &v1, &v2, &weight); G->edges[v1][v2] = weight; G->edges[v2][v1] = weight; // 无向图 } fclose(fp); printf("初始化成功:共 %d 个建筑,%d 条道路。\n", G->numVertexes, G->numEdges); }数据文件 graph_data.txt 的格式是有讲究的,按行读的,别用逗号分隔,fscanf 的空格分隔最不容易出错:
6 6 0 图书馆 1 第一教学楼 2 第二教学楼 3 学生食堂 4 体育馆 5 宿舍区 0 1 200 0 2 350 1 3 180 2 3 250 3 4 300 4 5 150这里有个坑:顶点的 id 必须和数组下标完全一致。如果你从 1 开始编号,那数组就要多开一位,或者读入时id--。我的建议是:数据文件里就从 0 开始编号,和数组下标天然对齐,省去所有转换逻辑。这看起来不优雅,但课设不需要优雅,需要的是少一个出错的可能。
4.3 手动模拟一遍 Dijkstra,把 prev 数组画出来给你看
代码可以打印,算法流程光看代码是不容易“懂”的。我建议你写完 Dijkstra 函数后,拿上面这个 6 顶点的小图,手动跑一遍,把 dist 和 prev 的变化写在一张表上。
以顶点 0(图书馆)为起点:
初始时,dist = [0, 200, 350, INF, INF, INF],prev = [0, 0, 0, -1, -1, -1]。
第一轮:找到未确定点中 dist 最小的,是顶点 1(dist=200)。确定它。然后更新与 1 相邻的点:顶点 3 的 dist 从 INF 变成 200+180=380,prev[3]=1。
第二轮:未确定点中 dist 最小的是顶点 2(dist=350)。确定它。更新顶点 3:380 与 350+250=600 比较,380 更小,所以 prev[3] 保持 1,dist[3] 不动。
第三轮:顶点 3(dist=380)确定。更新顶点 4:dist=680,prev[4]=3。
第四轮:顶点 4(dist=680)确定。更新顶点 5:dist=830,prev[5]=4。
第五轮:顶点 5 确定,结束。
如果你要查从图书馆到宿舍区的路径,从 prev[5]=4,prev[4]=3,prev[3]=1,prev[1]=0,反推得到路径:0 -> 1 -> 3 -> 4 -> 5,也就是图书馆 -> 第一教学楼 -> 学生食堂 -> 体育馆 -> 宿舍区,总长度 200+180+300+150 = 830 米。
把这张表放在你的课程设计报告里,比贴代码有用得多。老师看报告时,看到你能把算法的运行过程画出来,就证明你真的会 Dijkstra,而不是从网上找了一段代码改改名。
4.4 菜单功能与代码文件的划分:没有第三人称的项目结构
课设代码不建议写完丢在一个 main.c 里,也不建议拆成一百个文件。推荐结构是:graph.h 放结构体定义和函数声明,graph.c 放图的初始化、Dijkstra、排序等核心实现,main.c 放交互逻辑。
// graph.h #ifndef GRAPH_H #define GRAPH_H #define MAXVEX 20 #define INF 65535 typedef struct { char name[20]; int id; } Vertex; typedef struct { int edges[MAXVEX][MAXVEX]; int numVertexes, numEdges; Vertex vexs[MAXVEX]; } MGraph; void InitGraph(MGraph *G); void Dijkstra(MGraph *G, int start, int dist[], int prev[]); void PrintPath(int start, int end, int prev[]); void ListAllBuildings(MGraph *G); void QueryShortestPath(MGraph *G); void SortEdgesByWeight(MGraph *G); void DFS_Traverse(MGraph *G); #endif头文件里必须写#ifndef防重复包含。这个习惯在课设里体现不出来,因为你的代码就三四个文件,但老师看代码时会注意到。你可以在答辩时说:我用条件编译宏防止头文件被重复包含,这是大型项目里必须养成的习惯。这句话虽然简单,但能瞬间把你和那些所有代码怼在一个文件里的同学区分开。
queryShortestPath 是连接 Dijkstra 和交互界面的枢纽,逻辑是:输入两个编号 -> 检查合法性 -> 调 Dijkstra -> 输出路径。这里检查合法性必须在调用算法之前做,不能等 Dijkstra 算完再检查,因为 Dijkstra 遇到非法编号就是数组越界。
void QueryShortestPath(MGraph *G) { int start, end; int dist[MAXVEX], prev[MAXVEX]; printf("请输入起点建筑编号:"); scanf("%d", &start); printf("请输入终点建筑编号:"); scanf("%d", &end); if (start < 0 || start >= G->numVertexes || end < 0 || end >= G->numVertexes) { printf("编号超出范围,请重新输入!\n"); return; } if (start == end) { printf("起点和终点相同,距离为 0。\n"); return; } Dijkstra(G, start, dist, prev); if (dist[end] == INF) { printf("两栋建筑之间不存在可达路径。\n"); } else { printf("最短路径长度:%d 米\n", dist[end]); printf("路径:"); PrintPath(start, end, prev); } }注意里面两个特殊分支:起点等于终点的处理,以及路径不存在时的处理。这两个分支对应的就是需求分析时写的“边界情况”。你不写这两个分支,程序一般也不会崩,但输出会非常难看——起点终点相同时,路径打印可能输出一个空串或一个孤立节点,老师会问“你这算对吗”。你提前处理好了,演示时就可以主动输入这两个边界情况,展示程序的健壮性,这是加分的。
4.5 初始化后的第一轮自测:功能全绿再往下走
代码写到能编译通过,别急着去找验收老师,先自己当一遍“验收老师”。按照下面的清单过一遍,每一条都要真的跑一遍看输出:
- 正常路径查询:选一条明显绕远的路,看程序算出来的最短路径是不是你的手算结果。
- 起点终点相同:输出“距离为 0”,没有多余路径输出。
- 不存在的编号:输入 -1 或 99,程序输出提示语,不崩溃。
- 不连通节点对:如果数据文件里有孤立的建筑,查询它和其他建筑时输出“不存在可达路径”。
- 菜单输入错误:输入 8 或者 abc,程序回到菜单而不是陷入死循环。
- 图数据文件缺失:删掉 graph_data.txt 后运行程序,看程序是否给了明确提示。
第 5 条有个隐藏坑:如果输入的是字母而不是数字,scanf 会返回 0,但变量里保留的是旧值或者未初始化的值,程序可能进入死循环。简单的处理方式是检查 scanf 的返回值,如果等于 0 就清空输入缓冲并提示重新输入:
if (scanf("%d", &choice) != 1) { printf("请输入数字!\n"); while (getchar() != '\n'); // 把错误输入全部清掉 continue; }这一步检查在课设里尤其重要。演示时你紧张了、手误了,输入了一个字母,程序直接卡死,你对着黑窗口不知所措——这种场景每年验收都在上演,你提前把防护做好,就不至于翻车。
5. 避坑清单:验收前最容易翻车的 5 个位置
5.1 编译环境差异:Windows 下能跑,换台机器就崩
现象:在自己电脑上编译运行一切正常,拿到实验室或答辩机器上编译报一堆错误,或者运行时闪退。
原因:课设最常见的环境是 Dev-C++,但它的 MinGW 编译器版本可能比你在 VS Code 里用的 GCC 旧,对 C 标准支持不到位。比如你在代码里用了for (int i = 0; ...),在 C89 标准下就不允许在 for 循环里声明变量。还有//注释之外,如果你混用了 C 和 C++ 代码(比如用new或// 单行注释),严格模式下编译直接卡死。
解决:写代码时统一用 C 语言语法,变量声明一律放在语句块开头;换行注释没问题,但不要用 C++ 的特性。另外提交前最后用 Dev-C++ 打开项目,重新编译一次,确认没有警告。警告里经常藏着大坑,比如“隐式声明函数”——那就是说你调了一个函数但没包含它的头文件,你自己的编译器可能因为自动链接或旧 libc 而放过,但答辩机器上就会链接失败。
5.2 scanf 输入残留:菜单第二次输入直接跳过
现象:输入菜单选项后按回车,程序不会等待输入下一次选项,直接“吃掉”了残留的换行符,菜单飞速跳转。
原因:scanf("%d")只读取数字,把输入缓冲区里的换行符留下。下一次循环里如果碰到scanf("%c")或gets(),会立即读到这个换行符,导致输入跳过。
解决:判断输入是数字还是字符后用while (getchar() != '\n')清空缓冲;或者在每个输入语句后统一加一行fflush(stdin)。但注意——fflush(stdin) 在 C 标准里是未定义行为,在 Windows 下的 Dev-C++ 里能用,在 Linux 的 GCC 下没用。所以我建议用 getchar 清缓冲的写法,兼容性更好,也体现你知道输入缓冲区的运作方式,答辩时可以主动讲这个细节。
5.3 无穷大 INF 参与运算:Dijkstra 松弛时整数溢出
现象:图里有不连通的顶点,程序输出的最短路径长度是一个莫名其妙的负数,或者路径序列里出现连续的 -1。
原因:INF 用 65535 定义时,如果一行里两个 INF 相加——比如min + G->edges[k][j]两个都是 INF——结果超过 int 上限的溢出是小事,更常见的是 65535 + 65535 = 131070,没有溢出,但 result 是 131070,然后你把 131070 和另一个 INF=65535 比较,131070 不小于 INF,所以不更新——这没错。但如果前面 min 被算成了 32767 之类的更大值,或者 INF 定义成0x7fffffff,那两个 INF 相加就直接变成负数,负数是小于正数 INF 的,于是松弛条件永远成立,dist 被反复更新成负数,输出彻底乱了。
解决:在松弛操作前加判断,两个值都必须小于 INF 才做加法比较:
if (!final[j] && min < G->edges[k][j] + min) // 这行有问题正确写法是:
if (!final[j] && G->edges[k][j] != INF && min + G->edges[k][j] < dist[j]) { dist[j] = min + G->edges[k][j]; prev[j] = k; }先把 INF 的边排除掉,再做加法。这是一个典型的防御性编程问题,代码里处理不好,一旦图里有一个不连通点,你的 Dijkstra 输出就全错,这种 bug 往往到验收前才被发现,改起来牵一发动全身,血泪教训。
5.4 DFS 递归深度:图的顶点多了一层就可能崩
现象:在图的遍历功能里用递归实现 DFS,图有 15 个顶点时正常,你为了演示加到 20 个顶点,程序在遍历到深处时突然崩溃退出。
原因:递归函数DFS(int v)每次调用都在栈上分配新的栈帧,图的深度太大时堆栈溢出。课设的顶点数一般不会上万,但运行时栈大小只有 1MB 左右,如果递归层数上百层就可能爆栈。
解决:把 DFS 改成显式栈实现。用一个辅助数组标记访问状态,配一个栈来模拟系统调用栈。
void DFS_Traverse(MGraph *G) { int visited[MAXVEX] = {0}; int stack[MAXVEX], top = 0; int v, i; printf("DFS 遍历结果:"); for (v = 0; v < G->numVertexes; v++) { if (!visited[v]) { visited[v] = 1; printf(" %s", G->vexs[v].name); stack[top++] = v; while (top > 0) { int cur = stack[--top]; for (i = 0; i < G->numVertexes; i++) { if (G->edges[cur][i] != INF && !visited[i]) { visited[i] = 1; printf(" %s", G->vexs[i].name); stack[top++] = i; } } } } } printf("\n"); }这里我用的是“右入栈、先访问标记再入栈”的写法,避免了重复打印问题。实际上这段代码的遍历输出顺序和递归版 DFS 会略有差异,但课设里没有任何人关心输出顺序是和递归版一模一样还是略有偏差,只要“每个顶点都被访问且输出正确”就没问题。这趟换栈的收益是:即使顶点数几百个也绝对不崩,你答辩时可以说“这里为了避免递归栈溢出,我用了显式栈实现 DFS”,这一句话就把整段代码的分量抬起来了。
5.5 报告里的运行截图:截图不清晰是最亏的丢分点
现象:课程设计报告里贴了运行截图,但截图分辨率过低或者窗口被拉伸,老师看不清输出的文字内容,只能看到模糊的色块。
原因:屏幕缩放比例太高,或者截图时只截了窗口的一小块,报告排版时又把图片拉大了,字就变成了“马赛克”。
解决:截图时用 Win+Shift+S 选择窗口区域,不要把整个屏幕截进来;截图后用画图工具把图片裁剪到只留窗口黑色区域;保证截图里字体大小用默认的 16 号以上,不要为了多放内容把窗口缩得很小。另外,每张截图下方要有一行说明文字:“图 3-1 最短路径查询结果”,这对应报告里的“运行结果与分析”章节。文字说明里要写清楚输入了什么数据、看到了什么输出、结果是否符合预期。这一条不是代码问题,但很多代码写得好的人在这里翻车,报告分上不去,很可惜。
6. 答辩与验收:从“代码能跑”到“老师点头”的最后一公里
到了验收环节,代码已经没得改了。这时候拼的是两样东西:你怎么讲,以及你怎么应对提问。我这里说三个我最常用的答辩技巧,都是实战里验证过有用的。
第一个是“演示脚本”。不要到了现场凭感觉操作,提前写好一个三分钟的演示流程:启动程序,顺手点开数据文件,说明“这是图的数据文件,共 6 个顶点 6 条边”,然后查询从“图书馆”到“学生食堂”的最短路径,故意先说一句“如果走第二教学楼会更近,但实际最短路径是先到第一教学楼”——这句话的作用是让老师知道你理解“最短”的含义,你不是只会跑程序。然后展示按距离排序和 DFS 遍历,最后演示输入非法编号,程序提示错误并返回菜单。三分钟结束,每个功能都被验证过,而且不会冷场。
第二个是“高频问题预案”。按我的经验,老师最爱问的问题固定就那么几个,你提前把答案准备好,背熟,现场就不卡壳。
问:Dijkstra 算法为什么不能处理负权边?
答:因为 Dijkstra 基于贪心,每次确定一个距离最小的顶点就不再更新。如果存在负权边,某个顶点可能在确定之后通过负权边被更短地发现,但算法已经不会回头更新它了。所以负权图得用 SPFA 或 Bellman-Ford。
问:你的图是无向的,如果改成有向图,代码哪里要改?
答:读取边时只赋一个方向的权值,G->edges[v1][v2] = weight,去掉回赋的那一行。另外 DFS 遍历时也要按有向边来访问邻接点。
问:为什么邻接矩阵的 INF 用 65535 而不是 32767?
答:65535 是 2 的 16 次方减 1,在 int 范围内,而且足够大,任何两个 INF 相加也不会溢出 int(65535+65535=131070,远小于 2^31),确保松弛比较时不被意外“污染”。
这三个问题你答得顺,老师对你的印象分就会往上走。最怕的是背了答案但没理解,老师追问一层就露馅。所以预案里的答案一定要自己先弄懂,别硬背。
第三个是“报告与演示的一致性”。你课程设计报告里写的是什么数据结构、什么算法流程,演示时就必须是什么。有些同学报告里写“本系统采用邻接表存储图结构”,但代码里用的是邻接矩阵——这种情况老师一旦发现,印象分直接清零。报告和代码必须对口。我一般建议在报告里专门画一张“系统功能框图”——但这个不用画得很复杂,用方框和箭头把功能模块和数据结构之间的关系标示出来即可。
最后一个技巧是关于“求帮助”的。如果现场真的遇到突发状况,程序起不来——这不是你的错,但别愣着。你可说“我先把数据文件路径检查一下”,然后打开当前目录确认文件在不在。如果还是不行,就诚恳一点:“正常情况下这个功能是好的,可能是当前环境的字符编码问题导致文件没有正确读取。” 说实话,这个责任其实不在你——你答辩前没在答辩机器上跑过一遍,谁知道这机器少了什么运行库。所以我的习惯是:答辩前一天借一台跟答辩环境最接近的电脑,装上相同的编译器,完整跑一遍演示流程。这个动作能排掉 80% 的“机器差异”导致的尴尬。希望帮到你。
本文还有配套的精品资源,点击获取