简介:这份数据结构课程设计航空订票系统文档,面向计算机专业学生与数据结构学习者,提供一套完整的课程设计参考方案。文档围绕航空订票业务展开,涵盖总体设计、概要设计、详细设计、调试分析、时间复杂度分析、问题思考、算法改进设想、课设总结体会及附录等章节,完整呈现了从需求分析到代码实现的开发脉络。系统功能包括航班信息录入、按航班号或起降城市查询、订票与满仓候补队列处理、退票及余票通知、航班信息修改与文件持久化存储,并给出了单链表、等候订票队列等结构体定义与各模块算法说明。资源包为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-check=full ./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”,比较就会出错。所以录入时统一格式化成两位小时。
还有一个习惯我保持了多年:每次写完一个模块,立刻用几个边界数据测一下。空链表、单个节点、重复插入、删除头节点、删除尾节点,这几个场景跑通了,基本就不会有大问题。课设答辩前,我会把测试用例写在一张纸上,挨个过一遍,比临时瞎点靠谱得多。
希望帮到你。
本文还有配套的精品资源,点击获取