资讯详情

数据结构课程设计航空订票系统:链表、排序与文件操作实战

📅 2026/10/9 8:50:58 | 华诺云谱 👁 阅读
数据结构课程设计航空订票系统:链表、排序与文件操作实战
简介这是一份面向高校计算机专业学生的C语言数据结构课程设计报告主题为航空订票系统围绕航班信息录入、航线查询、订票、退票和航班信息修改等业务场景给出了完整的系统设计方案。资源为1个doc文档压缩包大小约1.18MB目前已有1293人学习浏览。文档结构清晰依次包含总体设计、概要设计、详细设计、调试分析、测试数据及截图、时间复杂度分析、问题思考、算法的改进设想、课设总结体会、附录源代码和主要参考文献可直接用作课程设计报告的写作范本和源码参考。在设计中系统以单链表和队列为主要数据结构定义了航班信息结构体、客户订单结构体以及等候订票队列并以模块化方式说明了录入、查询、订票、退票、文件读写等功能的算法流程。调试分析部分配有测试数据和运行截图便于对照验证程序正确性时间复杂度分析则帮助读者评估各模块性能附录中的完整源代码可为进一步改进和二次开发提供基础。1. 数据结构课程设计航空订票系统一门课设如何把链表、排序和文件操作全部串起来“数据结构课程设计航空订票系统”听起来只是每个计算机专业学生都要过的一道坎但真正动手之后你会发现它不是简单的增删改查而是把链表、排序、查找、文件读写这些核心知识点压缩进一个完整业务流程里。航班信息的动态增删、订票时的余票判断、退票后的状态回滚、按时间和余票排序每一个功能背后都对应一种数据结构和一种算法策略。这套系统最适合两类人一类是刚学完数据结构、准备用课程设计检验自己掌握程度的人另一类是工作后想拿一个完整的C语言项目练手、顺便补链表和文件操作短板的人。这篇文章不讲空泛理论直接沿着需求拆解、存储选型、代码实现、踩坑排查、验证进阶这条路径走跟着做就能跑通做完也能讲清楚。2. 需求拆解与存储选型为什么说这个系统的灵魂是“链表”不是“界面”2.1 先画功能清单从用例到数据流我一般拿到这种课设题目第一步不是写代码而是先在纸上把功能拆成四类查询类、订票类、退票类、管理类。查询类包括按航班号查、按目的地查、查看全部航班订票类要处理余票判断、座位数更新、写回文件退票类要处理已订状态的回滚管理类一般包括航班信息的录入、删除、修改和排序。把功能映射到数据结构上你会发现这个系统天然适合用链表来组织功能模块对应数据结构操作涉及核心知识点航班/目的地查询链表遍历 字符串匹配查找算法订票节点定位 字段更新 写文件链表访问退票节点定位 状态回滚条件判断航班删除节点摘除 内存释放链表删除按时间/余票排序链表排序排序算法文件持久化顺序读写 格式化解析文件操作这个表列清楚之后你会发现整个系统的主角是链表节点和节点上的字段控制台界面反而只是外壳。这也是这个课设的评分重点老师关心的是你有没有把数据结构用对而不是界面多漂亮。2.2 存储结构选型链表、顺序表还是文件航班数据在程序运行期间的存储方式不外乎三种顺序表数组、链表、文件直接读写。很多人一开始会选数组因为数组好理解下标访问方便。但航班系统的特点是数据量不确定且需要频繁增删——订票和退票不会改变航班数量但管理员录入新航班、删除停飞航班是常规操作。数组做插入和删除的时间复杂度是O(n)而且需要移动大量元素链表只要改指针时间复杂度是O(1)。文件直接读写的问题更明显每次订票都要打开文件、定位、改写、关闭操作频繁时效率低而且一旦中途崩溃文件状态不可控。所以我更推荐的做法是文件只负责持久化程序启动时一次性把数据读入链表所有业务操作在链表上进行需要保存时再整体写回文件。这样链表结构负责动态性文件负责存储两边各司其职。2.3 核心数据结构定义与初始化数据结构定义是整个系统的基础字段设计直接决定后面代码好不好写。我一般会这样定义航班节点#include stdio.h #include stdlib.h #include string.h typedef struct Flight { char flightNo[10]; // 航班号如 FL1001 char origin[20]; // 出发城市 char dest[20]; // 到达城市 char departTime[12]; // 起飞时间格式 HH:MM int totalSeats; // 总座位数 int bookedSeats; // 已订座位数 float price; // 票价 struct Flight* next; // 指向下一个节点的指针 } Flight;航班号用定长字符数组而不是动态分配是为了减少内存管理的负担。定长数组在拷贝和比较时直接strcpy/strcmp不会因为malloc/free次数太多而出现内存碎片。totalSeats和bookedSeats分开存而不是只存一个余票字段原因是退票时我们需要知道已经订了多少如果只存余票退票时还得反过来推算逻辑上容易绕晕。初始化链表时用不用头节点很多教材里两种都讲但实际做课设我建议加一个空的头节点。头节点的next指向第一个真实航班节点这样插入和删除操作不需要对“第一个节点”做特殊处理代码会简单很多。Flight* createList() { Flight* head (Flight*)malloc(sizeof(Flight)); if (head NULL) { printf(内存分配失败\n); return NULL; } strcpy(head-flightNo, HEAD); head-next NULL; return head; }这里注意头节点只承担“哨兵”角色里面的业务字段不会被访问。malloc之后一定要判断是否为空很多同学在内存不足时直接往下走程序就会在后续操作中崩溃。3. 用C语言把订票核心流程跑通建表、查询与订票3.1 从文件加载航班数据到链表数据文件我习惯用纯文本格式每一行代表一个航班字段之间用空格分隔。这种格式的好处是可以用fscanf直接解析也方便手工构造测试数据。加载函数的任务是把文件的每一行读出来生成节点并挂到链表尾部。void loadFromFile(Flight* head, const char* filename) { FILE* fp fopen(filename, r); if (fp NULL) { printf(文件 %s 不存在自动创建\n, filename); fp fopen(filename, w); if (fp NULL) { printf(无法创建文件\n); return; } fclose(fp); return; } Flight* tail head; while (tail-next ! NULL) { tail tail-next; } Flight* temp (Flight*)malloc(sizeof(Flight)); while (fscanf(fp, %s %s %s %s %d %d %f, temp-flightNo, temp-origin, temp-dest, temp-departTime, temp-totalSeats, temp-bookedSeats, temp-price) 7) { temp-next NULL; tail-next temp; tail temp; temp (Flight*)malloc(sizeof(Flight)); } free(temp); fclose(fp); }调用fscanf时有一个很隐蔽的问题如果最后一次读取失败temp里可能残留上一次的数据所以我在读取成功的分支里才把节点挂到链表上并在循环结束后把那个“多分配出来”的临时节点释放掉。temp-next NULL必须在挂链之前设置否则尾节点会指向一个未初始化的地址。参数说明tail指针从head开始一直移动到当前链表的尾部用尾插法保持文件里的航班顺序和链表顺序一致。如果你希望新加载的记录放在链表头部也可以在读取时选择头插法那样顺序会反转需要注意。3.2 按航班号和按目的地查询两种匹配策略查询功能是高频操作订票前必须查到目标航班。按航班号查询用精确匹配按目的地查询用模糊匹配两种情况我都建议写成独立的函数方便各自调整匹配策略。Flight* findFlightByNo(Flight* head, const char* no) { Flight* p head-next; while (p ! NULL) { if (strcmp(p-flightNo, no) 0) { return p; } p p-next; } return NULL; } void findFlightByDest(Flight* head, const char* dest) { Flight* p head-next; int found 0; while (p ! NULL) { if (strcmp(p-dest, dest) 0) { printf(%s %s - %s %s 余票%d\n, p-flightNo, p-origin, p-dest, p-departTime, p-totalSeats - p-bookedSeats); found 1; } p p-next; } if (!found) { printf(没有找到前往 %s 的航班\n, dest); } }按航班号查询返回节点指针而不是打印信息是为了让订票、退票函数可以直接复用这个查找结果避免重复遍历链表。按目的地查询是“一对多”场景所以用遍历打印更合适同时用一个found标志判断是否查到结果。这里的strcmp是精确匹配如果你想做包含匹配改成strstr(p-dest, dest)就能支持目的地关键字模糊搜索。3.3 订票事务从余票判断到写回文件订票是这个系统的核心动作逻辑上要严格按顺序执行先查航班再判断余票然后更新已订座位数最后提示用户。这里最容易犯的错误是“查完就订”跳过了余票判断。int bookTicket(Flight* head, const char* no, int num) { if (num 0) { printf(订票数量必须大于0\n); return 0; } Flight* f findFlightByNo(head, no); if (f NULL) { printf(航班 %s 不存在\n, no); return 0; } if (f-bookedSeats num f-totalSeats) { printf(余票不足当前余票 %d\n, f-totalSeats - f-bookedSeats); return 0; } f-bookedSeats num; printf(订票成功%s 已订 %d/%d\n, no, f-bookedSeats, f-totalSeats); return 1; }返回值的设计很关键函数返回int而不是void成功返回1失败返回0。这样调用方可以根据返回值决定是否要写回文件也能在失败时给出不同提示。num 0的判断很多人会漏如果用户输入0或负数不加判断的话bookedSeats不会被增加但也不会报错用户会误以为订票成功。写回文件的操作我一般放在订票流程的最后用一个独立的saveToFile函数完成。这样做的好处是如果一次订多张票的过程中某一步失败文件不会处于半更新状态。void saveToFile(Flight* head, const char* filename) { FILE* fp fopen(filename, w); if (fp NULL) { printf(无法打开文件 %s 写入\n, filename); return; } Flight* p head-next; while (p ! NULL) { fprintf(fp, %s %s %s %s %d %d %.1f\n, p-flightNo, p-origin, p-dest, p-departTime, p-totalSeats, p-bookedSeats, p-price); p p-next; } fclose(fp); }用w模式打开文件会直接清空原文件内容再写入所以每次保存都是全量写入。数据量到几千条航班时这种方式的性能也可以接受课程设计场景完全够用。4. 退票与排序容易翻车但分值最高的两个点4.1 退票三种状态的正确处理退票逻辑看似是订票的逆操作但它有三个边界状态航班不存在、没有订过票、退票后座位数不能为负。这三个状态如果不分开处理程序会得到错误的余票数。int refundTicket(Flight* head, const char* no, int num) { if (num 0) { printf(退票数量必须大于0\n); return 0; } Flight* f findFlightByNo(head, no); if (f NULL) { printf(航班 %s 不存在\n, no); return 0; } if (f-bookedSeats 0) { printf(该航班没有已订票记录无法退票\n); return 0; } if (f-bookedSeats - num 0) { printf(退票数量超过已订数量当前已订 %d\n, f-bookedSeats); return 0; } f-bookedSeats - num; printf(退票成功%s 剩余已订 %d\n, no, f-bookedSeats); return 1; }这里最关键的是“退票数量超过已订数量”的判断。很多人只写了bookedSeats - num没有检查会不会减成负数。这种错误在测试时不容易发现因为正常测试都是退1张但一旦用户连续退票就会翻车。判断顺序也有讲究先查航班、再查是否订过、最后查数量是否合法顺序不能调换否则会出现空指针解引用或者负数结果。4.2 按起飞时间和余票排序不改链式结构的冒泡法排序是这个课设的加分项但也是事故高发区。链表排序有两种思路一种是交换节点里的数据字段另一种是改变节点的next指针。我强烈建议课程设计用第一种——交换数据字段。理由很简单交换数据不会破坏链表结构不需要处理前驱节点的next指针不会出现断链。void sortByTime(Flight* head) { if (head NULL || head-next NULL) return; Flight* p; Flight* q; int n 0; for (p head-next; p ! NULL; p p-next) n; for (int i 0; i n - 1; i) { p head-next; for (int j 0; j n - 1 - i; j) { q p-next; if (strcmp(p-departTime, q-departTime) 0) { swapFlightData(p, q); } p q; } } } void swapFlightData(Flight* a, Flight* b) { Flight temp; strcpy(temp.flightNo, a-flightNo); strcpy(temp.origin, a-origin); strcpy(temp.dest, a-dest); strcpy(temp.departTime, a-departTime); temp.totalSeats a-totalSeats; temp.bookedSeats a-bookedSeats; temp.price a-price; strcpy(a-flightNo, b-flightNo); strcpy(a-origin, b-origin); strcpy(a-dest, b-dest); strcpy(a-departTime, b-departTime); a-totalSeats b-totalSeats; a-bookedSeats b-bookedSeats; a-price b-price; strcpy(b-flightNo, temp.flightNo); strcpy(b-origin, temp.origin); strcpy(b-dest, temp.dest); strcpy(b-departTime, temp.departTime); b-totalSeats temp.totalSeats; b-bookedSeats temp.bookedSeats; b-price temp.price; }先遍历一遍统计节点数n然后用双重循环做冒泡排序。外层循环控制轮数内层循环里p和q是相邻的两个节点比较它们的departTime。departTime是HH:MM格式的字符串字典序和实际时间序一致所以可以直接strcmp不需要转换成分钟数再比较。如果按余票量排序只需要把比较条件换成p-totalSeats - p-bookedSeats q-totalSeats - q-bookedSeats。这里有个细节内层循环每轮结束后p指向这一轮最后一组比较的第二个节点下一轮要重头开始所以p要重新赋值为head-next不能接着上一轮的位置继续。这个排序写法的时间复杂度是O(n^2)数据量小没问题但如果你在答辩时主动提到“数据量大时会换成归并排序”会显得你对复杂度有真实理解。4.3 链表销毁收尾时不留下内存泄漏很多课程设计的代码能跑完流程但退出程序前没有释放链表内存。短时间运行看不出问题但如果把这个系统嵌入到一个需要反复初始化的场景里内存泄漏就会越积越多。链表销毁的正确方式是“先保存下一个节点的指针再释放当前节点”。void destroyList(Flight* head) { if (head NULL) return; Flight* p head; Flight* temp; while (p ! NULL) { temp p-next; free(p); p temp; } }注意必须先取p-next再free(p)因为free之后p指向的内存已经归还系统再访问p-next是未定义行为程序可能立即崩溃也可能在运行很久之后才出问题。这类野指针问题是C语言里最难排查的bug之一。5. 避坑链表课程设计的五个经典翻车现场5.1 遍历一次之后头指针丢了现象第一次查询正常第二次查询或者再次遍历时程序崩溃或者打印出乱码。原因某个函数里用了p head;然后一路p p-next函数结束时head本身没变但如果在函数内部不小心写成了head head-next头指针就被改了。特别是代码里同一个变量名既当遍历指针又当头指针时最容易发生。解决约定俗成的规矩是任何函数内只允许用局部指针遍历链表head作为入口参数只读使用。如果确实需要修改链表头部用返回值把新的头指针传出去。5.2 删除节点之后内存没有释放现象反复执行“删除航班”操作之后程序内存占用不断上涨。原因删除节点时只做了prev-next p-next没有free(p)。节点从链表上摘除了但堆上分配的内存还在成为游离块。解决删除一个节点后立即free(p)并且把p置为NULL避免后面误用这个已经失效的指针。这个习惯应该成为一种条件反射。5.3 fscanf读取时字段错位导致数据全是乱的现象文件加载成功后打印航班号正常但打印价格或余票时出现巨大数字。原因格式字符串和文件实际格式不一致。比如文件里票价是650.0但fscanf里写的是%d解析出来的值就会是某个随机整数。还有一种情况是字段里混入了逗号或制表符空格分隔失效。解决打开数据文件人工检查每一行的分隔符确保fscanf的格式字符串和文件完全对应。我在加载函数里加了一个计数器如果读到的有效记录数和文件行数不一致立刻打印告警方便定位格式问题。5.4 排序时改动next指针导致死循环现象按余票排序时程序卡死CPU占用100%。原因排序时试图用“交换节点位置”的方式把p-next和q-next交叉赋值结果链表变成了环。链表一旦成环遍历永远走不到NULL死循环就出现了。解决课程设计阶段统一用“交换数据字段”的方式排序不要动next指针。这样排序的时间复杂度虽然是O(n^2)但正确性有保证。等你真正理解了链表指针操作再考虑优化成插入排序或归并排序。5.5 订票成功后没有写回文件重启程序数据消失现象程序运行期间一切正常关掉程序重新打开之前订的票全没了。原因所有操作只在内存链表上进行没有调用保存函数。文件里的数据是旧版本。解决在订票、退票、删除航班、新增航班这四个会改变数据的操作之后统一在main函数的流程末尾调用一次saveToFile(head, flights.txt)或者每次修改后立即保存。我建议统一在main里保存因为分散保存容易出现“某条分支漏保存”的问题。6. 验证与进阶从能跑到答辩能讲清楚6.1 边界用例手动测试清单代码写完不是终点验证才是。我一般会用一组针对性的用例来测试系统边界订0张票、订超过剩余座位的票、退0张票、退超过已订数量的票、查询不存在的航班号、查询不存在的目的地、删除链表里的第一个节点、删除最后一个节点、对只有一条记录的链表排序。这一组用例跑完大部分隐藏的边界问题都会暴露出来。6.2 数据规模与性能粗测课程设计的数据量一般不大但你可以用脚本生成一个1000条航班记录的测试文件感受一下遍历和排序的性能差异。用shell的一行循环就能生成for i in $(seq 1 1000); do echo FL$(printf %04d $i) A市 B市 $(printf %02d $((i % 24))):00 200 $((i % 190)) $((300 i)) big_test.txt done1000条记录下链表加载是毫秒级按余票排序的冒泡法可能要几秒这个体感就是O(n^2)的真实代价。如果你能在答辩时说出“冒泡实现在1000条数据下大约需要几秒数据量再大就要换归并排序”老师会觉得你是真正理解复杂度的人。6.3 答辩加分项日志输出和防御式编程我给这类课设额外加过的两个小功能都很简单但效果好一是操作日志每次订票退票都打印一条带时间戳的记录方便老师看到运行过程二是对用户输入做防御检查比如菜单选项越界、航班号为空、订票数量为非数字字符都给出明确提示而不是直接崩溃。这些代码量不多但对体验的提升很明显。我现在拿到任何链表类的课程设计都会先写清空内存和边界输入的测试用例再写功能代码。这个习惯让我在正式项目里少踩了很多内存泄漏的坑。希望这份整理能帮你在课程设计这条路上少走一段弯路把链表、排序和文件操作真正变成自己的东西。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