ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

航空订票系统课设:链表+哈希表完整实现与答辩避坑指南

航空订票系统课设:链表+哈希表完整实现与答辩避坑指南 简介这份数据结构课程设计航空订票系统文档面向计算机专业学生与数据结构学习者提供一套完整的课程设计参考方案。文档围绕航空订票业务展开涵盖总体设计、概要设计、详细设计、调试分析、时间复杂度分析、问题思考、算法改进设想、课设总结体会及附录等章节完整呈现了从需求分析到代码实现的开发脉络。系统功能包括航班信息录入、按航班号或起降城市查询、订票与满仓候补队列处理、退票及余票通知、航班信息修改与文件持久化存储并给出了单链表、等候订票队列等结构体定义与各模块算法说明。资源包为1个doc文档大小约1.18MB目录结构清晰便于按模块查阅。已有1292人学习下载适合需要完成数据结构课程设计、理解线性表实际应用或撰写实验报告的学生参考借鉴。1. 航空订票系统课设从链表到哈希表一个能过答辩的完整方案每年一到期末数据结构课程设计就成了计算机专业学生绕不开的一道坎。航空订票系统几乎是出现频率最高的选题之一原因很简单它天然覆盖了线性表、查找、排序、文件读写这几个核心知识点老师出题顺手学生做起来也有章可循。但真正动手时你会发现问题不在于“写不出来”而在于“怎么写才能既满足课设要求又能扛住答辩老师的追问”。我见过太多同学用一个大数组硬撑所有功能增删改查全靠遍历最后演示时卡得不行老师一句“你这时间复杂度多少”就答不上来了。这篇内容面向的是正在做数据结构课程设计、选题为航空订票系统的同学也适合教这门课的老师参考。我会把整个系统的设计思路、数据结构选型理由、核心代码实现、参数设置以及我当年踩过的坑全部讲清楚。你不需要有很强的工程经验只要学过 C 语言或 C 的基本语法跟着走就能搭出一个结构清晰、能跑通、能答辩的版本。如果你用的是 Java 或 Python思路完全一样只是语法换一下。整个方案的核心思路是用链表管理航班动态信息用哈希表加速航班号查找用文件做数据持久化用简单的冒泡或快排处理排序需求。不追求花哨的界面重点是把数据结构用对、用出理由。答辩时你能说清楚“为什么这里用链表不用数组”“哈希冲突怎么解决的”基本就稳了。2. 系统功能拆解与数据结构选型为什么链表加哈希是课设的最优解2.1 航空订票系统到底需要哪些功能模块先把需求理清楚不然后面写代码就是一团乱麻。一个标准的航空订票系统课设通常要求实现以下功能航班信息的录入与删除、航班信息的查询按航班号、按起点终点、乘客订票与退票、航班信息排序按时间或票价、数据的文件保存与读取。有些老师还会加一个“管理员登录”或者“查看某航班所有乘客”的功能这些属于加分项不是必须的。把这些功能映射到数据结构上你会发现核心操作其实就三类增删、查找、排序。增删对应的是线性结构查找对应的是查找结构排序对应的是排序算法。课设的评分点也基本围绕这三块展开。所以你的系统架构应该是一个主链表存所有航班每个航班节点下面挂一个乘客链表存该航班的订票记录再加一个哈希表用来按航班号快速定位。为什么不用数组因为航班信息是动态增减的数组扩容麻烦中间删除还要移动元素时间复杂度 O(n)。链表插入删除都是 O(1)前提是你已经找到了位置更适合这种场景。为什么还要加哈希表因为链表查找是 O(n)如果航班数量多每次按航班号查都要从头遍历效率太低。哈希表能把查找降到接近 O(1)而且哈希表本身就是数据结构课程的重点内容用上它答辩时多一个亮点。2.2 链表、哈希表、文件存储各自承担什么角色具体分工是这样的主链表负责存储所有航班节点每个节点包含航班号、起点、终点、起飞时间、票价、剩余座位数、乘客链表头指针。乘客链表每个节点存乘客姓名、身份证号、订票时间。哈希表用来建立航班号到航班节点指针的映射查找时先算哈希值直接跳到对应位置。文件存储用最简单的文本格式就行每行一条航班记录字段之间用逗号或竖线分隔。读取时逐行解析重建链表和哈希表。写入时遍历链表逐行输出。不要用二进制格式调试麻烦老师也看不清楚。注意哈希表的长度建议取一个质数比如 101 或 211这样取模后分布更均匀。如果你用的是除留余数法表长取质数是基本要求。2.3 核心结构体定义与参数说明下面是我一般会用的结构体定义用 C 语言写的C 的话把 typedef 去掉、用 class 也行。#define HASH_SIZE 101 // 哈希表长度取质数减少冲突 #define MAX_SEATS 200 // 单航班最大座位数 // 乘客节点 typedef struct Passenger { char name[32]; // 乘客姓名 char id[20]; // 身份证号 struct Passenger *next; } Passenger; // 航班节点 typedef struct Flight { char flightNo[10]; // 航班号如 CA1234 char origin[20]; // 起点城市 char dest[20]; // 终点城市 char depTime[10]; // 起飞时间格式 HH:MM float price; // 票价 int seatsLeft; // 剩余座位 Passenger *passHead; // 乘客链表头指针 struct Flight *next; // 主链表下一航班 } Flight; // 哈希表存航班节点指针 Flight *hashTable[HASH_SIZE];这里有几个参数需要解释。HASH_SIZE取 101 是因为它是质数而且比一般课设的航班数量大不少冲突概率低。MAX_SEATS设 200 是常见客机座位数你可以改成 150 或 300不影响逻辑。flightNo长度给 10 够用了国内航班号一般 6 到 8 个字符。depTime用字符串存是为了排序方便直接 strcmp 就能比大小不用转成时间戳。哈希函数用最简单的除留余数法把航班号所有字符的 ASCII 值加起来对表长取模int hashFunc(char *flightNo) { int sum 0; while (*flightNo) { sum sum * 31 (*flightNo); // 31 是常用乘子减少碰撞 flightNo; } return sum % HASH_SIZE; }乘子取 31 是 Java 里 String.hashCode 的经典做法分布比较均匀。你也可以用 131 或 1313效果类似。冲突处理用链地址法哈希表每个槽存一个链表头冲突了就挂上去。虽然我们哈希表存的是航班指针但冲突时多个航班会挂在同一个槽下面查找时需要沿着链表比对航班号。2.4 查找、插入、删除的时间复杂度对比为了让你答辩时有话可说我把关键操作的时间复杂度列一下。按航班号查找哈希表 O(1) 平均链表 O(n)。插入新航班链表头插 O(1)哈希表插入 O(1)。删除航班找到后 O(1)但找的过程哈希表 O(1)、链表 O(n)。按起点终点查找只能遍历链表 O(n)这个没法优化除非你再建一个索引。排序冒泡 O(n²)快排 O(n log n)课设用冒泡就够了数据量不大。老师如果问你“为什么不全用哈希表”你就说哈希表不支持按范围查找和排序链表虽然查找慢但遍历方便两者结合各取所长。这个回答基本能过关。3. 从零搭建订票系统链表操作、哈希查找与文件读写的完整代码路径3.1 初始化与航班录入头插法建链表系统启动时先初始化哈希表为空然后从文件读取已有航班数据。如果没有文件就从空链表开始。录入新航班的逻辑是创建一个 Flight 节点填好信息头插到主链表同时插入哈希表。Flight *flightHead NULL; // 主链表头指针 // 初始化哈希表 void initHash() { for (int i 0; i HASH_SIZE; i) { hashTable[i] NULL; } } // 插入航班到哈希表 void insertToHash(Flight *f) { int idx hashFunc(f-flightNo); // 头插法冲突时新节点放在槽头部 f-next hashTable[idx]; // 注意这里复用 next 指针会破坏主链表 hashTable[idx] f; }上面这个插入哈希表的代码有个问题Flight 结构体只有一个 next 指针主链表和哈希表冲突链都用它会互相干扰。解决办法有两个一是哈希表槽里不存 Flight 指针而是存一个单独的 HashNode 结构里面包含 Flight 指针和 HashNode 的 next二是给 Flight 加一个 hashNext 指针专门给哈希表用。我一般用第二种改起来简单。typedef struct Flight { // ... 其他字段同上 struct Flight *next; // 主链表指针 struct Flight *hashNext; // 哈希冲突链指针 } Flight; void insertToHash(Flight *f) { int idx hashFunc(f-flightNo); f-hashNext hashTable[idx]; hashTable[idx] f; }这样主链表和哈希表互不干扰。录入航班时先头插主链表再插入哈希表。头插主链表的代码void addFlight(char *no, char *org, char *dst, char *time, float price, int seats) { Flight *f (Flight *)malloc(sizeof(Flight)); strcpy(f-flightNo, no); strcpy(f-origin, org); strcpy(f-dest, dst); strcpy(f-depTime, time); f-price price; f-seatsLeft seats; f-passHead NULL; f-next flightHead; // 头插主链表 flightHead f; insertToHash(f); // 插入哈希表 }参数说明no是航班号字符串org和dst是城市名time是起飞时间price是票价seats是初始座位数。头插法的时间复杂度 O(1)但会导致链表顺序和录入顺序相反。如果你希望保持录入顺序用尾插法多维护一个尾指针就行。3.2 按航班号查找哈希表 O(1) 定位与冲突链遍历查找是订票系统的核心操作订票、退票、查询余票都要先找到航班。用哈希表查找的代码如下Flight *findFlight(char *flightNo) { int idx hashFunc(flightNo); Flight *p hashTable[idx]; while (p ! NULL) { if (strcmp(p-flightNo, flightNo) 0) { return p; // 找到了 } p p-hashNext; // 沿冲突链继续找 } return NULL; // 没找到 }逻辑很直接先算哈希值定位到槽然后遍历冲突链比对航班号。平均情况下冲突链很短查找接近 O(1)。最坏情况是所有航班都冲突到同一个槽退化成 O(n)但只要你哈希函数选得合理、表长取质数这种情况基本不会出现。这里有个细节strcmp返回 0 表示相等不要写成if (strcmp(...))那样是反的。我当年就在这里翻过车调试了半天才发现。3.3 订票与退票乘客链表的插入与删除订票的逻辑是先找到航班检查剩余座位是否大于 0然后创建乘客节点头插到该航班的乘客链表座位数减一。退票则是找到乘客节点从链表中删除座位数加一。// 订票 int bookTicket(char *flightNo, char *name, char *id) { Flight *f findFlight(flightNo); if (f NULL) return -1; // 航班不存在 if (f-seatsLeft 0) return -2; // 没座位了 Passenger *p (Passenger *)malloc(sizeof(Passenger)); strcpy(p-name, name); strcpy(p-id, id); p-next f-passHead; // 头插乘客链表 f-passHead p; f-seatsLeft--; return 0; // 成功 } // 退票 int cancelTicket(char *flightNo, char *id) { Flight *f findFlight(flightNo); if (f NULL) return -1; Passenger *p f-passHead; Passenger *prev NULL; while (p ! NULL) { if (strcmp(p-id, id) 0) { if (prev NULL) { f-passHead p-next; // 删除头节点 } else { prev-next p-next; // 删除中间或尾节点 } free(p); f-seatsLeft; return 0; } prev p; p p-next; } return -2; // 没找到该乘客 }退票的删除操作要注意头节点和中间节点的区别。头节点删除直接改头指针中间节点让前驱的 next 跳过当前节点。这是链表删除的标准写法答辩时老师很可能让你手写背也要背下来。3.4 文件保存与读取数据持久化的最小实现课设要求数据能保存到文件、下次启动能读回来。用文本文件最简单每行一个航班字段用竖线分隔乘客信息跟在航班后面或者单独存一个文件。我一般把乘客信息也放在同一行用分号隔开多个乘客。// 保存所有数据到文件 void saveToFile(char *filename) { FILE *fp fopen(filename, w); if (fp NULL) return; Flight *f flightHead; while (f ! NULL) { fprintf(fp, %s|%s|%s|%s|%.2f|%d|, f-flightNo, f-origin, f-dest, f-depTime, f-price, f-seatsLeft); Passenger *p f-passHead; while (p ! NULL) { fprintf(fp, %s,%s;, p-name, p-id); p p-next; } fprintf(fp, \n); f f-next; } fclose(fp); }读取时用fgets逐行读然后用strtok按竖线切割字段再解析乘客部分。注意strtok会修改原字符串所以要先拷贝一份。读取的代码稍微长一点但逻辑就是解析字符串、重建链表和哈希表这里不展开你按这个思路写就行。提示文件路径不要写死成绝对路径用相对路径比如 flights.txt这样换台电脑也能跑。答辩演示时提前把数据文件放在同目录下。4. 排序、去重与边界处理课设答辩最容易被追问的几个实现细节4.1 按票价排序冒泡排序在链表上的写法课设通常要求能按票价或起飞时间排序。链表排序用冒泡最直观虽然效率不高但数据量小的时候完全够用。链表冒泡和数组冒泡的区别在于交换的是节点内容而不是指针这样简单不容易出错。// 按票价升序排序交换节点数据不交换指针 void sortByPrice() { if (flightHead NULL) return; int swapped; Flight *p; do { swapped 0; p flightHead; while (p-next ! NULL) { if (p-price p-next-price) { // 交换两个节点的数据字段 Flight temp *p; *p *(p-next); *(p-next) temp; // 注意交换后 next 指针乱了需要修复 Flight *tmpNext p-next; p-next tmpNext-next; tmpNext-next p; swapped 1; } p p-next; } } while (swapped); }上面这个交换数据的写法有个坑直接交换整个结构体会把 next 指针也交换了导致链表断裂。正确的做法是只交换数据字段不交换指针。或者更简单交换节点的数据内容但保留各自的 next 指针。我一般会写一个 swapData 函数只交换 flightNo、origin、dest、depTime、price、seatsLeft、passHead 这些字段next 和 hashNext 不动。void swapData(Flight *a, Flight *b) { // 只交换数据字段不交换指针 char tmpNo[10], tmpOrg[20], tmpDst[20], tmpTime[10]; float tmpPrice; int tmpSeats; Passenger *tmpPass; // 逐个字段交换... }这样排序后链表结构不变哈希表也不需要更新因为哈希表存的是节点指针节点还在原来的位置只是数据变了。但注意如果按航班号排序哈希表的映射关系就不对了因为航班号变了。所以排序只影响显示顺序不影响查找。查找还是走哈希表没问题。4.2 航班号去重插入前先查哈希表录入新航班时如果航班号已经存在应该拒绝插入并提示用户。这个检查用哈希表做最快int addFlightSafe(char *no, ...) { if (findFlight(no) ! NULL) { return -1; // 航班号已存在 } // 执行插入... return 0; }如果不做去重同一个航班号会出现多个节点哈希表冲突链里会有重复查找时返回第一个匹配的但数据不一致退票可能退错航班。这是课设里常见的逻辑漏洞老师演示时如果输入重复航班号系统没反应或者出错就会扣分。4.3 座位数边界与空链表判断订票时座位数减到 0 就不能再订了这个边界要处理好。退票时如果航班已经满座seatsLeft MAX_SEATS说明没有乘客退票应该失败。空链表判断也很重要删除航班或乘客时如果链表为空直接返回错误码不要解引用空指针。// 删除航班 int deleteFlight(char *flightNo) { Flight *f findFlight(flightNo); if (f NULL) return -1; // 从主链表删除 Flight *p flightHead; Flight *prev NULL; while (p ! NULL p ! f) { prev p; p p-next; } if (prev NULL) { flightHead f-next; } else { prev-next f-next; } // 从哈希表删除 int idx hashFunc(flightNo); Flight *hp hashTable[idx]; Flight *hprev NULL; while (hp ! NULL hp ! f) { hprev hp; hp hp-hashNext; } if (hprev NULL) { hashTable[idx] f-hashNext; } else { hprev-hashNext f-hashNext; } // 释放乘客链表 Passenger *pass f-passHead; while (pass ! NULL) { Passenger *tmp pass; pass pass-next; free(tmp); } free(f); return 0; }删除操作要同时维护主链表和哈希表还要释放乘客链表的内存一步都不能少。漏了哈希表的删除下次查找还会找到已删除的节点这是野指针程序可能崩溃。4.4 内存泄漏排查valgrind 和手动检查C 语言课设最常见的问题就是内存泄漏。每次 malloc 都要有对应的 free删除节点时要先保存 next 指针再 free。如果你在 Linux 下开发用 valgrind 跑一下gcc -g -o airline airline.c valgrind --leak-checkfull ./airlinevalgrind 会告诉你哪些内存没释放。Windows 下可以用 Visual Studio 的内存检测工具或者自己仔细检查每个 malloc 的配对。我当年课设就因为忘记释放乘客链表被扣了分血泪经验。5. 避坑与排查课设答辩现场最容易翻车的五个问题5.1 哈希表查找返回了已删除的航班现象删除航班后按航班号还能查到显示的信息是乱码或者旧数据。原因删除时只从主链表摘除了节点没有从哈希表冲突链中移除。解决删除操作必须同时处理主链表和哈希表参考 4.3 的代码两个链表都要摘。5.2 文件读取后哈希表为空查找全部失败现象程序启动时从文件读入了航班主链表遍历能看到数据但按航班号查找总是返回 NULL。原因读取时只重建了主链表忘记调用 insertToHash 把节点插入哈希表。解决每读入一个航班节点插入主链表后立即插入哈希表。或者读取完成后遍历主链表统一建哈希表。5.3 排序后订票订到了错误的航班现象按票价排序后输入航班号订票结果订到了另一个航班。原因排序时交换了整个结构体把 flightNo 和 next 指针一起交换了导致哈希表指向的节点和实际数据不匹配。解决排序只交换数据字段不交换 next 和 hashNext 指针。或者排序后重建哈希表。5.4 退票时程序崩溃提示段错误现象退票操作输入一个不存在的身份证号程序直接崩溃。原因遍历乘客链表时没有判空或者删除头节点时没有正确处理 prev NULL 的情况。解决遍历前检查 passHead 是否为 NULL删除时区分头节点和非头节点。参考 3.3 的代码。5.5 多次录入同一航班号数据混乱现象同一个航班号录入两次系统都接受了订票时随机订到其中一个退票时退到另一个。原因插入前没有做去重检查。解决addFlight 开头调用 findFlight如果返回非 NULL 就拒绝插入并提示“航班号已存在”。6. 让课设多拿几分用快排替换冒泡、加一个按时间范围查询如果你已经跑通了上面的版本想再往上提一提有两个方向可以加分。第一个是把冒泡排序换成快速排序。链表快排的写法比数组快排绕一些但思路一样选一个基准节点把小于它的挂左边、大于它的挂右边递归处理。课设数据量不大快排的优势不明显但老师看到你用快排会认为你对排序算法掌握得更深。我一般会保留冒泡作为默认排序加一个菜单选项“使用快速排序”让老师自己选。第二个加分项是加一个按起飞时间范围查询的功能。比如输入“08:00”到“12:00”列出这个时间段内所有航班。实现很简单遍历主链表用 strcmp 比较 depTime 字符串落在范围内的输出。这个功能不需要额外数据结构但演示效果好老师会觉得你的系统更实用。// 按时间范围查询航班 void queryByTimeRange(char *start, char *end) { Flight *p flightHead; int found 0; while (p ! NULL) { if (strcmp(p-depTime, start) 0 strcmp(p-depTime, end) 0) { printf(%s %s-%s %s 余票%d\n, p-flightNo, p-origin, p-dest, p-depTime, p-seatsLeft); found 1; } p p-next; } if (!found) printf(该时间段无航班\n); }时间字符串用 HH:MM 格式strcmp 比较结果和实际时间先后一致因为都是两位数补零的。如果你的时间格式不统一比如有的写“8:00”有的写“08:00”比较就会出错。所以录入时统一格式化成两位小时。还有一个习惯我保持了多年每次写完一个模块立刻用几个边界数据测一下。空链表、单个节点、重复插入、删除头节点、删除尾节点这几个场景跑通了基本就不会有大问题。课设答辩前我会把测试用例写在一张纸上挨个过一遍比临时瞎点靠谱得多。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进