数据结构课程设计实战:航班查询与检索的图与哈希表实现
2026/9/19 5:47:40 网站建设 项目流程

简介:这是一份面向计算机专业学生的数据结构课程设计完整方案,围绕航班查询与检索系统,综合运用结构体、链表、顺序表、队列等数据结构,实现航班信息的动态存储与组织,并采用基数排序对航班号进行排序、通过二分法提升有序数据的查询效率。文档完整收录了C++源码、算法流程图以及实际运行输出结果,涵盖按时间、航班号、地点、票价等多种查询方式,便于读者对照代码理解每个模块的设计思路。其中基数排序利用队列完成分配与收集,流程图清晰展示了主菜单、二分查找及各查询条件的判断分支,降低了算法学习的门槛。资源包共1个doc文件,大小217KB,内容结构紧凑,适合作为课程设计参考或数据结构算法复习的案例材料。目前已有104人学习浏览,对需要独立完成课设或希望提升数据抽象与算法设计能力的同学具有较好的借鉴价值。

1. 数据结构课程设计里的航班查询与检索到底考什么

航班查询与检索几乎是数据结构课程设计里出现频率最高的一类题目,教学平台上常见的那份「航班查询与检索(含代码、流程图、输出结果).doc」打包了完整源码、流程说明和运行截图,但很多同学拿到手只会跑一遍,换一批数据就不知道怎么改了。这道题表面是做一个查询程序,核心其实在考三件事:第一,会不会用图结构表达城市与航线的连通关系;第二,会不会为“按航班号查”“按城市查”“按时间排序”这三种典型查询选择合理的存储与检索方式;第三,能不能把思路转成流程图和规范的实验报告,而不是只会贴代码。适合正在做课程设计、准备数据结构期末答辩,或者想补图与哈希检索这一块的开发者。这篇文就顺着“建模—编码—画图—整理结果”这条线,把一套能复现也能讲清楚的方案完整过一遍。

2. 航班查询与检索的数据结构选型:图、哈希表与邻接表

2.1 需求拆解:先分清四种查询再定结构

拿到题目先别急着写代码,把需求拆成四类再决定数据结构,这是数据结构课程设计最基本的套路。一般航班查询系统至少包含四种查询:按航班号精确查找、按起点城市查所有出港航班、按终点城市查所有进港航班、按起飞时间排序输出。前两种常见,加上“中转路径查询”之后难度就上来了,因为中转意味着要在城市图上做图的遍历,不能只靠单条记录筛选。

从数据规模上看,课程设计里航班量通常在几十到几百条,城市数量在十到二十个。这个规模决定了“性能最优”不是第一目标,结构可解释性、代码量可控、答辩时能讲清楚才是。所以我一般会推荐“邻接表 + 哈希索引 + 顺序容器”三件套,而不是一上来就写多重链表或者平衡二叉树。邻接表负责城市与航班的连通关系,哈希表负责按航班号的 O(1) 查找,顺序数组或链表负责按时间排序。选型理由在下表里给出。

存储结构适合操作复杂度缺点本项目的定位
顺序数组按时间排序、遍历全部航班查询 O(n),排序 O(n log n)航班号精确查慢作为航班表主体,配合索引使用
哈希表按航班号精确查平均 O(1)无法直接支持范围或模糊查询航班号索引
邻接表按城市扩展航线遍历邻接点 O(度)找单条航班需遍历城市与航线的图结构
十字链表 / 多重表航班的起降双向关系结构复杂实现量大、易错课程设计不推荐

2.2 城市建模:用邻接表表达“城市—航班”网络

常见做法是用邻接表把城市当成顶点,每个顶点挂一条航班链表。每条航班记录里存起点城市、终点城市、航班号、起飞时间、降落时间、票价、余票量。要注意的是,航线是“有向边”,北京到上海和上海到北京是两条不同记录;但查询“某城市所有相关航班”时,经常需要同时看起降,这就要在数据结构上提前留出两个方向的访问入口。我用 C 语言时一般这样定义:

#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_CITY 20 #define MAX_FLIGHT 500 #define MAX_NAME 32 typedef struct Flight { char flightNo[MAX_NAME]; // 航班号,例如 CA1234 char startCity[MAX_NAME]; // 起点城市 char endCity[MAX_NAME]; // 终点城市 int startHour; // 起飞时间:小时,如 8 int startMin; // 起飞时间:分钟,如 30 int endHour; int endMin; int price; // 票价 struct Flight* next; // 同一城市链表上的下一跳 } Flight; typedef struct CityNode { char name[MAX_NAME]; // 城市名 Flight* outList; // 该城市出发的航班链表 Flight* inList; // 到达该城市的航班链表 } CityNode;

