
简介一份面向高校计算机/软件相关专业学生的数据结构课程设计报告主题为飞机订票系统。报告围绕乘客购票与航司运营场景完整覆盖需求分析、概要设计、详细设计、测试分析及用户使用说明重点阐述航班信息录入/查询、订票退票、订单查询和航班修改等功能模块并给出了数据结构选取、模块调用关系与查找删除等算法设计思路。压缩包内共1个文件即docx格式课程设计报告正文包体约662KB打开即可参照修改。报告中还包含输入输出格式规范、合法与非法数据测试用例以及业务流程说明可帮助读者理解课程设计的完整撰写框架也可作为同类信息管理系统的设计参考。目前已有79人学习下载。1. 数据结构课程设计报告里的飞机订票系统难点不在“订票”而在“数据怎么组织”数据结构课程设计报告里的飞机订票系统看着像个纯业务题实际上考的是数据结构基本功。很多人一拿到题目就奔着界面和菜单去先把订票流程写通直到做退票和候补才发现余票怎么跟订单保持一致退掉一张票以后链表节点怎么摘候补名单按什么结构排队这些才是课设真正要交的东西。这篇笔记适合正在写报告、准备代码验收的同学也适合想用这个题目把链表、队列、查找、排序串起来复习的从业者。下面按“先定结构、再写代码、最后补报告”的顺序把这个题目完整拆开。2. 把订票业务拆成数据对象链表、队列、栈分别扛哪一块2.1 先分清“静态数据”和“动态数据”拿到题目先不要在菜单上浪费时间。我一般会在纸上列两张表一张写“有哪些数据”一张写“每个数据会被怎么操作”。飞机订票系统里最常见的数据对象是这几个航班信息、乘客订单、候补名单外加一份可选的“改签操作记录”。航班信息包括航班号、出发地、目的地、起飞时间、总座位数、已售座位数。它的特点是数量有限几十条以内运行期几乎不增删但会被反复查询。这种数据适合放进顺序表也就是结构体数组按下标访问随机查询最快录入时如果要做排序直接交换数组元素也方便。乘客订单是动态的订票就往里加退票就删而且要能按姓名、航班号或者订单号查。它适合用单链表。每个节点是一张订单节点里带一个指向下一张订单的指针头插法加订单是 O(1)退票时按订单号找到节点摘链。如果要求“按航班分组查看”还可以在每个航班下挂一条子链表后续检索更直观。候补名单是“余票不足但还想等”的乘客队列核心要求是先到先得。剥开业务外壳这就是一个先进先出的队列。队列实现可以是链队列也可以是有上限的循环队列。选哪个取决于你有没有设置“候补人数上限”。数据对象核心操作推荐结构选型理由航班信息随机查询、排序、余票更新顺序表结构体数组数量固定、按下标访问 O(1)、排序交换方便乘客订单插入订票、删除退票、按编号查单链表插入删除频繁、节点动态分配、头插 O(1)候补名单入队、出队、先到先得链队列 / 循环队列FIFO 语义和订票公平性一致改签记录追加、撤销最近一次栈可选改签本质是“先退后订”栈只用来回滚这张表就是“把业务翻译成结构”的核心成果。报告里如果能先给出这样一张表后面写代码、做复杂度分析都会顺很多老师扫一眼就知道你的设计是有依据的而不是随手堆了几个结构体。2.2 航班表为什么是顺序表而不是链表先提醒一个容易踩的坑很多人觉得链表“高级”就把航班表也做成链表。等写查询时才发现按航班号找一架飞机要拿着链表从 head 一路 next 走到停二十次查询就是二十次全表扫描演示时特别尴尬。顺序表在这里明显更合适因为航班数量的上限可以定死比如MAX_FLIGHT100初始化时一次性建好运行期的操作大多是“找到某个航班然后改它的 sold 字段”顺序表直接下标命中。顺序表的代价是什么插入和删除要搬移元素。但航班录入通常发生在初始化阶段一次批量录入完就不动了。即使中途要加航班把插入逻辑写成一个函数放在菜单里最坏复杂度 O(n)对几十条数据来说完全无感。所以“用顺序表”不是因为它完美而是因为它贴合题目数据的访问模式。这里还要注意一个细节订票记录里不要存航班在数组里的下标要存航班号字符串。因为航班表可能会被排序函数打乱数组下标不可靠而航班号是稳定的业务主键。这个约定能在后面规避掉一整类“排完序订单对不上航班”的诡异问题。2.3 订单链表和候补队列“一进一出”刚好对上订单链表要处理的场景是乘客订票成功生成一张订单乘客退票删掉订单同时把航班余票加回。这两步必须放在同一个逻辑单元里否则就会翻车只删链不恢复余票或者余票恢复了但订单还在。链表的好处是节点动态生成退票时摘链释放内存不浪费。候补队列单独看很简单余票不足时入队有人退票时先补队头乘客。真正容易写错的是“从队头补票之后余票该变成多少”以及“航班出现空位时是只通知一个乘客还是等乘客确认后再操作”。课程设计一般按“自动补票”处理退票后队头乘客直接成为正式订单把交互逻辑减到最少也方便在演示时一条链路跑通。顺序表和链表在这个系统里是协作关系不是二选一的关系。航班余票在顺序表里维护订票记录在链表里维护中间靠 flight_no 关联。写清楚这一层“关联字段”的设计报告的数据结构部分就已经能站稳了。2.4 改签里的“栈”可加分但别硬凑改签的本质是先退旧票再订新票这个流程用链表和队列已经能覆盖并不需要引入栈。但如果报告想体现“栈”这种结构一个可行的做法是每次改签前把旧订单压入操作栈提供一次撤销。撤销时弹栈把旧订单重新插入链表把新订单删除再把两个航班的余票分别改回去。要注意栈只会让事务处理变复杂如果基础链路还没跑通不建议先做。课程设计的评分看的是结构选型是否合理、代码是否稳定不是“用到的数据结构种类越多越好”。一个跑得稳的链表加队列比一个半成品链表栈更有说服力。3. 跑通最小核心航班顺序表、订单链表、候补队列的代码骨架3.1 先把结构体和全局变量写对再谈其他C 语言实现课设最常见先从结构体定义开始。这一步如果字段设计错后面所有函数都要返工。#define MAX_FLIGHT 100 #define MAX_NAME 20 #define MAX_ID 20 typedef struct { char flight_no[8]; // 航班号如 CZ3101 char dep[16]; // 出发城市 char arr[16]; // 到达城市 int time; // 起飞时间存 830 表示 08:30 float price; // 票价排序和统计都要用 int total; // 总座位数 int sold; // 已售座位数 } Flight; typedef struct Order { int order_no; // 订单编号全局自增 char name[MAX_NAME]; // 乘客姓名 char id[MAX_ID]; // 身份证号 char flight_no[8]; // 乘机航班号关联航班表 char date[12]; // 乘机日期如 2025-06-01 struct Order *next; // 指向下一张订单 } Order; Flight flights[MAX_FLIGHT]; int flight_count 0; Order *order_head NULL; int order_counter 0;time 用整型存 HHMM比较大小直接比数值不需要字符串转换date 用字符串存查询时需要strcmp。航班数组是全局量能简化函数传参课程设计规模这样写没问题以后要封装成工程再把这些全局放进一个“系统上下文”结构体。price 字段按题目需求取舍下面 4.2 的排序会用到它。订单链表只用了一个order_head头指针没做尾指针。订票时头插 O(1)退票时要遍历链表按订单号找节点这是合理的取舍订票比退票频繁而且退票本身就是 O(n) 的查找过程加尾指针并不能减少遍历。3.2 订票先查余票再头插订单订票是整个系统的核心入口。一个完整的订票函数要处理四种结果航班不存在、余票不足进候补、订票成功、内存分配失败。int book_ticket(char *flight_no, char *name, char *id, char *date) { int i; for (i 0; i flight_count; i) { if (strcmp(flights[i].flight_no, flight_no) 0) break; } if (i flight_count) return -1; // 航班不存在 if (flights[i].sold flights[i].total) { enqueue_wait(flight_no, name, id); // 余票不足进候补队列 return 0; } Order *new (Order *)malloc(sizeof(Order)); if (new NULL) return -2; // 内存分配失败 new-order_no order_counter; strcpy(new-name, name); strcpy(new-id, id); strcpy(new-flight_no, flight_no); strcpy(new-date, date); new-next order_head; // 新订单插到链表头部 order_head new; flights[i].sold; // 余票同步减一 return 1; }这里的逻辑顺序不能颠倒先判断航班存在再判断余票再分配节点最后才改链表和余票。如果把sold写在malloc之前万一内存分配失败余票就少了一张。返回值约定为-1 航班不存在、0 进候补、1 订票成功、-2 内存失败。调用方在菜单里根据返回值打印不同提示后面排查问题也方便。航班查询用顺序遍历是因为航班数量小最坏 O(n) 可以接受。如果你在报告里写了“航班表按航班号有序查询用二分查找”那这里的逻辑要换成 4.1 里的二分函数两者不能矛盾。3.3 退票摘链、释放、恢复余票的顺序不能乱退票比订票更容易写错问题几乎都出在“链表摘链和内存释放的顺序”上。int cancel_ticket(int order_no) { Order *cur order_head; Order *prev NULL; while (cur ! NULL) { if (cur-order_no order_no) break; prev cur; cur cur-next; } if (cur NULL) return -1; // 订单不存在 // 摘链先改指针再释放节点 if (prev NULL) { order_head cur-next; // 删的是第一个节点 } else { prev-next cur-next; // 跳过当前节点 } int fi find_flight_index(cur-flight_no); // 找到对应航班 if (fi 0 flights[fi].sold 0) { flights[fi].sold--; // 余票恢复 } free(cur); // 最后释放 return 1; }摘链的顺序是“先定位 → 再改前驱的 next → 最后 free”。如果你先free(cur)再去访问cur-next拿到的就是悬空指针链表会进入未定义行为演示时表现就是“删一张票后面所有订单一起消失”。另一个容易漏的是余票恢复。这里用find_flight_index(cur-flight_no)去查航班而不是直接把航班下标存在订单里。原因前面说过航班表排序后下标会变flight_no 才是稳定关联键。如果该航班有候补队列正确处理是在退票函数的尾部触发补票。出队一个候补乘客把他转成正式订单再让sold加一。这样任何入口的退票都会自动补票而不是在菜单里重复写补票逻辑。3.4 候补队列链队的入队和出队候补人数没法预知用链队列最省心。队列结构里只需要两个指针front 指向队头rear 指向队尾。typedef struct WaitNode { char name[MAX_NAME]; char id[MAX_ID]; char flight_no[8]; struct WaitNode *next; } WaitNode; typedef struct { WaitNode *front; // 队头出队在这边 WaitNode *rear; // 队尾入队在这边 } WaitQueue; WaitQueue wait_queue {NULL, NULL}; void enqueue_wait(char *flight_no, char *name, char *id) { WaitNode *node (WaitNode *)malloc(sizeof(WaitNode)); strcpy(node-flight_no, flight_no); strcpy(node-name, name); strcpy(node-id, id); node-next NULL; if (wait_queue.rear NULL) { // 队列为空 wait_queue.front wait_queue.rear node; } else { wait_queue.rear-next node; wait_queue.rear node; } } WaitNode *dequeue_wait(void) { if (wait_queue.front NULL) return NULL; // 没有候补 WaitNode *node wait_queue.front; wait_queue.front wait_queue.front-next; if (wait_queue.front NULL) wait_queue.rear NULL; // 队空复位 return node; }入队永远在 rear 一侧出队永远在 front 一侧这就是 FIFO。特别注意出队函数里的最后一步当front变成 NULL 时rear也必须复位成 NULL。很多实现只更新 front导致队列清空后rear还指着已释放的节点下一次入队时直接往野指针后面挂节点程序跑着跑着就崩了。出队返回的节点调用方要决定怎么处理。自动补票的场景下拿到节点后调用订票逻辑把sold加一如果不需要补票就手动free这个节点。这块逻辑放在退票函数末尾能让整条业务闭环。提示候补自动补票要放在退票函数内部不要分散在菜单的各个分支里。否则每新增一个退票入口就要复制一遍补票代码很容易漏。4. 查询与排序在课设里的落点二分查找、选择排序、多关键字比较4.1 航班查询有序才能二分航班号是天然的主键。如果录入航班时保持航班号升序查询就能用二分查找这是数据结构课设里最值得写进报告的点。int find_flight_index(const char *flight_no) { int low 0, high flight_count - 1; while (low high) { int mid (low high) / 2; int cmp strcmp(flights[mid].flight_no, flight_no); if (cmp 0) return mid; else if (cmp 0) low mid 1; else high mid - 1; } return -1; }二分查找的前提是“数组按航班号有序”。如果你在录入航班时用的是尾部追加那就必须先在初始化里调用一次排序函数把航班表按 flight_no 排好。否则二分找到的是错误结果而且这种错误不会崩程序只会让查询结果一会儿对一会儿错非常难排查。按目的地查询不能用二分因为 dep 和 arr 字段没有全局有序性。这种查询只能顺序遍历把strcmp(flights[i].dep, keyword) 0当成匹配条件。报告里可以明确写航班号用二分查找 O(log n)目的地用顺序查找 O(n)区别对待才显得你真的理解了算法边界。4.2 航班排序为什么课设里选择排序够用航班列表通常要支持按价格排序或按起飞时间排序。排序算法的选择我一般建议用选择排序不追求快速排序的“高级感”。void sort_flights_by_price(Flight *arr, int n) { int i, j, min; for (i 0; i n - 1; i) { min i; for (j i 1; j n; j) { if (arr[j].price arr[min].price) { min j; } } if (min ! i) { Flight tmp arr[i]; arr[i] arr[min]; arr[min] tmp; } } }选择排序最坏和平均复杂度都是 O(n²)但对几十条航班数据运行时间可以忽略。它在课设里的优势是代码直观、交换次数少、空间复杂度 O(1)报告里好讲。如果你非要用快速排序也可以但要额外解释递归栈深度和退化场景分析难度反而上去了。排序会改变数组元素的下标位置但不会破坏订单链表的关联因为订单节点里存的是 flight_no 字符串不是下标。这一点在报告里可以专门写一句逻辑主键与物理位置解耦。4.3 订单列表排序多关键字怎么比较订单列表要支持的排序一般有两种按乘机日期排或按“同一航班内按订票顺序排”。后者需要两个关键字第一关键字是航班号第二关键字是乘机日期。int compare_order(const Order *a, const Order *b) { int c strcmp(a-flight_no, b-flight_no); if (c ! 0) return c; // 第一关键字航班号 return strcmp(a-date, b-date); // 第二关键字乘机日期 }链表排序可以用插入排序也可以用“转数组后快排再重建链表”。课设数据量小插入排序最稳妥而且它是稳定排序两个订单航班号相同、日期也相同时保持原始的先后顺序不会出现“同航班内顺序乱跳”的现象。如果转数组用快排稳定性不保证你需要额外写一个order_no第三关键字来兜底代码量就多了一截。4.4 报告里的复杂度表怎么写把每个操作的复杂度列成表是报告里性价比最高的一段内容。操作结构算法复杂度按航班号查航班顺序表二分查找O(log n)按目的地查航班顺序表顺序查找O(n)订票订单链表头插O(1)退票订单链表遍历 摘链O(n)候补入队链队列队尾插入O(1)候补出队链队列队头删除O(1)写报告的时候别把所有操作都写成 O(1)退票要遍历订单链表这一步就是 O(n)老老实实写反而更可信。复杂度表和代码实现必须对得上老师问一句“你这里为什么是 O(log n)”你要能说出“因为航班号有序且用了二分查找”。5. 避坑飞机订票系统最容易翻车的 5 个问题5.1 余票越登越乱最后订到“负座位”现象连续订几票、退几票后航班余票变成负数或者明明显示有余票却订不上。原因订票、退票的入口分散在菜单里有的分支更新了sold有的分支忘了退票时只删订单没恢复余票是最高发的错法。解决把余票变动收敛到两个函数内部。订票函数在订单节点成功插入后执行flights[i].sold退票函数在摘链后执行flights[i].sold--菜单里任何分支都不直接改航班余票。数据一致性靠函数边界保证不靠“每次记得改”。5.2 候补队列“先到先得”变成“插队成功”现象乘客 A 先候补乘客 B 后候补有人退票后 B 反而先分到票。原因出队实现里没使用front而是遍历整个队列找“第一个没有票的人”或者出队后front没更新导致队头永远不变每次补票都补到同一个乘客。解决出队函数永远只取wait_queue.front入队只往wait_queue.rear后面追加两个操作各自只动一个指针。出队后检查front是否为空为空时把rear也置 NULL保证下一次入队能正确初始化。5.3 链表退票把整条链弄断现象删掉中间一张订单后后面的订单在列表里全部消失或者程序在遍历链表时进入死循环。原因先free(cur)再去访问cur-next更隐蔽的是没有保存前驱节点删除中间节点时前驱还指向旧地址链就从这里断了。解决用两个指针同时走prev始终指向cur的前一个节点。先执行prev-next cur-next完成摘链最后执行free(cur)。退票后打印一次订单链表确认断点恢复这是最直接的验证手段。5.4 scanf 留下换行菜单循环里输入错乱现象输入航班号时明明打了字程序却直接跳过或者姓名输入被吞掉一个字符。原因scanf(%d)读取菜单选项后换行符还残留在输入缓冲区接下来的scanf(%s)直接跳过空白用scanf(%s)读姓名遇到空格又会被断开。解决菜单读数字后加一句getchar()吸收换行姓名和航班号尽量用自定义的字符串读取函数读完整行再去掉末尾换行。不要指望scanf处理带空格的姓名这是 C 语言课设里最不值得浪费时间调试的问题。5.5 链表和队列存文件后重启程序数据乱掉现象运行一次订票退票关闭程序后再打开订单顺序错乱候补队列丢失或者读出来的记录是乱码。原因直接把结构体内存用fwrite写入文件链表节点里存的是内存地址写进文件的是地址值下次程序启动后这些地址指向的位置毫无意义。解决序列化时按逻辑顺序写字段订单表从头到尾依次写order_no、name、id、flight_no、date读入时逐条malloc新节点再重新建立next关系。候补队列按 front 到 rear 的顺序写读入时同样逐条重建。文件格式用文本或二进制都行但绝对不要把内存指针写进文件。6. 把课设从“能跑”做到“像数据结构课设”验收演示和报告写法6.1 演示时先跑一条“边界链路”给老师演示时不要从“录入航班”开始讲。最有说服力的演示链路是把一个航班订满 → 继续订票触发候补 → 退一张票 → 候补乘客自动补上 → 最后展示余票、订单链表、候补队列三个数据是否一致。这一条链路把顺序表查询、链表头插与删除、候补队列入队出队、余票同步全部走了一遍比单独展示“能查询”更有说服力。6.2 报告里画对比图不要只贴代码报告的数据存储部分至少画两张示意一张是航班顺序表的数组结构一张是订单链表的节点插入与删除变化过程。图旁边写一句“订单频繁插入删除所以选链表而不是顺序表”这就是把书上的知识点和题目场景连起来了。算法部分把上一章的复杂度表放进“算法分析”小节再补一句“航班号有序所以查询用二分查找”体现的是分析能力不是代码量。6.3 有余力再加一个“结构增量”时间富裕的话可以加一个哈希索引按航班号建哈希表key 是航班号value 是航班在数组中的下标查询从 O(log n) 变成平均 O(1)冲突处理用链地址法就行。没有余力就不加基础链路跑稳比堆砌结构更划算。我后来做这类课设养成的习惯是拿到题目先不碰 IDE在纸上把数据对象、数据操作、复杂度各列一张表写代码前先把两三个核心函数的调用关系画出来。这个习惯帮我省掉了很多翻车时间。希望这篇笔记也能帮到你至少让飞机订票系统这道题不再“看着简单做着翻车”。本文还有配套的精品资源点击获取