简介:这份数据结构课程设计文档面向计算机相关专业学生与课程设计指导教师,围绕航空订票系统的完整实现展开,帮助读者掌握线性表在实际业务场景中的应用。文档共1个doc文件,压缩包约1.18MB,内容涵盖总体设计、概要设计、详细设计、调试分析、测试数据及截图、时间复杂度分析、问题思考、算法改进设想、课设总结体会与附录源代码等模块。系统功能包括航班录入、按航班号或起降城市查询、订票与满仓候补队列、退票及余票处理、航班信息修改与文件持久化存储,并给出单链表、等候订票队列等结构体定义与各模块算法说明。已有1292人学习,适合需要参考完整课设方案、理解链表增删改查与队列应用、撰写实验报告或准备答辩的读者,可据此快速梳理设计思路、对照源码调试并完成自己的课程设计。
1. 航空订票系统课设拆包:一份能跑通的 C 语言链表实战文档
如果你正在做数据结构课程设计,选题又恰好是航空订票系统,那这份 30 页的文档大概率能帮你省下不少翻车时间。它不是那种只给题目不给代码的“空壳课设”,而是把总体设计、概要设计、详细设计、调试分析、时间复杂度、源代码全部摊开写完了。核心数据结构就两块:航班信息用单链表串起来,每个航班下面挂两个子结构——已订票乘客链表和等候订票队列。功能覆盖录入、查询、订票、退票、修改航班信息、文件读写六个模块,代码量不大但五脏俱全。适合两类人:一是课设赶工期、需要一份能编译能演示的参考实现;二是刚学完链表和队列、想找一个完整项目把增删改查串一遍的练手者。文档里连调试时踩过的坑都写进去了,比如字符串赋值用错、break 提前跳出循环、余票只减一张,这些细节比代码本身更值钱。
2. 数据结构选型:为什么是单链表加链队列,而不是数组
2.1 航班主链表的插入方式与时间复杂度取舍
文档里录入模块的做法是“查找单链表的链尾,在链头插入新结点”。这句话初看有点矛盾——既然要找链尾,为什么又插在链头?实际代码里用的是头插法:p->next = H->next; H->next = p;,新航班直接挂在头结点后面。头插的好处是插入本身 O(1),不需要遍历到尾;代价是航班在链表里的顺序和录入顺序相反。对于课设演示来说,浏览时能看到所有航班就行,顺序反了不影响功能。但如果你想让航班按起飞时间或航班号有序排列,头插就不合适了,得改成尾插或者插入时排序。
这里有个选型上的关键判断:为什么不用数组存航班?数组的优点是随机访问快,查询第 i 个航班 O(1);但订票系统里航班数量是动态的,今天录入 5 条,明天可能加到 20 条,数组扩容要么浪费空间要么频繁 realloc。链表虽然查询要 O(n),但插入删除不用搬移元素,而且每个航班下面挂的乘客链表和等候队列本身就是动态结构,用链表更自然。文档里时间复杂度分析写“录入为 O(1)”,严格说头插确实是 O(1),但前提是你不需要先找到链尾。如果按概要设计里写的“查找链尾再插入”,那就是 O(n) 了。这里文档的表述和代码有细微出入,以代码为准。
2.2 乘客链表与等候队列的双结构设计
每个航班结点lineinfo里有两个指针:Lnode *order指向已订票乘客链表的头结点,linkqueue *wait指向等候替补队列。这个设计是整份课设最值得看的部分。已订票乘客用单链表存,因为订票是头插、退票要按姓名和票号查找后删除,链表都支持。等候队列用链队列,front 和 rear 两个指针,入队在队尾、出队在队头,符合先来先服务。
为什么不用一个链表同时管已订票和等候?因为两者的操作语义不同。已订票乘客需要按票号精确查找和删除,等候乘客只需要按入队顺序通知。分开之后,退票逻辑就清晰了:先删已订票乘客结点,然后检查wait队列是否为空,非空则出队一个等候乘客,把他插入已订票链表,余票不变;如果等候队列为空,才把余票加回去。文档里退票模块的流程图就是这个逻辑。这个双结构设计在课设级别算比较完整的,比那些只用一个数组存乘客的实现高出一个档次。
2.3 结构体定义里的字段规划与内存布局
文档给出了四个结构体:wat_ros(等候乘客)、pqueue(等候队列)、ord_ros(订单/乘客)、airline(航班)。字段规划有几个点值得注意。航班号air_num[7]和飞机型号plane_num[10]用定长字符数组,这是 C 语言课设的常规做法,好处是文件读写时格式固定、方便用fscanf和fprintf。日期拆成year[5]、month[3]、day[3]三个字段,起飞和降落时间拆成qhour[3]、qminute[3]、jhour[3]、jminute[3],这样在查询和显示时可以灵活拼接,也方便按日期比较。
票价price和折扣zhekou用 float,余票量tkt_sur和乘员定额tkt_amt用 int。这里有个隐患:float 做等值比较会有精度问题,但课设里票价只用于显示和简单计算,不涉及精确比较,所以问题不大。乘客结构体ord_ros里的票号piaohaio[20]是拼接生成的:航班号 + 年 + 月 + 日 + 订票前余票量。这个设计保证了同一航班同一天不同订单的票号唯一,因为余票量在变。文档里用itoa把余票量转成字符串再strcat拼接,逻辑是对的,但itoa不是标准 C 函数,在 VC++6.0 里能用,换到 GCC 或 Clang 就编译不过。这是后面避坑章节要展开的点。
3. 核心模块实现:从录入到退票的代码拆解
3.1 录入模块:头插法建表与文件写入
录入模块的代码在文档里叫Greatelist,函数签名是int Greatelist(lineair &H, int n)。注意这里用了引用传参&H,这是 C++ 的语法,但文件扩展名如果是.c就会编译报错。VC++6.0 对.cpp文件支持引用,所以文档说“在 VC++6.0 上编译运行”是前提。函数先给头结点分配空间,然后循环 n 次,每次分配一个航班结点,用scanf读入所有字段,余票量初始化为乘员定额,然后头插。
int Greatelist(lineair &H, int n) { lineinfo *p; // 头结点分配 H = (lineair)malloc(sizeof(lineinfo)); H->next = NULL; H->order = (linklist)malloc(sizeof(Lnode)); H->order->next = NULL; H->wait = (linkqueue*)malloc(sizeof(linkqueue)); H->wait->rear = H->wait->front = NULL; for (int i = 0; i < n; i++) { p = (lineair)malloc(sizeof(lineinfo)); // 读入航班信息,注意字段顺序要和 printf 提示一致 scanf("%s%s%s%s%s%s%s%s%s%s%s%f%f%d", p->qdname, p->zhname, p->air_num, p->plane_num, p->year, p->month, p->day, p->qhour, p->qminute, p->jhour, p->jminute, &p->zhekou, &p->price, &p->tkt_amt); p->tkt_sur = p->tkt_amt; // 初始余票等于定额 p->order = (linklist)malloc(sizeof(Lnode)); p->order->next = NULL; p->wait = (linkqueue*)malloc(sizeof(linkqueue)); p->wait->rear = p->wait->front = NULL; p->next = H->next; // 头插 H->next = p; } return 1; }参数说明:H是航班链表头指针的引用,n是要录入的航线条数。scanf的格式串里%s读字符串,%f读 float,%d读 int。注意zhekou和price是 float,必须传地址&p->zhekou。代码里每个航班结点都单独分配了order和wait,这是对的,因为每个航班的乘客和等候队列是独立的。但头结点的order和wait分配了却没用上,属于冗余,不影响运行但浪费一点内存。
3.2 订票模块:余票判断、票号生成与等候队列入队
订票函数Dinpiao是整份代码里最长的。逻辑分三层:先按航班号和日期查找,找到后判断余票是否足够;够则输入乘客信息、生成票号、头插到乘客链表、余票减去订票量;不够则遍历所有航班,找同月同起降城市且有余票的替代航班;如果替代航班也没有,就把乘客信息入等候队列。
// 票号生成:航班号 + 年 + 月 + 日 + 订票前余票量 char *b = (char*)malloc(sizeof(char)); strcpy(q->airnum, p->air_num); itoa(p->tkt_sur, b, 10); // 余票量转字符串 strcpy(q->piaohaio, p->air_num); strcat(q->piaohaio, p->year); strcat(q->piaohaio, p->month); strcat(q->piaohaio, p->day); strcat(q->piaohaio, b); // 拼接成唯一票号 p->tkt_sur -= m; // 余票减少 q->next = p->order->next; // 头插到乘客链表 p->order->next = q;这里有几个参数要盯住。m是订票张数,必须m <= p->tkt_sur才允许订。itoa的第三个参数 10 表示十进制。票号拼接的顺序是航班号在前、日期在中、余票量在后,这样同一航班同一天的不同订单,因为余票量不同,票号就不会重复。但有个边界:如果两个乘客同时订票,余票量在第一次订票后已经变了,第二次生成的票号自然不同。课设是单机单用户,不存在并发,所以这个设计够用。
等候队列入队的代码在文档里被截断了,但根据结构体定义可以补全。qnode有name、phone、next,入队就是s->next = NULL; p->wait->rear->next = s; p->wait->rear = s;,如果队列为空则front和rear都指向s。这部分文档没贴全,但逻辑是标准的链队列入队。
3.3 退票模块:双链表查找与等候乘客通知
退票的输入是乘客姓名和票号。先在航班的order链表里按姓名和票号查找,找到则删除结点,然后判断wait队列是否为空。非空则出队一个等候乘客,把他插入order链表,余票不变;空则tkt_sur += 退票数。
// 退票核心逻辑(根据文档描述补全) Lnode *pre = p->order; Lnode *cur = p->order->next; while (cur) { if (strcmp(cur->name, name) == 0 && strcmp(cur->piaohaio, ticket_id) == 0) { pre->next = cur->next; // 删除结点 int refund = cur->dpl; free(cur); if (p->wait->front != NULL) { // 等候队列非空,出队一个乘客补位 qnode *w = p->wait->front; p->wait->front = w->next; if (p->wait->front == NULL) p->wait->rear = NULL; // 把 w 的信息插入 order 链表(此处省略具体插入代码) free(w); } else { p->tkt_sur += refund; // 无人等候,余票加回 } return 1; } pre = cur; cur = cur->next; } return 0; // 未找到,退票失败这里的关键参数是refund,即退票张数,从被删除结点的dpl字段取。等候队列出队后,乘客信息要从qnode转到Lnode,字段不完全对应——qnode只有姓名和电话,没有证件号和订票量。文档里没展开这部分,实际补全时需要给等候乘客默认订票量 1,或者要求等候时也输入证件号。这是课设代码的一个粗糙点,但不影响演示退票主流程。
3.4 文件读写:链表与磁盘数据的同步
文件模块分两个方向:启动时从文件读入航班和乘客信息建链表,退出时把链表写回文件。文档里只给了写入的流程图,读取部分没展开。写入逻辑是遍历航班链表,对每个航班先写航班信息,再遍历order链表写乘客信息。格式必须严格固定,否则读取时fscanf会错位。
// 写入航班和乘客信息 void SaveToFile(lineair H, FILE *fp) { lineinfo *p = H->next; while (p) { fprintf(fp, "%s %s %s %s %s %s %s %s %s %s %s %.1f %.1f %d %d\n", p->qdname, p->zhname, p->air_num, p->plane_num, p->year, p->month, p->day, p->qhour, p->qminute, p->jhour, p->jminute, p->zhekou, p->price, p->tkt_amt, p->tkt_sur); Lnode *q = p->order->next; while (q) { fprintf(fp, "%s %s %s %d %s\n", q->name, q->IDnum, q->airnum, q->dpl, q->piaohaio); q = q->next; } p = p->next; } }参数说明:fp是已打开的文件指针,模式为"w"或"a"。航班信息一行,乘客信息每个一行,用换行符分隔。读取时先读一行判断是航班还是乘客,或者用固定字段数区分。文档里提到“文件检测函数运用错误”导致程序终止,常见原因是fopen返回 NULL 没判断,或者feof用法不对。后面避坑章节会展开。
4. 避坑与排查:课设代码里那些让人调半天的错误
4.1 字符串赋值用成=,编译不报错但运行崩溃
文档调试分析里明确写了:“在一个字符串的复制中使用了赋值,调试过程指出错错误半天都不知道改”。C 语言里字符数组不能用=直接赋值,char a[20]; a = "hello";编译会报错,但如果是char *a; a = "hello";编译通过,运行时如果后面试图修改a指向的内容就会崩溃。课设里常见的是结构体里的字符数组,比如p->name = q->name,这种错误编译器会直接报“赋值给数组类型”,但如果是通过指针操作,就可能绕过编译检查。解决方法是统一用strcpy或strncpy,并且确保目标数组足够大。
4.2break放在查找循环里,只能查到第一条
文档里写:“查询信息只能查询链表中的第一条航线,检查程序原来是多用了 break 造成过早跳出循环”。查找逻辑通常是while (p) { if (匹配) { 输出; break; } p = p->next; },如果break写在了if外面,或者循环里有多余的break,就会在第一次迭代后直接跳出。更隐蔽的情况是if里用了return,导致函数提前返回。排查方法是把查找循环单独拎出来,用 printf 打印每次比较的航班号,看循环是否真的遍历完了。如果只打印了第一条就停,那就是break或return位置不对。
4.3 余票只减一张,订多张时数据不对
文档里写:“乘客订多张票后浏览信息发现余票只减了一张,检查程序发现乘客订票后只对余票做了自减”。p->tkt_sur--和p->tkt_sur -= m是两回事。如果订票量m是 3,自减只减 1,余票就多了 2。这个错误在测试时如果只订 1 张票不会暴露,一旦订多张就翻车。解决方法是所有涉及数量的更新都用-=或+=,并且在做减法前判断m <= p->tkt_sur。另外,退票时加回余票也要用+= refund,不能只加 1。
4.4 文件写入后不重新写回,余票还是初始值
文档里写:“运行程序后打开所写的文件,发现航班信息的余票量没有随乘客的订票而减少,还是初始值”。原因是订票操作只改了内存里的链表,没有在每次订票后重新写文件。文件写入只在退出系统时执行一次,如果程序异常终止或者没走正常退出流程,文件就不会更新。解决方法是每次修改链表后立即调用保存函数,或者至少在订票、退票、修改航班后都触发一次写文件。代价是频繁 IO 会慢,但课设数据量小,可以接受。
4.5itoa不是标准函数,换编译器就报错
文档代码里用了itoa(p->tkt_sur, b, 10),这个函数在 VC++6.0 的<stdlib.h>里有,但 GCC 和 Clang 不提供。如果你把代码复制到 Dev-C++、Code::Blocks 或 VS Code + MinGW 里编译,会报“undefined reference to itoa”。替代方案是用sprintf(b, "%d", p->tkt_sur),标准 C 都支持。另外getch()也不是标准函数,需要<conio.h>,在非 Windows 环境同样不可用。如果课设要求跨平台,这两个点必须改。
5. 进阶改造:把课设代码变成能写进简历的项目
5.1 用sprintf替换itoa,让代码在 GCC 下编译通过
原始代码依赖 VC++6.0 的itoa和getch,换到现代编译器直接翻车。改造第一步就是替换这两个函数。itoa换成sprintf,getch换成getchar或者用scanf读密码。密码不回显的需求可以用getch在 Windows 下实现,但跨平台可以用termios在 Linux 下关回显,代码量稍大。课设演示如果只在 Windows 下跑,保留getch也行,但至少把itoa换掉,因为sprintf更通用。
// 替换 itoa char b[10]; sprintf(b, "%d", p->tkt_sur); strcat(q->piaohaio, b);sprintf的第二个参数是格式串,第三个是整数。b要预留足够空间,tkt_sur最大是乘员定额,一般不超过 4 位数,char b[10]够用。
5.2 给航班链表加排序,把查询从 O(n) 降到 O(log n)
原始代码的查询是遍历单链表,时间复杂度 O(n)。文档在“算法的改进设想”里提到可以按起飞抵达城市排序,然后用分块查找。更实际的做法是建一个按航班号排序的索引数组,或者直接用二叉搜索树存航班。但课设代码改动量最小的方案是:录入时用尾插 + 插入排序,保持链表按航班号有序,查询时用二分查找。不过链表二分查找需要随机访问,链表做不到,所以要么改成数组 + 链表混合,要么用跳表。对于课设级别,更简单的是把航班信息读进数组,用qsort排序后二分查找,找到后再操作链表。这样查询 O(log n),插入删除还是 O(n),但查询是高频操作,收益明显。
5.3 用文件持久化验证数据一致性
改造后的代码要验证文件读写是否真的同步。方法很简单:录入 3 条航班,订 2 张票,退 1 张票,然后退出程序,重新运行,浏览所有航班和乘客信息,看余票和订单是否和退出前一致。如果不一致,检查保存函数是否在每次修改后都调用了,以及读取函数是否正确解析了文件格式。常见问题是写入时用了fprintf带空格分隔,读取时用fscanf的格式串不匹配,导致字段错位。建议写入和读取用同一套格式串,或者干脆用二进制模式fwrite/fread直接写结构体,但结构体里有指针,不能直接写,需要先序列化。
// 验证数据一致性的测试流程 // 1. 运行程序,录入 3 条航班 // 2. 对第 1 条航班订 2 张票 // 3. 对第 1 条航班退 1 张票 // 4. 退出程序 // 5. 重新运行,浏览航班和乘客 // 6. 检查第 1 条航班余票是否 = 定额 - 2 + 1 // 7. 检查乘客链表是否还有 1 个订单这个测试能覆盖录入、订票、退票、文件写入、文件读取五个模块。如果第 6 步余票不对,说明退票时余票加回逻辑有问题;如果第 7 步订单数不对,说明退票删除结点或文件写入有问题。我一般会在每次改完代码后强制走一遍这个流程,比单步调试快。
5.4 把等候队列的通知逻辑补完整
原始代码里等候队列的入队和出队只写了一半,退票时通知等候乘客的部分被截断了。补全的思路是:等候乘客入队时存姓名和电话,退票时从队头取一个,然后要求这个乘客补充证件号和订票量,再插入已订票链表。如果等候乘客联系不上,就继续取下一个,直到队列空或有人确认。课设演示可以简化成:出队后直接按默认订票量 1 插入,票号用当前余票量生成。这样退票后余票不变,等候乘客自动补位,逻辑闭环。
从那以后我每次拿到课设代码,都先做三件事:把非标准函数替换掉、把文件读写跑一遍、把边界条件(空链表、满仓、重复票号)测一遍。这三步走完,基本不会在答辩现场翻车。希望帮到你。
本文还有配套的精品资源,点击获取