这段代码的逻辑说明:城市节点只存名字和两条头指针,出港链表和进港链表复用了Flight里的next指针。这样做的好处是查询“从北京出发”只需要走cityTable[北京].outList,查询“飞往上海”走inList,不需要全表扫描。参数MAX_CITYMAX_FLIGHT是课程设计里常用的固定上限,方便静态分配数组;如果你用链表动态创建城市节点也可以,但会导致代码量增加,答辩时也要多解释一层内存管理。

这里有一个容易踩的坑:inListoutList都用同一个next字段,如果某一条航班要同时挂在起点城市的出港链和终点城市的进港链上,要么给Flight增加nextOutnextIn两个指针,要么在插入时分配两个节点。推荐后者,简单直接;在航班量几百条时,双份节点的额外开销可以忽略。

2.3 航班号索引:哈希表让“按号查”变成 O(1)

城市链表把“按城市查”解决了,但“按航班号查”如果还去遍历城市链表,时间复杂度会到 O(n)。课程设计要求里通常只要求“查询平均时间最好”,这时候建一个航班号到记录位置的哈希索引最合适。哈希函数可以直接用航班号字符串的 ASCII 码累加取模,简单且对课程数据足够稳定。

#define HASH_SIZE 128 typedef struct HashNode { char flightNo[MAX_NAME]; Flight* flightPtr; // 指向航班节点 struct HashNode* next; // 拉链法解决冲突 } HashNode; HashNode* hashTable[HASH_SIZE]; int hashFunc(const char* key) { int sum = 0; for (int i = 0; key[i] != '\0'; i++) { sum = sum * 31 + key[i]; } return (sum & 0x7FFFFFFF) % HASH_SIZE; } void insertHash(const char* flightNo, Flight* flightPtr) { int idx = hashFunc(flightNo); HashNode* node = (HashNode*)malloc(sizeof(HashNode)); strcpy(node->flightNo, flightNo); node->flightPtr = flightPtr; node->next = hashTable[idx]; hashTable[idx] = node; } Flight* searchHash(const char* flightNo) { int idx = hashFunc(flightNo); HashNode* cur = hashTable[idx]; while (cur != NULL) { if (strcmp(cur->flightNo, flightNo) == 0) { return cur->flightPtr; } cur = cur->next; } return NULL; }

逻辑说明:hashFunc采用 31 倍累积,和 Java 的String.hashCode思路一致,能有效降低“CA1234”这类数字+字母混合字符串的碰撞率;& 0x7FFFFFFF是为了把负数处理成正数。插入用头插法,代码短且不需要尾指针。查找时只在冲突链上比较,平均长度在 128 个槽、几百条数据下非常小。需要注意:哈希表里的flightPtr指向的是城市链表中的节点,不是复制品;如果程序退出时释放内存,只释放一次,不要按哈希表再释放一遍,否则会出现双重释放。

3. 航班查询与检索的核心算法实现:按号、按城市、按时间

3.1 按航班号精确查询:哈希命中后直接输出

核心算法的第一块是“按航班号查”。有了 2.3 节的哈希表,这个查询函数只需要三步:调searchHash,判断返回指针是否为空,非空则打印航班信息。这里还需要一个统一的打印函数,否则后面按城市查询也要重写一遍输出逻辑。

void printFlight(Flight* f) { if (f == NULL) return; printf("%-8s %-10s -> %-10s %02d:%02d 起飞 %02d:%02d 到达 票价 %d\n", f->flightNo, f->startCity, f->endCity, f->startHour, f->startMin, f->endHour, f->endMin, f->price); } int queryByFlightNo(const char* flightNo) { Flight* f = searchHash(flightNo); if (f == NULL) { printf("未找到航班 %s\n", flightNo); return 0; } printFlight(f); return 1; }

参数说明:%-8s是左对齐占 8 个字符,航班号一般不超过 6 位,留出空格让输出整齐;%02d保证时间输出成08:30而不是8:30。这个查询在课程设计的验收演示里最容易讲明白,时间复杂度 O(1),答辩老师常问“你用什么结构加速”,直接回答哈希表并指出冲突解决方式是拉链法即可。

需要注意:如果文件里存在重复航班号,insertHash的头插法会让后读入的记录覆盖前一条。课程设计的测试数据一般不会重复,但如果用真实数据,建议在插入前先查一次哈希,重复则打印警告或跳过。这样能避免输出结果与文件数据对不上。

3.2 按起点终点找直达与中转:BFS 找最少换乘

按起终点查询是这道题上难度的关键点。直达航班只需要遍历起点城市的outList,逐一比较终点城市;但“如果今天没有直达,能不能中转一次”是加分的点,也直接用到图的遍历。我一般用 BFS 而不是 DFS,因为 BFS 找到的路径是换乘次数最少的,适合“最少中转”这类问题。

