ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

全国交通咨询模拟系统:用Dijkstra与BFS实现最短路径与换乘查询

全国交通咨询模拟系统:用Dijkstra与BFS实现最短路径与换乘查询 简介全国交通出行咨询模拟系统C语言实现源码包是一份数据结构课程设计项目面向高校计算机相关专业学生在课程设计、期末大作业中的直接参考与复用可作为完整方案下载即用。项目以全国城市间交通网络为背景综合运用图存储、最短路径查询、邻接表等核心数据结构知识涵盖公路、铁路、航班等多类交通方式并已获导师指导、以97分高分通过验收。压缩包共10个文件核心为5个头文件与1个C主程序分别承担系统模块接口与主逻辑另含3个文本数据文件用于城市、航班、火车信息初始化数据格式简洁、便于按需修改同时附有1份README说明文档整体约16KB目录结构清晰便于快速阅读和二次开发。目前已有308人学习下载。下载解压后即可编译运行无需修改能帮助使用者快速完成一份结构完整、可演示的课程设计也可作为理解交通咨询模拟算法实现的参考资料代码注释与模块划分便于进一步扩展功能并适合在课程答辩中直接演示。1. 全国交通出行咨询模拟系统一张带权图把“出行咨询”变成一道最短路径题数据结构课程设计里“全国交通出行咨询模拟系统”这类题目几乎年年换皮出现。它听上去要做成一个小型产品剥开来看骨架其实非常固定把城市抽象成顶点把交通线路抽象成带权边然后用最短路径算法回答“从A市到B市怎么走最近、换乘最少”。真正让初学者卡住的不是“全国”有多大而是C语言里怎么组织这张图、怎么把文本数据可靠地加载进内存、怎么让算法结果以看得懂的方式输出。这篇文章按我实际做课设时惯用的路线从建模、存储、算法选型、交互菜单到排错整体走一遍最后给出测试数据和答辩扩展方向。2. 建模与存储把交通网变成C语言能读懂的图2.1 为什么是一张无向带权图城市、线路和权重怎么映射交通出行咨询这件事核心信息只有两类有哪些城市哪些城市之间有线路、线路代价是多少。城市就是图的顶点线路就是连接两个顶点的边边的权重可以表示距离、时间或票价。换乘信息不需要额外存它可以从路径经过的边数推导出来。最常见的做法是把全国交通网建模成一张无向带权图。火车、长途汽车大多是双向运行所以边按无向处理存的时候对称写入邻接矩阵。权重用什么口径决定了系统的查询标题。如果课程设计题目写的是“最短路径”权重用公里数如果写的是“最少时间”权重就存分钟数两者都做就需要准备两套边权或者运行时动态替换。这一步先做结构体定义。我用一个全局图对象来装城市表和邻接矩阵后续所有函数都操作它省去在函数间反复传指针的麻烦。课程设计规模下几十个城市邻接矩阵浪费的空间完全可以接受。#include stdio.h #include stdlib.h #include string.h #define MAX_CITY 256 #define INF 1000000000 // 1e9表示不可达 typedef struct { int id; // 城市编号直接当数组下标用 char name[64]; // 城市名 } City; typedef struct { int n; // 当前城市数量 City cities[MAX_CITY]; // 城市表 int edge[MAX_CITY][MAX_CITY]; // 邻接矩阵 } TransportGraph; TransportGraph g_graph;City 结构体把编号和名字绑在一起方便按数组下标访问城市名。TransportGraph 里 n 表示有效顶点数量edge 是邻接矩阵edge[i][j] 存放 i 到 j 的代价。这里的编号策略我建议直接让文件的 id 等于数组下标从 0 开始后面所有算法都不用再做转换少一类 bug。2.2 邻接矩阵还是邻接表课程设计规模下怎么选很多教材把邻接表和邻接矩阵放在一起讲学生就容易纠结。其实选择标准很简单顶点少、边比较密、要反复查询任意两点关系用邻接矩阵顶点多、边稀疏、经常只沿着某几个邻居遍历用邻接表。交通咨询系统里查询频繁最短路径算法每次都要扫描当前顶点的所有邻居邻接矩阵的随机访问是一步到位代码也直观。全国几百个城市矩阵大小 500×500每个 int 占 4 字节总共约 1MB完全不是负担。如果坚持邻接表路径回溯、判断两点是否直连、Floyd 预计算都会变得麻烦课设答辩时还要多解释一套链式结构。所以这个项目我一般直接上邻接矩阵。只有当题目明确要求“城市数量超过 1000内存受限”时才考虑邻接表加 Dijkstra 堆优化那是另一个故事。2.3 文件存储与加载city.txt 和 route.txt 的约定数据不能写死在代码里这是课设评分的重要观察点。用两个文本文件分别存城市和线路将来替换数据就能模拟不同区域系统就不只是“全国”换成任意网络都能跑。城市文件每行一个城市线路文件每行一条边格式越简单越好。我用两个文件city.txt 存城市编号和名字route.txt 存边的两个端点和权重。下面这个表格是字段约定文件字段说明示例city.txtid 城市名城市编号从 0 开始城市名单词间不要有空格0 A市route.txt起点 终点 权重权重表示距离或时间整数0 2 10加载函数分开写先读城市再读线路。线路加载前必须把邻接矩阵初始化成全 INF对角线初始化为 0否则程序会拿着内存里的残留值当距离算。int loadCities(const char* path) { FILE* fp fopen(path, r); if (!fp) { printf(无法打开城市文件: %s\n, path); return -1; } g_graph.n 0; while (fscanf(fp, %d %63s, g_graph.cities[g_graph.n].id, g_graph.cities[g_graph.n].name) 2) { g_graph.n; } fclose(fp); return g_graph.n; } void loadRoutes(const char* path) { for (int i 0; i MAX_CITY; i) { for (int j 0; j MAX_CITY; j) { g_graph.edge[i][j] (i j) ? 0 : INF; } } FILE* fp fopen(path, r); if (!fp) { printf(无法打开线路文件: %s\n, path); return; } int u, v, w; while (fscanf(fp, %d %d %d, u, v, w) 3) { if (u MAX_CITY || v MAX_CITY) continue; g_graph.edge[u][v] w; g_graph.edge[v][u] w; // 无向图对称写入 } fclose(fp); }fscanf 的 %63s 限制了城市名长度避免一个超长名字把缓冲区写穿。 2 和 3 的判断很关键它表示“每行必须完整读到对应数量的字段才算一条有效数据”空行和坏行会被跳过而不是让程序崩溃。加载线路前重置 n 和邻接矩阵是重复加载数据的后悔药程序支持重新载入文件时不会越界。2.4 初始化细节INF 的值为什么不能拍脑袋邻接矩阵初始化时不可达边用什么值看似小事实际是课设翻车重灾区。用 0 肯定不行0 会被当成一条零代价路径Dijkstra 一跑全是 0。用 INT_MAX 也不行松弛比较 dist[u] w 时两个大数相加直接溢出成负数结果比任何路径都小算法全乱。我在代码里统一用 1e9 作为 INF。城市数 256边权就算到十万dist[u] w 撑死几千万离 int 上限还远。这个值既大得足够表示“不可达”又小得不会溢出是面试和课设里最稳的选择。如果你手痒想用 2147483647那要么在松弛前判 w INF要么改成 long long否则迟早被溢出坑一次。3. 核心算法Dijkstra 与 Floyd 的选型、实现和参数3.1 选型判断单点查询用 Dijkstra全源预计算用 Floyd交通咨询系统最常见的需求是“查 A 到 B 的最短路”。单次查询用 Dijkstra时间复杂度 O(n²)简单直接。如果系统需要频繁查询任意两城市之间的最短路每次跑 Dijkstra 就会重复计算这时候应该上 Floyd一次性求出所有点对的最短路后续每次查询 O(1) 取结果。课设里的选择标准我总结成一句话菜单只有单次查询Dijkstra 就够了菜单有“任意两地查询”且城市数不超过 200Floyd 更省事代码只有三层循环。有些同学两个都写了答辩时反而要解释两套代码的行为差异没必要。我一般用 Dijkstra 做单次查询换乘查询用 BFS各司其职。3.2 Dijkstra 模板邻接矩阵版的最短路与路径记录Dijkstra 的教材实现到处都是但很多版本只返回距离不返回路径。课设里如果没有路径查完只会输出一个数字根本没法演示。所以我写的模板把路径也记录下来用 prev 数组存每个节点的前驱最后从终点回溯到起点。void dijkstra(int start, int dest, int* dist, int* prev, int* path, int* pathLen) { int visited[MAX_CITY] {0}; for (int i 0; i g_graph.n; i) { dist[i] g_graph.edge[start][i]; prev[i] (dist[i] ! INF i ! start) ? start : -1; } visited[start] 1; dist[start] 0; prev[start] -1; // 起点没有前驱回溯到此停止 for (int step 1; step g_graph.n; step) { int u -1; int minDist INF; for (int i 0; i g_graph.n; i) { if (!visited[i] dist[i] minDist) { minDist dist[i]; u i; } } if (u -1) break; // 剩余节点不可达提前结束 visited[u] 1; for (int v 0; v g_graph.n; v) { if (!visited[v] g_graph.edge[u][v] ! INF dist[u] g_graph.edge[u][v] dist[v]) { dist[v] dist[u] g_graph.edge[u][v]; prev[v] u; } } } *pathLen 0; if (dist[dest] INF) return; for (int cur dest; cur ! -1; cur prev[cur]) { path[(*pathLen)] cur; if (cur start) break; } for (int i 0; i *pathLen / 2; i) { int tmp path[i]; path[i] path[*pathLen - 1 - i]; path[*pathLen - 1 - i] tmp; } }代码第 7 行给所有点初始化了“通过起点直接能到的前驱”这个动作很多模板会漏。如果不初始化回溯路径时 prev 是野值轻则路径错乱重则死循环。每轮选最小未访问节点用线性扫描城市数量几百时完全够用。松弛的判断条件是dist[u] edge[u][v] dist[v]这里要求 edge 不为 INF防止把“不可达”加成“可达”。3.3 最少换乘不是“权重小一点”是另一张图上的 BFS最短距离和最少换乘经常被混为一谈。距离最短的路径要经过三个城市、换两次车换乘最少的路径可能是直连但绕远路。两个指标本质不同不能用同一套权重。最少换乘问题的标准做法是把每条边权重视为 1跑无权图的最短路径BFS 就是最优解。换乘次数等于“经过的边数 − 1”直连线路是 1 条边换乘 0 次。int minTransfers(int start, int dest) { int queue[MAX_CITY], head 0, tail 0; int step[MAX_CITY]; for (int i 0; i g_graph.n; i) step[i] -1; step[start] 0; queue[tail] start; while (head tail) { int u queue[head]; if (u dest) break; for (int v 0; v g_graph.n; v) { if (g_graph.edge[u][v] ! INF step[v] -1) { step[v] step[u] 1; queue[tail] v; } } } if (step[dest] -1) return -1; // 不可达 if (step[dest] 0) return 0; // 起点就是终点 return step[dest] - 1; // 边数减一 }step 数组记录从起点到每个点经过的边数同时兼作 visited 标记。这里必须用 BFS 而不是 DFS因为 BFS 第一次到达某个点时就是最短边数DFS 会绕路。如果题目里所谓“换乘”指换乘交通工具类型比如高铁转飞机那要建“线路图”而不是“城市图”顶点变成车次两个车次有共同城市才连边那是进阶版本课设一般用不到。3.4 参数与边界不可达节点、起点终点相同、负权边Dijkstra 的适用前提是边权非负。交通网络里的距离、时间、票价天然非负所以放心用。但如果自己扩展系统时允许“临时优惠”“反向里程”之类负数权重Dijkstra 会给出错误结果那就要换算法。课设文档里写一句“本系统交通场景均为非负权值故采用 Dijkstra”是很好的边界说明。不可达的情况也要处理。dijkstra 结束后如果 dist[dest] 还是 INF不能给用户输出 0要提示“暂无可达路径”。起点和终点相同的查询直接返回 0 和只含一个顶点的路径。这些细节在答辩演示时容易被老师专门问到提前挡住。4. 从算法到系统菜单交互、结果展示和一组可信的自测数据4.1 菜单状态机用 while switch 把查询串成一个系统算法写好了还得有一个让用户操作的壳。课程设计系统不需要图形界面命令行菜单就够。一个死循环加用户输入通过 switch 分发到不同功能退出时用 break 跳出这个结构既简单又好解释。void runMenu() { int choice -1; while (choice ! 0) { printf(\n 全国交通出行咨询模拟系统 \n); printf(1. 查询两地最短距离\n); printf(2. 查询两地最少换乘\n); printf(3. 查看所有城市列表\n); printf(0. 退出系统\n); printf(请选择功能: ); if (scanf(%d, choice) ! 1) { printf(输入无效请重新选择\n); while (getchar() ! \n); // 清空输入缓冲 choice -1; continue; } switch (choice) { case 1: queryShortestPath(); break; case 2: queryMinTransfer(); break; case 3: listAllCities(); break; case 0: printf(感谢使用再见\n); break; default: printf(没有该选项\n); break; } } }输入无效时用 getchar 把缓冲区的残留字符读完这是命令行程序的常规血泪处理。如果不做清理用户随便敲一个字母scanf 失败后无限循环刷屏演示时相当尴尬。菜单层级也不要做得太深一两层正好太深老师操作起来会觉得系统不好用。4.2 路径回溯与格式化输出把数组变成“A市 - C市 - D市 - B市”Dijkstra 算出的 path 数组存的是节点下标直接打印数字没人能看懂。输出前要映射回城市名并按顺序拼接成一条完整路径。路径在回溯阶段是从终点倒着存进数组的所以要先翻转再逐段输出。void printPath(int* path, int pathLen) { if (pathLen 0) { printf(暂无可达路径\n); return; } for (int i 0; i pathLen; i) { if (i 0) printf( - ); printf(%s, g_graph.cities[path[i]].name); } printf(\n); } void queryShortestPath() { int from, to, dist[MAX_CITY], prev[MAX_CITY]; int path[MAX_CITY], pathLen; printf(请输入起点城市编号: ); scanf(%d, from); printf(请输入终点城市编号: ); scanf(%d, to); if (from 0 || from g_graph.n || to 0 || to g_graph.n) { printf(城市编号越界\n); return; } if (from to) { printf(起点和终点相同距离为 0\n); return; } dijkstra(from, to, dist, prev, path, pathLen); if (dist[to] INF) { printf(暂无可达路径\n); return; } printf(最短距离: %d\n, dist[to]); printf(路径方案: ); printPath(path, pathLen); }城市编号越界、起终点相同、不可达三种情况都在用户输入层拦截算法层不用重复操心。输出格式保持一致先给距离再给路径这是最直观的展示顺序。4.3 我用来验证算法的 5 节点样本数据写完算法最怕自我感觉良好随手填几组数据全是直连算法再错也看不出来。我习惯准备一个 5 节点的小样本故意设计一条“距离最短但换乘多”的路径和一条“直连但距离远”的路径用来同时验证 Dijkstra 和换乘 BFS 是否各司其职。city.txt0 A市 1 B市 2 C市 3 D市 4 E市route.txt0 2 10 2 3 20 3 1 30 0 1 100 0 4 50 4 1 5这张图里A 到 B 有直连路线代价 100经过 C、D 绕行的代价是 60经过 E 的路线代价 55距离最短。换乘方面直连只要乘坐 1 段换乘 0 次经过 E 要乘坐 2 段换乘 1 次经过 C、D 要乘坐 3 段换乘 2 次。三个方案的排名完全不同正好测试系统区分两个查询目标的能力。查询预期结果最优路径A 到 B 最短距离55A市 - E市 - B市A 到 B 最少换乘0 次A市 - B市如果程序跑出来的最短距离是 60 或 100说明 Dijkstra 有 bug如果换乘查询返回 1 次说明 BFS 把“经过边数”和“换乘次数”算混了。用这 5 条边、6 条数据就能把两套核心逻辑全部覆盖比写几十个城市更有效率。5. 避坑指南从编译通过到答案正确中间藏着这些坑5.1 数据读入与初始化阶段三个绕不开的问题**现象**路径回溯时程序卡死或者打印出来的路径在几个城市之间来回循环最后栈溢出。 **原因**城市编号基准不统一。城市文件里编号从 1 开始数组下标从 0 开始Dijkstra 里一边用数组下标一边用文件编号prev 数组里存的是旧编号回溯时找不到起点甚至形成环。 **解决**统一用数组下标。文件里读出 id 后存成 id-1或者要求文件编号从 0 开始我直接采用后者。只要代码里出现“城市编号 − 1”这种神秘代码就说明基准又混了。**现象**城市名读出来是乱码或者最后一个字消失。 **原因**源码文件、数据文件、控制台三者的编码不一致。Windows 下编译器默认用 GBK 存源码文本编辑器却把 city.txt 存成了 UTF-8控制台按 GBK 解码自然乱码。 **解决**把数据文件一律另存为与源码相同的编码。Windows 下统一 GBKLinux/macOS 下统一 UTF-8。写文档时附一句“数据文件需与源码编码保持一致”能省掉演示前最后一分钟的手忙脚乱。**现象**大边权时程序输出负数距离路径完全错乱。 **原因**INF 初始化为 INT_MAX松弛比较时执行 dist[u] edge[u][v]两个值相加溢出为负每次更新都被当成更短路径。 **解决**INF 用 1e9不要用 INT_MAX。或者把所有距离变量改成 long long但那样打印格式又要调。最简单就是固定 1e9省心。5.2 算法与输出阶段两个容易被当成玄学的错误**现象**Floyd 三层循环写完后部分点对结果正确部分错误而且错误还“看起来挺对”。 **原因**循环顺序把 k 写到了最内层。Floyd 的中间节点 k 必须放在最外层才能保证每次用到的 dist[i][k] 和 dist[k][j] 已经包含了前 k-1 个中间节点的更新结果。顺序错了松弛状态会互相污染。 **解决**牢记标准顺序k 在最外i 次之j 最内。写代码时先写循环框架再填决策不要边写边调顺序。**现象**Dijkstra 输出的距离正确但路径方向反了终点在前、起点在后。 **原因**回溯时从终点一路往 prev 找路径自然是从终点倒着收集的。忘了翻转数组直接打印就成了 A 市在前面。这一条不算复杂但几乎每届课设都有同学中招属于典型的手滑问题。 **解决**回溯完成后对半翻转数组或者在收集时从起点向终点递归。翻转代码只有三行写在 dijkstra 函数末尾统一处理不要在 printPath 里翻避免查询函数多得时候漏掉。**现象**第二次加载数据文件时程序崩溃城市数量翻倍增长。 **原因**loadCities 里 n 从上次的值开始继续累加城市数组越界或者旧数据残留干扰新数据。 **解决**每次加载前先把 g_graph.n 置 0邻接矩阵在 loadRoutes 里重新初始化。重复加载本就是课设加分项但前提是状态干净。我习惯在文件读取模块提供 resetGraph 函数加载前统一调用。6. 答辩前还能做什么验证习惯、扩展方向与怎么讲清楚6.1 先跑通 5 节点最小样例再上全国数据我自己的习惯是数据从小往大加。先用第 4 章的 5 节点数据把所有功能跑通确认输出与手算一致再替换成几十个城市的大文件。这样做的好处是出错时缩圈容易5 个节点都能手推答案算法对没对一眼就知道直接上大数据错了根本不知道正确答案是什么。6.2 三个低成本高分扩展时间权重、换乘限制、线路维护第一个扩展是给系统加第二套边权距离和时间各存一个矩阵查询时让用户选择“按距离”还是“按时间”Dijkstra 函数提取对应矩阵逻辑不用改。第二个扩展是限定“最多换乘 N 次”BFS 里加一层深度判断超过 N 就剪掉。第三个扩展是“新增/删除一条线路”提供菜单入口修改邻接矩阵并重新持久化到 route.txt这能让系统从“只读查询”变成“可维护系统”。6.3 答辩怎么讲把图论价值讲清楚答辩时不要上来就讲代码行数老师想听的是建模思想。我一般这样讲把全国交通网抽象成无向带权图城市是顶点线路是带权边最短距离用 Dijkstra核心是贪心加松弛最少换乘把权重置为 1用 BFS 求最少的边数。把这三句话说清楚比背十段代码都管用。另外一定要现场演示一次不可达查询让系统输出友好提示这个场景比正常查询更能体现边界处理意识。这套方案做完我最大的教训就是数据初始化和编号基准比算法本身更容易翻车。算法只需要理解数据问题需要排查而且排查起来毫无成就感。所以现在每写一个查询模块我都先用小样本跑通再扩数据这个习惯让我少熬了很多夜。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表