ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C语言单链表实战:飞机订票系统数据结构课设全解析

C语言单链表实战:飞机订票系统数据结构课设全解析 简介飞机订票系统是数据结构课程设计中常见的应用型题目这份报告以该题目为选题完整呈现了一份结构规范、内容详实的课程设计文档适合高校计算机相关专业学生参考。报告从需求分析入手明确了输入输出形式、取值范围及功能要求随后围绕航班信息管理、订票、退票、查询、修改等核心模块展开概要设计与详细设计给出了航班信息类、座位信息类、订单信息类等数据结构设计以及二分查找、哈希表等算法在系统中的应用思路。测试与分析部分覆盖了合法与非法数据用户使用说明部分还列出了运行环境与操作注意事项附录配有完整程序代码。整份报告可作为数据结构知识落地应用的参考也能作为课程设计报告撰写的范例。资源为单个docx文档压缩包大小约662KB打开即可查看完整目录与代码。已有79人学习下载适合正在准备数据结构课程设计的学生学习借鉴。1. 数据结构课程设计报告里的飞机订票系统一座能直接拆解的 C 语言链表“样板间”数据结构课程设计里「飞机订票系统」是出镜率最高的一类题目但也是最容易写浅的——它表面上是增删改查实际上考察的是怎么用单链表把航班和订单两类数据组织起来并保证每一步操作和落盘数据一致。这份《数据结构课程设计报告-飞机订票系统.docx》我完整拆过一遍航班信息用单链表存储结点带九个数据项客户订单单独维护一条链表结点带五个数据项六大功能模块覆盖录入、订票、退票、查询航班、查询订单、修改航班末尾还带测试用例和附录代码。它解决两类问题一是课设不知道从哪下笔需要一份结构完整、能直接套用的报告模板二是链表增删改查停留在课本例题需要一个带业务流程和数据持久化的 C 语言项目来验证。适合正在做数据结构课程设计的学生也适合想照着练手链表实战的初学者。2. 数据结构选型单链表、九个航班字段和头尾指针的设计逻辑2.1 航班结点的九个数据项是一张业务表的字段转写飞机订票系统的第一件事是把「航班」在内存里的形态定下来。报告定义了一个 flightnode 结构每个结点对应一个航班字段依次是航班号、起飞时间、降落时间、起飞城市、抵达城市、空座数、票价、票价折扣、是否满仓。这里是整理后的完整字段对照字段名C 语言类型存储内容业务含义air_numchar[10]航班号检索关键字start_timechar[15]起飞时间字符串不做时间运算end_timechar[15]抵达时间字符串不做时间运算start_placechar[20]起飞城市按航线查询的输入条件end_placechar[20]抵达城市按航线查询的输入条件leftint空座数订票时扣减、退票时恢复pricefloat票价浮点数运算price_discountfloat票价折扣与 price 配套输出isFullint是否满仓1 满仓0 未满对应的结构体定义是这份报告的核心零件C 语言描述如下typedef struct flightnode { char air_num[10]; // 航班号 char start_time[15]; // 起飞时间 char end_time[15]; // 抵达时间 char start_place[20]; // 起飞城市 char end_place[20]; // 抵达城市 int left; // 空座数 float price; // 票价 float price_discount; // 票价折扣 int isFull; // 是否满仓 struct flightnode *next; // 指向下一个航班结点 } flightnode;这里有个值得注意的取舍起飞时间和抵达时间都用字符串而不是结构化的时间类型。因为课程设计只要求「显示航班时刻」和「修改航班时间」不要求按时间排序或计算时长用字符串能直接 strcpy 覆盖、直接 printf 输出省去时间格式转换的麻烦。如果你是跟着严蔚敏《数据结构C语言版》走过来的看到这个结构应该很眼熟它其实就是线性表那一章的单链表应用每个结点包含数据域和指针域指针域指向下一个航班结点。为什么选单链表而不是结构体数组订票系统运行时增加航班和删除航班是常态操作数组在中间位置插入要整体后移元素删除也要前移补齐最坏情况是 O(n) 的移动量单链表插入删除只改相邻指针代价 O(1)。而查询是这个系统里最频繁的操作本质仍是遍历匹配航班号链表和数组的遍历都是 O(n)。数据量按课程设计规模也就几十条航班链表的额外内存开销可以忽略。所以选单链表的理由很直白插入删除是高频随机访问不是刚需。2.2 客户订单结点用航班号做关联而不是存指针航班信息之外系统还要维护客户资料。报告定义了一个 passengernode 结构保存姓名、证件号、航班号、订单号、订票数量五个数据项typedef struct passengernode { char name[20]; // 客户姓名 char ID_num[20]; // 证件号 char flight_num[10]; // 所订航班号 int order_num; // 订单号 int ticket_num; // 订票数量 struct passengernode *next; // 指向下一个订单结点 } passengernode;这里最关键的设计是订单结点里保存的是航班号字符串而不是航班结点的内存指针。很多初学者会想当然地认为「订单应该直接指向航班结点」但这会在退票和删除航班时制造野指针航班链表一旦删除或重新加载指向它的订单结点里的指针就失效了。订单里只存航班号查找航班时回到航班链表重新匹配虽然多一次遍历但链表之间的耦合关系降到最低。这和数据库里用外键关联而不是用内存地址关联是同一个思路——按业务主键关联不按内存地址关联。2.3 passengerList 结构头尾指针包一层尾部插入从 O(n) 降到 O(1)客户链表还额外包了一层结构包含头指针和尾指针两个数据项typedef struct passengerList { passengernode *head; // 客户链表头指针 passengernode *rear; // 客户链表尾指针 } passengerList;为什么要单独定义一个包裹结构而不是直接用 passengernode * 头指针因为客户订单的追加操作最频繁——每一次订票都会在链表尾部插入一个新订单。如果只有一个裸的头指针尾插需要从头遍历到最后时间复杂度 O(n)有了尾指针插入过程变成两步操作PList-rear-next q; PList-rear q;直接 O(1)。航班链表没有做这个优化因为航班录入和删除频率远低于订票退票它的插入偶尔做一次遍历到尾部可以接受。这个不对称设计很能体现报告作者对操作频率的判断同是单链表根据业务特点决定要不要额外维护尾指针。3. 功能模块实现录入、订票、退票、查询、修改的完整调用链3.1 录入与新增航班三步完成链尾插入录入航班信息模块的调用链是主函数输入“1”进入 add_flightadd_flight 接收用户输入的航班信息后调用 insert_flight把新结点挂到航班链表尾部。操作可以拆成三步找到链尾、申请新结点、链尾指向新结点。void insert_flight(flightnode *h, char *flight_num, char *start_place, char *end_place, char *start_time, char *end_time, int left, float price, float price_discount, int isFull) { flightnode *p h; // 从头结点开始走 while (p-next ! NULL) // 走到链尾 p p-next; flightnode *q (flightnode *)malloc(sizeof(flightnode)); if (q NULL) return; strcpy(q-air_num, flight_num); strcpy(q-start_place, start_place); strcpy(q-end_place, end_place); strcpy(q-start_time, start_time); strcpy(q-end_time, end_time); q-left left; q-price price; q-price_discount price_discount; q-isFull isFull; q-next NULL; // 新结点 next 置空 p-next q; // 链尾接上新结点 }参数说明h 用引用传递因为后面删除航班、修改航班时头结点位置可能变化引用能让函数内对头结点的更新直接反映到主调函数q-next NULL 这步不能省否则新结点的 next 是随机地址后续遍历会访问非法内存轻则输出乱数据重则段错误。malloc 后建议检查返回值虽然课程设计场景内存申请失败的概率很低但答辩时老师问起来这是明显的加分点。3.2 订票模块查询、扣座、追加订单三件事一次完成订票模块在主函数中输入“2”进入 book。流程是先让客户输入起飞抵达城市调用 place_check 查出该航线有哪些航班如果没有航班直接提示有航班则让客户输入航班号、订票数量、姓名、证件号再检查对应航班是否满仓、空座数是否够最后插入客户结点并扣减空座数。int book(flightnode *h, passengerList *PList) { char start_place[20], end_place[20], flight_num[10]; char name[20], ID_num[20]; int ticket_num; printf(请输入起飞城市); scanf(%s, start_place); printf(请输入抵达城市); scanf(%s, end_place); if (!place_check(h, start_place, end_place)) { printf(该航线暂无航班\n); return 0; } printf(请输入航班号、订票数量、姓名、证件号\n); scanf(%s %d %s %s, flight_num, ticket_num, name, ID_num); flightnode *p h-next; while (p ! NULL strcmp(p-air_num, flight_num) ! 0) p p-next; if (p NULL) { printf(无此航班\n); return 0; } if (p-isFull 1 || p-left ticket_num) { find_same_flight(h, flight_num); // 满仓或余座不足输出可选航班 return 0; } insert_passengerList(h, PList, name, ID_num, flight_num, ticket_num); p-left - ticket_num; // 扣减剩余座位 if (p-left 0) p-isFull 1; // 余座为 0 时同步满仓标记 return 1; }这段逻辑有两个关键点。第一p-left - ticket_num 是订票对航班数据的唯一修改点修改后必须调用 save_flight 保存否则程序重启后空座数回退。第二满仓判断用 p-isFull 1 || p-left ticket_num 两个条件前者覆盖录入时手动标记的满仓状态后者覆盖订票过程中余座逐渐变成不足的情况。原报告在录入航班时没有校验 left 和 isFull 的一致性所以订票前的判断必须把两个条件都写上否则会出现「isFull0 但 left0」还能继续订票的矛盾。insert_passengerList 内部完成两件事申请 passengernode 结点按订单链表尾部插入利用尾指针 PList-rear 达到 O(1) 插入同时设置 name、ID_num、flight_num、ticket_num。订单号 order_num 的生成一般用全局计数器每次订票自增报告里没有细说生成规则你自己实现时建议保证同一会话内不重复。注意book 里调用 place_check 时这个函数本身会输出匹配航班的完整信息所以订票菜单里你会先看到一次该航线的航班列表再被要求输入航班号。这是课程设计常见的交互设计不是 bug。3.3 退票模块先恢复座位再删除订单退票模块的入口是 cancel调用 delete_passenger 完成查找和删除。匹配条件包括姓名、证件号和航班号三个信息比订票多了一个航班号原因是同一个客户可能订了多个不同航班只靠姓名和证件号会删掉全部订单。找到匹配订单结点后首先要恢复航班余座再执行链表删除// 在 delete_passenger 中找到匹配订单结点 p 后 flightnode *f h-next; while (f ! NULL strcmp(f-air_num, p-flight_num) ! 0) f f-next; if (f ! NULL) { f-left p-ticket_num; // 空座数加回 f-isFull 0; // 一旦有退票航班肯定未满 } pr-next p-next; // 前驱越过待删结点 free(p); // 释放订单结点这里两个操作顺序有讲究必须先恢复航班余座再删除订单结点。因为删除结点后 p 指向的内存被释放再读取 p-flight_num、p-ticket_num 就是访问野指针。很多下载版代码在这里翻车把 free 放在前面退票后航班余座没有恢复越退座越少。3.4 查询航班与查询订单三种入口共享同一套遍历逻辑查询航班模块 flight_check 提供三个入口按航班号查flight_num_check、按起飞抵达城市查place_check、浏览全部航班check_all_flight。查询订单模块 passenger_check 也是三个入口按姓名和证件号查ID_name_check、按订单号查order_num_check、浏览全部订单check_all_passenger。两个查询模块的实现套路一样区别只在匹配字段按航班号查询的核心就是遍历链表比对字符串int flight_num_check(flightnode *h, char *flight_num) { flightnode *p h-next; while (p ! NULL) { if (strcmp(p-air_num, flight_num) 0) { printf(%s %s %s %s %s %d %f %f %d\n, p-air_num, p-start_time, p-end_time, p-start_place, p-end_place, p-left, p-price, p-price_discount, p-isFull); return 1; } p p-next; } printf(没有相应的航班信息\n); return 0; }注意原报告输出形式一节里的要求查询操作后显示要查找元素的位置。所以你最好在遍历时加一个位置计数匹配成功后把结点序号一并输出。很多照抄代码的人漏了这一点答辩老师如果按报告核对输出会被当场指出来。另外查询函数返回 int 表示是否找到正是为了让订票模块的 book 直接调用 place_check 来判断航线是否存在避免重复写一遍城市匹配逻辑。3.5 修改航班模块唯一涉及级联删除的地方修改航班模块包含三种操作增加航班、删除航班、修改起飞抵达时间。增加航班直接复用 add_flight修改时间先按航班号找到结点然后 strcpy 覆盖 start_time 和 end_time删除航班需要做级联处理既要删除该航班结点也要删除所有指向该航班的客户订单。// delete_flight 中的两个删除动作 flightnode *pr h, *p h-next; // pr 为 p 的前驱 while (p ! NULL strcmp(p-air_num, flight_num) ! 0) { pr p; p p-next; } if (p ! NULL) { pr-next p-next; // 摘掉航班结点 free(p); } passengernode *qr PList-head, *q PList-head-next; while (q ! NULL) { // 遍历订单链表 if (strcmp(q-flight_num, flight_num) 0) { qr-next q-next; // 摘掉匹配订单 free(q); q qr-next; // 继续查下一个订单 } else { qr q; q q-next; } }级联删除有个细节订单链表里可能有多条订单指向同一个航班所以删除订单要用 while 循环把所有匹配结点都删干净不能用 if 只删一个。删除航班后若不清理对应订单查询订单时会看到一条指向不存在航班的记录报表数据就是脏的。到这里你也能理解 2.2 节那个设计的价值订单结点只存航班号删除航班时拿 flight_num 遍历订单链表去匹配逻辑干净不会留下悬空的引用。4. 避坑与常见问题链表课设最容易翻车的五个点4.1 字符串溢出航班号多打一个字符整个链表变成乱码现象输入航班号时超过 9 个字符程序继续跑但后面查询时航班号打印出来是乱码或者 strcmp 永远匹配不上。这种乱码问题最像玄学排查半天不知道内存里发生了什么。 原因air_num 定义是 char[10]最多存 9 个字符加一个结束符 \0。strcpy 不检查目标缓冲区长度输入超过 9 个字符就溢出到相邻内存把结构体里的 start_time 甚至 next 指针都覆盖了链表结构被破坏。 解决录入前先做长度校验输入长度超过 9 就重新输入更稳妥的做法是用 strncpy 加手动置 \0。这份报告在录入模块里没有做任何长度限制属于已知缺陷你要在 add_flight 入口补一段判断这也是答辩老师喜欢追问的点。4.2 left 和 isFull 状态不一致余座 0 还能继续订票现象订完最后一张票后查询航班显示空座数为 0但再次订票系统没有提示满仓还能继续下单。 原因订票流程只扣减了 left没有同步把 isFull 更新为 1或者录入航班时手工输入了 left0、isFull0 的矛盾组合程序没有做校验。 解决订票函数在 p-left - ticket_num 之后加一行 if (p-left 0) p-isFull 1;。录入航班时也应校验用户输入的 left 和 isFullleft 为 0 时强制把 isFull 置 1isFull 为 1 时强制把 left 置 0。这一条如果做不好后续订票、查询模块会出现两种互相矛盾的输出。4.3 删除结点先 free 后取 next把链表后半段弄丢现象删除一个订单后订单链表后半段的记录全部查询不到甚至直接崩溃。这是链表操作里最典型的血泪经验。 原因典型错误写法是 free(p); p p-next;。free 之后 p 已经是野指针再取 p-next 是非法访问拿到的可能是随机地址链表从此断开。 解决删除前先保存后继写法是 pnext p-next; 然后 pr-next pnext; 最后 free(p)。这个原则在 delete_passenger 和 delete_flight 两个函数里都要贯彻。检查自己代码时凡是出现 free 语句的地方先看前面有没有用变量保存 next没有就直接改。4.4 删除头结点航班整个链表找不到现象删除编号靠前的航班后再执行查询航班程序直接崩溃或显示空列表重新录入也异常。 原因delete_flight 如果没有处理「要删结点是 h-next」的情况一前一后两个指针遍历时在链表只有一个有效结点的情况下删除逻辑可能把头结点 h 也一并释放。头结点一丢整条链表就断了。 解决删除函数中先特判如果要删的结点是头结点后的第一个有效结点更新 h-next 指向 p-next。函数入参用引用 flightnode *h删除后能在函数内部直接改头结点。这道题在课设答辩里出现频率很高务必提前验证。4.5 数据文件没保存辛辛苦苦录的数据全丢现象录入几十条航班订票退票也正常但关闭程序重新打开所有数据回到初始状态。 原因save_flight 和 save_passenger 只在主函数某个末日分支被调用如果退出分支写成直接 break内存链表里的数据没有写入数据文件自然全部丢失。 解决把保存动作放到三个位置每次录入或增加航班后立即 save_flight每次订票或退票后同时调用 save_flight 和 save_passenger每次修改或删除航班后同样保存。不要指望一个「退出保存」兜底因为断电、强制关闭都没有机会走到退出分支。这份报告定义了完整的 save/load 四件套说明作者考虑了持久化但很多流传的代码里主函数根本没调 save拿到手先检查这一处。5. 资源落地验证六步测试跑通全流程三种思路改造自己的课设5.1 六步验证跑通全流程这份代码是纯 C 语言实现依赖只有标准库不涉及图形界面和第三方库。用 Dev-C 或 Visual Studio 创建 C 项目把附录代码完整粘贴进去就能编译。我建议按下面顺序做一遍功能验证主菜单输入“1”录入两条航班一条满仓 isFull1一条空座数 3输入“2”走订票流程先订满仓那条确认系统调用 find_same_flight 给出其他可选航班订一张非满仓航班的票再输入“4”查询该航班确认空座数从 3 变为 2输入“3”退票再用“5”查询订单确认订单消失且航班余座恢复为 3输入“6”删除该航班再用“5”查询订单确认关联订单被级联删除退出程序再重进确认数据文件里的航班和订单信息被正确加载。这六步走完课设的核心功能就没有遗漏。原报告第 4 章的合法/非法测试用例只覆盖了菜单跳转和异常输入没有覆盖「满仓后订票」「删除有订单的航班」这两条最容易出问题的路径上面补的就是这两条。5.2 改成你自己的课设三个地方就够了飞机订票系统这套框架改造成其他管理类课设很直接只需要动三个地方改造点原资源字段改成其他题目示例航班结点air_num, start_place, end_place, price图书编号、作者、出版社、定价订单结点flight_num, ticket_num, order_num图书号、借阅数量、借阅证号功能菜单录入/订票/退票/查询/修改录入/借书/还书/查询/修改也就是说你可以在保持单链表结构不变的前提下把字段名和业务逻辑重新映射变成图书管理系统、火车票订票系统、酒店房间管理系统。改的时候守住三个原则结点里存业务主键字符串而不是指针插入删除前先保存前驱和后继每次修改数据后立即保存文件。这三点守住换什么业务场景都不会翻车。5.3 把它变成自己的东西拿别人的课设资源最忌讳的是不改一个字直接交。我每次拆这类资源都会强制自己走一遍「编译 → 六步功能验证 → 换字段 → 重新跑测试」的流程把它变成自己真正能讲明白的项目。这份报告的价值不在代码本身而在于它把单链表的增删改查、头尾指针优化、级联删除、文件持久化串成了一个完整业务系统。你把它读透、改一遍、跑通测试用例数据结构答辩基本就不会被问倒。希望帮到你。本文还有配套的精品资源点击获取
返回列表