#define MAX_QUEUE 200 typedef struct { char cities[MAX_QUEUE][MAX_NAME]; int front, rear; } Queue; void bfsTransfer(const char* start, const char* end) { int visited[MAX_CITY] = {0}; char prev[MAX_CITY][MAX_NAME]; char queue[MAX_QUEUE][MAX_NAME]; int head = 0, tail = 0; strcpy(queue[tail++], start); visited[getCityIndex(start)] = 1; strcpy(prev[getCityIndex(start)], ""); while (head < tail) { char cur[MAX_NAME]; strcpy(cur, queue[head++]); if (strcmp(cur, end) == 0) { printPath(prev, start, end); return; } Flight* f = cityTable[getCityIndex(cur)].outList; while (f != NULL) { int idx = getCityIndex(f->endCity); if (!visited[idx]) { visited[idx] = 1; strcpy(prev[idx], cur); strcpy(queue[tail++], f->endCity); } f = f->next; } } printf("未找到可从 %s 到 %s 的路径\n", start, end); }

逻辑说明:prev数组记录每个城市在 BFS 树里的前驱城市,搜索到终点后从end倒推回start,路径顺序正好是最少换乘。队列用数组模拟,容量MAX_QUEUE需要大于最大城市数,否则节点重复入队会溢出。这里假设了一个getCityIndex函数,把城市名映射到数组下标,常见实现是顺序扫描cityTable并比较strcmp,城市数量只有二十个左右,线性扫描完全可以接受。

printPath是递归打印前驱节点,或者用一个临时数组倒序输出;递归写法短但要注意城市链深度最多只有城市数,栈不会爆。这段 BFS 是答辩时最值得讲的部分,能体现出“图的深度优先/广度优先”是真实应用过的。

3.3 按起飞时间排序:排序前先想好“时间”怎么比

按时间排序看起来是复制一个数组然后调用快速排序,但坑在“时间”格式上。起飞时间由startHourstartMin两个 int 组成,排序比较时先比小时再比分钟,或者统一换算成startHour * 60 + startMin的分钟数。直接比较字符串"8:30"会得到错误结果,因为"10:00"会排在"9:00"前面。课程设计里用 C 语言写快速排序最常见,下面这段是我推荐的整体流程:

Flight* sortArray[MAX_FLIGHT]; int flightCount = 0; int cmpTime(const void* a, const void* b) { Flight* fa = *(Flight**)a; Flight* fb = *(Flight**)b; int ta = fa->startHour * 60 + fa->startMin; int tb = fb->startHour * 60 + fb->startMin; return ta - tb; } void sortByTime() { int i = 0; for (i = 0; i < cityCount; i++) { Flight* f = cityTable[i].outList; while (f != NULL) { sortArray[flightCount++] = f; f = f->next; } } qsort(sortArray, flightCount, sizeof(Flight*), cmpTime); for (i = 0; i < flightCount; i++) { printFlight(sortArray[i]); } }

参数说明:sortArray存的是Flight*指针,不是Flight值,这样避免了复制整条航线的开销,排序也只是交换八个字节的指针。qsort的比较函数原型是int (*)(const void*, const void*),所以把参数强转成Flight**再解引用一层,拿到真正的航班指针。ta - tb直接返回差值当比较结果,处理几百条数据没有任何溢出风险;如果数据量上万,建议改成return (ta > tb) - (ta < tb);更安全。

这里还要提醒一个选择:复制Flight*进数组后,排序结果是整个航班表按时间排列,不是只查某个城市的航班。课程设计要求若为“按起点城市+时间”,应在遍历outList时先判断f->startCity是否等于指定城市,只把匹配的放进数组。两种查询场景区分开,输出结果才不会让老师觉得“逻辑对不上需求”。

4. 航班查询与检索的流程图设计与输出结果整理

4.1 流程图的画法与符号规范:答辩老师的第一个问题都在这里

流程图是这份课程设计文档里占比很大、但往往写得最差的部分。很多同学用 Word 自带形状随便画几条线,流程符号混用,箭头方向不清,答辩老师一眼就看出来这是“事后补的”。流程图画法本身有一套约定:圆角矩形表示开始和结束,矩形表示处理步骤,菱形表示判断,平行四边形表示输入输出,箭头表示控制流向。针对航班查询系统,文档里应该有“主流程图”和“查询子流程图”两级,主流程图只描述系统初始化到进入菜单循环的宏观过程,查询子流程图再分“按航班号查询”“按城市查询”“按时间排序”三个分支。

图形含义在航班系统中的具体位置
圆角矩形开始/结束“航班管理系统启动”“退出系统”
平行四边形输入/输出读取文件数据、打印航班信息、输出结果
矩形处理建立邻接表、初始化哈希表、排序
菱形判断菜单选择、目录是否存在、哈希是否命中
箭头控制流从“输入菜单项”指向“判断菜单值”

