news 2026/10/9 8:50:44

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据结构课程设计航空订票系统:链表、排序与文件操作实战

简介:这是一份面向高校计算机专业学生的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 done

1000条记录下,链表加载是毫秒级,按余票排序的冒泡法可能要几秒,这个体感就是O(n^2)的真实代价。如果你能在答辩时说出“冒泡实现在1000条数据下大约需要几秒,数据量再大就要换归并排序”,老师会觉得你是真正理解复杂度的人。

6.3 答辩加分项:日志输出和防御式编程

我给这类课设额外加过的两个小功能都很简单但效果好:一是操作日志,每次订票退票都打印一条带时间戳的记录,方便老师看到运行过程;二是对用户输入做防御检查,比如菜单选项越界、航班号为空、订票数量为非数字字符,都给出明确提示而不是直接崩溃。这些代码量不多,但对体验的提升很明显。

我现在拿到任何链表类的课程设计,都会先写清空内存和边界输入的测试用例,再写功能代码。这个习惯让我在正式项目里少踩了很多内存泄漏的坑。希望这份整理能帮你在课程设计这条路上少走一段弯路,把链表、排序和文件操作真正变成自己的东西。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 8:50:01

Milvus多租户方案实战:用Partition Key实现数据隔离与高效检索

刚接触向量数据库的时候&#xff0c;我一度以为多租户只是个"数据库层面顺手支持一下"的小功能。真正把带十来个企业客户的RAG服务推进生产之后才发现&#xff0c;多租户方案的选型能直接决定你半夜被叫起来几次。Milvus这类向量数据库也一样&#xff0c;看起来无非是…

作者头像 李华
网站建设 2026/10/9 8:47:49

SpringBoot仓储管理系统实战:从需求到部署的全流程解析

做了几年Java后端&#xff0c;也带过不少毕业设计项目&#xff0c;我太熟悉“SpringBoot 仓储管理系统”这个选题了。你搜一下“SpringBoot、仓储管理系统、智能仓库、库存管控、物料追踪系统”这几个关键词&#xff0c;跳出来的基本都是同一类东西&#xff1a;用 SpringBoot 写…

作者头像 李华
网站建设 2026/10/9 8:47:21

T3 Stack 全栈实战:从 create-t3-app 到部署,绕过那些默认配置的坑

t3code 这个代号&#xff0c;是我当时给一个全栈 Web 应用随手起的仓库名。t3 指的不是数字三&#xff0c;而是前端圈里传得很广的那套 T3 Stack&#xff1a;TypeScript、Tailwind CSS、tRPC&#xff0c;再让 Next.js 当胶水把前后端串起来。项目本身是一个内部用的小型内容管理…

作者头像 李华
网站建设 2026/10/9 8:46:28

二分查找与二分答案:C语言实现、边界处理与竞赛实战

P8088&#xff0c;『JROI-5』Autumn&#xff0c;难度普及&#xff0c;标签里简简单单四个字&#xff1a;二分查找。第一次看到这道题的人&#xff0c;多半觉得这就是一道套模板的水题。但带过几年算法竞赛我就明白&#xff0c;凡是在“普及”这个档位被反复讨论的二分题&#x…

作者头像 李华
网站建设 2026/10/9 8:46:28

二分答案实战解析:从P8088看算法竞赛中的二分查找技巧

很多刚接触算法竞赛的朋友一听到“二分查找”这四个字&#xff0c;脑子里浮现的往往是“在一个有序数组里找一个数”的模板题。但真上了考场&#xff0c;二分查找出场的方式远比这个丰富得多&#xff0c;尤其是当它化身为“二分答案”的时候&#xff0c;整道题的难度和思维量会…

作者头像 李华
网站建设 2026/10/9 8:44:50

Hookify:让Claude-code自动化定制更简单的插件管理器

Claude-code 的玩法这两年变化很快。很多人装了 npm 上的anthropic-ai/claude-code&#xff0c;敲几行命令让它在终端里写代码、改文件&#xff0c;觉得已经很顺手。但真正让 Claude-code 从一个“有点聪明的命令行助手”变成“能嵌进自己工作流里的自动化引擎”的关键&#xf…

作者头像 李华