简介这份全国交通出行咨询模拟系统C语言实现源码面向数据结构课程设计与期末大作业场景适合需要完成交通网络查询、最短路径规划等经典课题的本科生直接参考与复用。项目已获导师指导并通过评审得分97分围绕全国城市间交通出行咨询展开完整演示了图结构存储、路径搜索与多方式换乘等核心算法的实际落地既可作为课设报告配套代码也能为同类系统设计提供可复用的模块化思路。压缩包共10个文件具体为5个头文件、1个源文件、3个文本数据文件与1份说明文档头文件按管理、用户、核心结构等职责拆分模块主程序负责整体调度文本数据提供城市、航班、列车基础信息整体仅16KB结构紧凑、无冗余文件方便快速阅读与二次开发。目前已有308人学习下载下载后可在本地直接编译运行适合用于课程设计、期末大作业或数据结构综合训练。1. 全国交通咨询模拟系统本质上就是一张带权图加两段 Dijkstra课程设计里全国交通出行咨询模拟系统听起来像是个大工程又是“全国”又是“咨询”很容易让人以为要做地图、做导航。实际上它就是一个标准的数据结构图论题把城市看成顶点、把交通线路看成带权边然后回答“从 A 到 B 怎么走最近、怎么花钱最少、怎么换乘最少”。数据全是模拟的不接真实地图 API不需要经纬度不需要路径渲染。我见过不少同学在这个题上卡住卡的点不是算法看不懂而是 C 语言怎么把菜单循环、文件读取、Dijkstra、路径回溯串成一个能演示的完整程序。这个题目适合已经选图论方向、想避开通讯录和图书管理这类烂大街选题的同学也适合想认真练一次 C 语言工程组织能力的人。2. 把全国交通建模成图邻接矩阵选型与数据文件设计2.1 三十个城市的规模邻接矩阵比邻接表更稳建全国交通网络最常见的存储方案就是邻接矩阵和邻接表。很多教材默认“全国”顶点很多、边很稀疏所以推荐邻接表但课程设计这个场景要反过来想。全国交通咨询模拟系统里的城市数量一般控制在 20 到 50 个以内省会加主要地级市就够演示了。这个规模下n×n 的矩阵也就 2500 个格子三张权重表加起来不到一万个 int内存完全无压力。邻接矩阵最大的好处是查边和改边都是 O(1) 复杂度读文件填充权重时直接w[i][j] value不用维护链表节点不用写头插法出 bug 的概率低很多。邻接表的意义在于稀疏图省内存、遍历相邻顶点快但 50 个顶点的图里O(n²) 和 O(ne) 的差距肉眼根本看不出来。对比项邻接矩阵邻接表码量少直接二维数组多要写节点和链表操作查边速度O(1)按下标取O(度)需要遍历链表内存占用n² 个 intn50 时很小较少但n小优势不明显适合场景顶点少、图较稠密顶点多、边稀疏课程设计友好度高调试直观低链指针查起来费劲我一般会直接用int **w动态申请一个 n×n 矩阵或者干脆用静态二维数组。矩阵越界这类低级错误在 50 的规模下也容易排查valgrind 一跑就能定位比链表节点丢指针好查得多。2.2 三张权重矩阵与城市、线路数据文件的结构系统要回答三种查询就得准备三种权重里程数、费用、耗时。常见做法是维护三张 n×n 矩阵分别叫distMat、costMat、timeMat读取不同交通方式的线路文件时往对应的矩阵里填值。数据文件分成两类。第一类是城市表格式做两列编号和城市名。第二类是线路表每种交通方式一个文件格式是“起点编号 终点编号 权重”权重具体代表什么意义由该文件对应哪种权重矩阵决定。cities.txt示例 0 北京 1 上海 2 广州 3 武汉 4 成都 roads.txt公路线路权重公里数 0 3 1100 1 3 800 3 4 1100 rails.txt铁路线路权重票价单位元 0 1 553 0 3 520 1 3 270 flights.txt航班线路权重飞行时间单位分钟 0 1 125 0 3 150 1 2 145这里有一个很容易误解的地方公路存公里数、铁路存票价、航班存时间那“最短里程、最低费用、最少时间”三种查询怎么统一权重体系我建议不要把三种方式的数据混合进同一张矩阵而是先汇总成三张 n×n 矩阵然后查询时按需取用。比如“最短里程”把公路里程和铁路里程逐条对比同一条城市对取较小值填入distMat因为没人会在一个咨询系统里反复比较“公路 1100 公里 vs 铁路 1050 公里”这种细节取最优即可。“最低费用”用costMat由铁路票价和航班票价对比填充。“最短时间”用timeMat公路时间按里程除以平均时速估算航班直接用飞行时间。这样设计的好处是Dijkstra 只认权重矩阵查询时传不同矩阵进去就行核心算法不用动。2.3 loadData 源码骨架把文件读进矩阵下面这段是读取城市文件和线路文件的骨架代码以roads.txt为例typedef struct { int id; char name[32]; } City; City cityTable[MAX_CITY]; int cityCount 0; int **distMat; // 里程矩阵 int **costMat; // 费用矩阵 int **timeMat; // 时间矩阵 void loadRoads() { FILE *fp fopen(roads.txt, r); if (fp NULL) { printf(无法打开 roads.txt请确认文件放在程序同目录\n); return; } int u, v, w; while (fscanf(fp, %d %d %d, u, v, w) 3) { if (w distMat[u][v]) { // 若已有路线取里程更短的 distMat[u][v] distMat[v][u] w; } } fclose(fp); }这段代码有个很实用的细节fscanf返回值判断为 3确保每次读满三个字段才处理能滤掉文件末尾的空行和格式错误行。w distMat[u][v]的判断实现了“同一条线路有多条记录时取优”这是真实数据里很常见的场景写进文档里也是加分项。注意初始化矩阵时把所有值设为INF一个很大但不溢出的数比如1000000000对角线设为 0。3. Dijkstra 的工程化封装一份代码同时查最短里程、最低费用和最少换乘3.1 可复用的 Dijkstra 封装含路径回溯查询功能的灵魂是 Dijkstra但为了避免复制三份代码我一般把它封装成一个通用的函数传入顶点数、权重矩阵、起点、终点返回最短权重值并用path数组带回路径。#define INF 1000000000 int dijkstra(int n, int **w, int src, int dst, int *path, int *pathLen) { int *dist (int *)malloc(n * sizeof(int)); int *vis (int *)calloc(n, sizeof(int)); int *pre (int *)malloc(n * sizeof(int)); int i, j; for (i 0; i n; i) { dist[i] INF; pre[i] -1; } dist[src] 0; for (i 0; i n; i) { int u -1, minv INF; for (j 0; j n; j) { // 找当前未访问的最小距离点 if (!vis[j] dist[j] minv) { minv dist[j]; u j; } } if (u -1) break; // 剩余点不可达 vis[u] 1; for (j 0; j n; j) { // 松弛操作 int nd dist[u] w[u][j]; if (!vis[j] w[u][j] INF nd dist[j]) { dist[j] nd; pre[j] u; } } } int ret dist[dst]; // dst 不可达时 ret 仍为 INF int cnt 0; for (j dst; j ! -1 cnt n; j pre[j]) { path[cnt] j; // 先存的是 dst 到 src 的逆序 if (j src) break; } *pathLen cnt; free(dist); free(vis); free(pre); return ret; }参数说明w是 n×n 权重矩阵src、dst是城市编号path是输出参数调用方传入一个足够大的数组函数运行完后path[]里保存的是从 dst 一路回溯 pre 数组得到的逆序路径所以调用方打印时要从后往前读。pathLen返回路径长度便于逆序打印。有一个实现细节值得注意在松弛前先判断w[u][j] INF避免dist[u] INF出现数值溢出导致结果变成负数这个问题很隐蔽后面避坑章会再展开。3.2 三种查询的权重定义与调用方式三个查询功能调用同一个 dijkstra 函数区别只在于传入的矩阵不同。void queryShortestDist() { int u inputCityId(); int v inputCityId(); int path[100], len 0; int d dijkstra(cityCount, distMat, u, v, path, len); if (d INF) { printf(这两个城市之间暂时没有可通行线路\n); return; } printf(最短里程: %d 公里\n, d); printPath(path, len); }queryLowestCost和queryShortestTime结构完全一样只是把distMat换成costMat或timeMat。这就是矩阵选型的好处核心算法一份代码数据维度随便切。“最少换乘”单独说。换乘次数不能在三种基础权重矩阵里直接跑 Dijkstra否则你得到的是“经过的城市数量最少”而不一定是“换乘次数最少”。我一般这样做定义一张transferMat如果两个城市之间有任意一种直达交通方式就置为 1代表一次直达否则为 INF然后跑 Dijkstra。路径经过的中间城市数量就是换乘次数。这个方案的前提是系统中每条线路都是两城市点对点直达不存在一条线路串起多个中间站如果你的演示数据里有“一趟车经停五站”这种设定这个近似就不成立了需要把整条线路建模成一个超边。课程设计的数据规模通常不需要这么复杂但想拿高分就要在报告中把这个边界说清楚。4. 菜单驱动的交互循环从“算法跑通”到“系统像样”4.1 主菜单与 scanf 残留的处理写 C 语言课程设计控制台交互是标配。主循环的常见写法是do-while加switch把各个查询函数挂在 case 分支上int main() { initMatrices(); loadCities(); loadRoads(); loadRails(); loadFlights(); int choice -1; do { printf(\n 全国交通出行咨询系统 \n); printf(1. 查询最短里程\n); printf(2. 查询最低费用\n); printf(3. 查询最少换乘\n); printf(4. 查询某城市可直达的城市\n); printf(0. 退出\n); printf(请输入选项: ); scanf(%d, choice); while (getchar() ! \n); // 吃掉残留的回车符 switch (choice) { case 1: queryShortestDist(); break; case 2: queryLowestCost(); break; case 3: queryTransfer(); break; case 4: queryReachable(); break; case 0: printf(感谢使用再见\n); break; default: printf(无效选项请重新输入\n); } } while (choice ! 0); return 0; }关键一行是while (getchar() ! \n);。scanf(%d)输入数字后回车会残留在缓冲区如果不吸掉后续如果有getchar()或scanf(%c)读取输入就会直接读到这个回车符导致查询流程还没开始就结束了。这个坑几乎每届都有人踩单独拿一节说是因为它真的会浪费调试时间。还有一点选择0退出后立即返回0不要在退出前释放矩阵内存课程设计演示完直接关掉控制台就行不用画蛇添足。4.2 路径的正向打印与调试输出path数组里存的是逆序路径直接正着打印就反了。打印函数要按索引从后往前走void printPath(int *path, int len) { printf(路线: ); for (int i len - 1; i 0; i--) { printf(%s, cityTable[path[i]].name); if (i 0) printf( - ); } printf(\n); }很多同学在这里犯迷糊自己写的时候建议先画一张图比如 0→3→4path里存的是{4, 3, 0}len3打印循环从i2开始输出cityTable[0]就是起点。为了调试方便我还会在这个函数里加一个临时打印把path数组内容先原样输出一遍比如“path raw: 4 3 0”确认路径回溯没错再正向打印。这种调试输出留在源码里不会扣分答辩时还能说明你理解了路径存储方式。4.3 查询直达城市的附加功能菜单里的第 4 项“查询某城市可直达的城市”代码量不大但演示效果很好因为它是遍历邻接矩阵的行实现了“直达查询”和最短路径互补。核心代码就三行void queryReachable() { int u inputCityId(); printf(从 %s 出发可直达:\n, cityTable[u].name); for (int v 0; v cityCount; v) { if (u ! v distMat[u][v] INF) { printf( %s %d 公里\n, cityTable[v].name, distMat[u][v]); } } }这里的distMat[u][v] INF判断体现了矩阵初始化的正确性如果初始化漏了distMat里全是随机值直达查询会输出一屏垃圾城市或者把不可达的城市也当成可达输出。所以初始化函数里两层循环把所有值赋为INF再写文件数据这个顺序不能乱。5. 课程设计高频坑排查五个最容易翻车的地方5.1 scanf 残留导致菜单“抽风”现象输入菜单选项后程序没有等待输入起点城市直接打印“请输入起点城市编号”然后立刻跳到“无效选项”或者第二次循环时根本不进查询直接退出。原因scanf(%d)只读走数字把回车符留在输入缓冲区。下一次scanf(%d)遇到空白符会自动跳过所以数字输入之间没事但只要你用了getchar()或者scanf(%c)这个回车就会被读走变成城市编号的输入内容逻辑自然错乱。解决每次scanf数字输入后立即加while (getchar() ! \n);清空缓冲。如果代码里多处需要输入封装一个int readInt()函数统一处理比到处写清缓冲更省心。5.2 INT_MAX 做加法最短路径变负数现象Dijkstra 松弛时dist[u] w[u][j]算出来一个巨大的负数程序认为这条路径比正数路径更短最终输出一个负的里程或者一个不合理的路径。原因INT_MAX约 21 亿加上一个小权重直接溢出变成负数比较运算符被负数欺骗。解决不要用INT_MAX初始化距离改用1000000000这类足够大又不会溢出的“伪无穷”。同时在松弛前判断w[u][j] INF这样dist[u] w[u][j] dist[j]只在权重有效时才会被执行。这个坑在数据结构题里写核心逻辑时就能发现但放到完整系统里会被场景输入掩盖等到演示那天翻车就晚了。5.3 中文城市名乱码和 strcmp 比较失败现象程序读入的城市名打印出来是乱码或者用户输入汉字查询时报“城市不存在”。原因C 语言源码里用中文字符串字面量编译器保存的编码和文件读取编码不一致控制台下输入的编码和代码里比较的编码不一致。Windows 控制台常见 GBKLinux 终端常见 UTF-8两头不对就乱码。解决城市名不要参与算法比较城市表只用编号定位城市城市名仅用于显示。inputCityId()接收的是整数编号用户查询路径时也不会拿汉字去strcmp。如果演示环境乱码最简单的方法是改用printf(0北京 1上海 ...)这种“编号城市名”的提示方式用户输入编号程序内部只处理 int乱码就变成纯显示问题不影响逻辑。5.4 图规模失控查询卡到没法演示现象把“全国”理解成真的要把几十上百个城市全建出来矩阵动态申请了 200×200 甚至更大的规模loadData变得很慢Dijkstra 也要算一会儿演示时敲完查询要等好几秒才出结果。原因不是矩阵太大是循环次数爆炸。200 个顶点的邻接矩阵初始化就要 4 万次赋值Dijkstra 是 O(n²)一次查询 4 万次循环看起来不多但如果用了 Floyd-WarshallO(n³)200 的三次方是 800 万次再加上三张矩阵演示环境稍微差一点就会肉眼可见的卡。解决城市数量控制在 30 左右Dijkstra 一次查询毫秒级返回体验非常流畅。不要在课程设计里追求“真的全国”要追求“结构完整、演示流畅”。如果想展示大数据量的能力最多做到 60 个城市30 以内最优。5.5 “最少换乘”查出的是公里数最短的路线现象用户查询“从北京到成都最少换乘几次”程序返回一条要换乘 5 次但总里程最短的路线而实际上有另一条只换乘 1 次的路线被忽略了。原因transferMat没有建好或者直接用了distMat跑 Dijkstra。“换乘次数”和“里程”是两种不同的代价权重矩阵不同结果自然不同。解决按 3.2 节的方案单独建transferMat把“两城之间有任意直达方式”定义成权重 1再跑 Dijkstra。同时测试用例要设计好至少准备一条“里程长但换乘少”和一条“里程短但换乘多”的路线演示时两个查询对照输出反而能说明你的权重设计是有效的。6. 答辩前再补三个升级点让项目从“会跑”到“能讲”6.1 加一个全矩阵 dump 功能调试和展示都靠它开发期间最容易遇到的问题是“图到底建对没有”眼睛盯着 txt 文件没法确认。写一个dumpAllMatrices()把三张权重矩阵按城市名格式化打印出来void dumpMatrix(const char *title, int **mat) { printf( %s \n, title); for (int i 0; i cityCount; i) { for (int j 0; j cityCount; j) { if (mat[i][j] INF) printf(%5s , INF); else printf(%5d , mat[i][j]); } printf(\n); } }加载完数据后调用一次手动对照 cities.txt 检查数据是否错位。答辩时如果老师问“数据怎么保证读入正确”直接展示这张表说明你做了数据自检这个回答比空口解释有说服力得多。6.2 把城市查询从“内置数字”改成“菜单选择”inputCityId()让用户输入编号新手演示容易输错。升级做法是打印当前所有城市菜单用户用上箭头或输入数字选中程序内部仍只处理编号。这个改动不大但操作体验会好很多演示时不用背编号老师也能直接上手。核心思路就是“显示层可以友好内部逻辑不变”这一句话讲出来会比单纯说“我做的系统能跑”更有层次。6.3 用同一个 dijkstra 函数把三次查询统一起来答辩时最容易拿分的一点我的三次查询共用了同一个dijkstra函数区别只是传入不同的权重矩阵。这体现了函数封装和模块化的意识。建议在报告的数据结构说明部分画一张表列出三张矩阵分别由哪些文件填充、服务哪种查询然后写一句“三层查询共用同一算法仅权重矩阵不同”。这个设计比单独写三个类似函数要简洁得多也是课程设计质量的核心差异。这学期我帮一个学弟排查这个题最后发现他的问题就是INT_MAX溢出改完的一瞬间他问了我一句“这个题是不是其实挺简单的”。我说对就是一张图加两个函数难的是你愿不愿意把每个环节的边界都想清楚。希望帮你把这道课程设计做成一个能讲清楚、敢演示、不翻车的项目也希望你在做完之后回头看时觉得这一段 C 代码写得值。本文还有配套的精品资源点击获取