4.2 主流程与子流程的文档化:从入口到查询的完整链路

主流程图可以按三段式描述:第一段是数据加载,程序启动后打开航班数据文件,循环读取每一行,插入邻接表和哈希表;第二段是菜单循环,打印操作选项,接收用户输入,判断菜单值是 1、2、3 还是 0,对应调用不同的查询函数;第三段是退出处理,释放哈希表和邻接表的内存。这个顺序决定了流程图里菱形判断的位置:菜单判断之后引出一个“是否为 0”的出口,为真则进入结束框,为假则继续执行查询函数。

子流程图里的关键判断点在“按城市查询”中:输入起点和终点后,先判断两个城市是否存在,不存在直接输出“城市不存在”;存在则遍历出发链,判断是否有直达航班,没有再看中转查询分支。这里建议用两级菱形表示,而不是把“到达终点”和“队列为空”混在一起判断。画图的工具用 ProcessOn 或者 draw.io 都可以,但要注意“用户管理模块流程图”那种带数据库交互的画法不适用于本系统,因为本程序不涉及账号权限,画复杂了反而暴露对业务不熟。

4.3 输出结果的格式化与测试记录整理

文档里的“输出结果”部分不是随便截三张图,而是要能说明每种查询都验证过。我一般会在printFlight的基础上再加一个统计行,让测试结果更直观:

void printResultHeader() { printf("\n========== 航班查询结果 ==========\n"); printf("%-8s %-10s %-10s %-12s %-12s %-6s\n", "航班号", "起点", "终点", "起飞时间", "到达时间", "票价"); } void printResultFooter(int count) { printf("----------------------------------\n"); printf("共查询到 %d 条航班\n", count); }

逻辑说明:printResultHeader打印表格标题,printResultFooter打印总条数。这样课程设计报告里的输出结果截图会显得规范,老师一眼能看到“查询成功”和“统计条数”,分数比只贴代码输出要高。建议测试记录至少包含四组:存在直达、存在中转、航班号不存在、按时间排序的第一条和最后一条。每组截图下面用一行字说明输入是什么、输出是否符合预期。

常见错误是只测试了正常数据,没有测“未查询到”的分支。答辩老师最常做的一件事就是输入一个不存在的航班号,看程序会不会崩溃或者输出乱码。如果searchHash返回 NULL 后直接打印了f->flightNo,就会出现段错误,这个问题在文档的“输出结果”里必须体现为一条“未找到”的正常提示。

5. 航班查询与检索从提交到答辩的收尾技巧

课程设计的文档交上去之后,真正拉开差距的是答辩表现。这部分不需要你改算法,但需要你对代码里几个关键参数和异常分支做到“问一句答三句”。我建议把注意力放在三个容易答不上来的点上。第一个是内存释放:书上说“数据结构要注重算法效率”,但老师更可能问你“你这段程序退出前有没有把动态内存释放干净”。你在main函数退出前,应该遍历所有城市,把outListinList的节点逐个free,再遍历哈希表释放HashNode。注意释放顺序:先释放航班节点,再释放城市名;如果在释放完航班节点后又去哈希表里取flightPtr,就是典型的悬空指针。

第二个是时间比较的边界:如果航班是跨天的,例如 23:50 起飞、次 01:30 到达,简单用endHour * 60 + endMin比较会得出“到达比出发早”的错误结论。课程设计的测试数据很少跨天,但答辩老师可能会“随口一问”。正确做法是给Flight增加一个isNextDay字段,排序时只比较起飞时间,到达时间仅展示;如果要做全程时长统计,时长 = (到达分钟 + 1440 * isNextDay) - 出发分钟。这个细节能体现你对业务边界有思考。

第三个是中文编码:Windows 上 Code::Blocks 用 GBK,Linux 上 GCC 默认 UTF-8,城市名从文件读入后如果编码不一致,strcmp永远不相等。最简单可靠的方案是强制约定输入文件为 UTF-8,并在读取后用setlocale(LC_ALL, "")让程序按本地环境处理中文字符。答辩演示前先在命令行跑一遍基础查询,确认“北京”和“上海”能正确匹配。

还想再给一个可操作的验证小技巧:答辩前准备一个只有 3 个城市、4 条航班的迷你数据文件,专门测中转逻辑。例如城市 A 到 B 有直达,但 A 到 C 必须经 B 中转。用这个文件跑bfsTransfer,确认输出的是A -> B -> C而不是直接报“未找到”。这段测试结果可以作为“算法正确性验证”放进文档的最后一节,比调一个几百条数据的航班文件更能讲清楚你的 BFS 思路。至此,这份航班查询与检索课程设计从数据建模、哈希索引、BFS 搜索到流程图整理,都形成了一条能复现、能解释的技术路径。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询