首页 / 资讯中心 / 文章详情

数据结构课程设计航班信息查询与检索:从顺序查找到二分查找与哈希索引

数据结构课程设计航班信息查询与检索:从顺序查找到二分查找与哈希索引 ★ FEATURED ARTICLE
简介数据结构课程设计《航班信息查询与检索》是一份完整的课程设计报告适合正在学习数据结构、需要完成类似课设作业的高校学生。资源以单个 doc 文档提供压缩包共 1 个文件、约 218KB内容涵盖课程设计任务书、成绩评定、目录、系统分析、概要设计、详细设计、测试数据、收获体会与参考文献等模块章节结构清晰。报告围绕航班信息查询与检索系统展开详细讲解了航班记录数据结构的定义方式并采用基数排序法对航班号排序利用二分查找实现按航班号快速检索起点站、终点站、起飞时间等次关键字则用顺序查找完成。同时附有核心算法描述、流程图和调试记录能够帮助读者理解数据结构在实际问题中的选择与应用。对需要撰写数据结构课设报告、掌握排序查找算法实现细节的学生有直接参考价值已有 334 人浏览/学习亦可用于设计思路梳理与报告模板对照。1. 数据结构课程设计航班信息查询与检索先定查询语义再写第一行代码很多同学拿到“数据结构课程设计航班信息查询与检索”这个题目时第一反应是写菜单、定结构体、用 switch 把六七个查询选项串起来。我见过不少课程设计作业代码能跑、能交但答辩时一被问“为什么用顺序查找”“航班量从 500 涨到 50000 怎么办”基本就卡住。这个题目真正考的是三件事航班数据怎么组织、查询用什么算法、排序和查找怎么配合恰好也是数据结构考研复习里查找与排序最常见的一组考点。这篇笔记直接给出一套用 C 语言实现的完整路线数据模型、文件读入、三种查询算法、检索加速以及那些让程序莫名翻车的边界细节。如果你正在做这个课程设计这篇可以直接照着改。2. 航班数据模型与文件读入三个决定后续代码结构的选型2.1 结构体定义字段怎么摆决定后面所有代码的写法航班信息本质上是一组“定长字段的记录集合”在 C 语言里最直接的组织方式就是结构体#include stdio.h #include stdlib.h #include string.h #define MAX_FLIGHTS 500 typedef struct flight { char flight_no[8]; // 航班号如 CA1835 char dep_city[16]; // 出发城市 char arr_city[16]; // 到达城市 char dep_date[11]; // 出发日期固定 YYYY-MM-DD char dep_time[6]; // 起飞时间 HH:MM char arr_time[6]; // 到达时间 HH:MM int remain_seats; // 剩余票数 } Flight;这段代码有三个选型理由。第一所有字段都用定长字符数组而不是char *指针。定长数组配合fscanf直接读入、直接strcmp比较不需要逐条malloc和free课程设计几百条航班的数据规模下动态分配省下的内存可以忽略却会引入大量释放不当的隐患。第二dep_date用char[11]而不是int因为显示和读入都方便代价是日期比较时需要规整这个坑在第 3 章和第 5 章都会再遇到。第三把查询最常用的字段“航班号”放在结构体第一个位置调试时用 gdb 打印结构体第一眼就能看到关键字段。字段长度也不是随手写的航班号 CA1835 加结尾空字符需要 7 字节char[8]够用城市名按两到三个汉字加空字符算16 字节基本能装下四个汉字日期YYYY-MM-DD固定 10 字符数组要 11时间HH:MM固定 5 字符数组要 6。原则是“最长内容 1”不用预留过大空间。2.2 从文本文件读入航班数据fscanf 的分隔符与返回值陷阱结构体定义好之后下一步是文件读入。常见做法是把航班数据放在一个纯文本文件里每行一条字段用空格分隔CA1835 北京 上海 2025-06-01 08:00 10:30 12 MU2151 上海 广州 2025-06-01 09:15 11:40 8 CZ3101 北京 广州 2025-06-01 07:50 10:55 0对应的读入函数int read_flights(const char *filename, Flight flights[], int capacity) { FILE *fp fopen(filename, r); if (fp NULL) { perror(无法打开航班数据文件); return -1; } int count 0; // 一次读 7 个字段全部成功才计为一条完整记录 while (count capacity fscanf(fp, %s %s %s %s %s %s %d, flights[count].flight_no, flights[count].dep_city, flights[count].arr_city, flights[count].dep_date, flights[count].dep_time, flights[count].arr_time, flights[count].remain_seats) 7) { count; } fclose(fp); return count; }这里最容易被新手写错的是循环条件。很多人习惯写while (!feof(fp))但feof只有在读写操作已经失败之后才会返回真循环体会多执行一次导致最后一条记录被重复读入或下标越界。正确的做法是把读入操作本身放进条件里严格判断fscanf的返回值等于 7只有 7 个字段全部成功赋值才计数。这样文件末尾的残缺行、空行都会自动被跳过行为是确定的。格式串里的%s会自动跳过输入中的空白字符包括空格、换行和 Tab所以文件里字段用什么分隔并不严格空格或 Tab 都能读。但如果数据文件是分号或逗号分隔%s就无能为力了需要改成%[^;]这类扫描集那又得多处理分隔符残留问题。因此课程设计里推荐自己生成、自己消费的空白分隔文本文件读入逻辑最干净。在主程序里这样调用Flight flights[MAX_FLIGHTS]; int n read_flights(flights.txt, flights, MAX_FLIGHTS); if (n 0) { fprintf(stderr, 没有读到有效航班数据\n); return 1; } printf(成功读入 %d 条航班记录\n, n);这里的capacity参数把数组大小传进函数read_flights内部不会踩到数组边界这是防止 buffer overflow 的基本功。如果文件行数超过MAX_FLIGHTS循环会在count capacity时停下来程序不崩溃但数据不完整所以容量要按题目规模预判。2.3 数组还是链表课程设计场景下的选型逻辑读入之后要考虑用数组存还是链表存。这一步选型直接决定后面排序和检索能不能用二分查找。先把两种容器在“航班查询与检索”这个场景下的表现摆出来判断维度动态数组单向链表随机访问第 i 条O(1)O(n)必须从头遍历二分查找天然支持要用跳表或额外索引复杂得多尾部追加可能触发 realloc均摊 O(1)O(1)直接挂尾结点中间插入删除O(n) 移动O(1)先找到位置内存分布连续cache 友好分散频繁遍历性能差实现复杂度低中题目叫“航班信息查询与检索”主操作是查不是增删。数组的随机访问和排序友好性在这个场景里几乎是碾压级的优势链表唯一的优势是中间插入删除但课程设计数据是启动时一次性读入、之后基本不变。所以我的选择是用数组存配合计数变量记录实际条数。如果题目额外要求“航班动态增加/取消”再考虑在数组上做一个“逻辑删除”标记也比链表插入一个结点省事。这块还有一层答辩价值能说清“为什么在这个问题里选数组而不是链表”本身就是数据结构课程设计的核心评分点。数组加二分查找的完整链路做完相当于把数据结构里查找一章大部分考点在代码里过了一遍。3. 查询算法落地线性匹配、多条件过滤与日期范围检索3.1 按航班号精确查询线性查找的最小实现先写一个通用的打印函数后面所有查询都会用到void print_flight(const Flight *f) { printf(%s %s - %s %s %s %s 余票%d\n, f-flight_no, f-dep_city, f-arr_city, f-dep_date, f-dep_time, f-arr_time, f-remain_seats); }按航班号精确查询最朴素的做法就是从头到尾线性匹配int search_by_flight_no(Flight flights[], int n, const char *target) { for (int i 0; i n; i) { if (strcmp(flights[i].flight_no, target) 0) { print_flight(flights[i]); return i; // 返回在数组中的下标 } } printf(未找到航班号 %s\n, target); return -1; }逻辑不用解释唯一的判断就是strcmp返回值是否为 0。需要注意两点strcmp是大小写敏感的用户输入ca1835而数据里是CA1835就查不到简单处理是在输入后统一转大写再比较也可以用strcasecmp但那是非标准函数课程设计里不建议用。第二这个函数返回的是下标不是“是否找到”的布尔值这样设计是为了后面哈希索引复用查到下标后可以继续拿flights[index]打印避免在多个函数里重复打印代码。性能上线性查找时间复杂度 O(n)500 条数据下查找一次在微秒级直接交作业没有任何问题。但答辩时老师一定会问“航班数据量变成 5000、50000 呢”这时顺序查找的劣势就暴露了第 4 章会给出排序加二分和哈希两条加速路线。3.2 按起降城市查询多条件过滤的书写顺序按出发城市和到达城市组合查询是这个课程设计最常见的功能需求核心是一个带两个条件的循环int search_by_route(Flight flights[], int n, const char *dep, const char *arr) { int found 0; for (int i 0; i n; i) { // 先检查出发城市不匹配直接跳过 if (strcmp(flights[i].dep_city, dep) ! 0) continue; // 到达城市为空时表示“只要从 dep 出发的都查” if (arr ! NULL arr[0] ! \0 strcmp(flights[i].arr_city, arr) ! 0) continue; print_flight(flights[i]); found; } if (found 0) { printf(没有找到 %s 出发的航班\n, dep); } return found; }这里有两个设计细节值得抄。第一两个条件都写成了“不满足就continue”的形式而不是把条件嵌套进if里。这样每个条件独立一行加第三个条件比如加日期限制时只需要再贴一个continue块不需要调整缩进层级。第二arr参数允许传 NULL 或空字符串此时第二个条件自动失效同一个函数就同时支持“查某城市所有出港航班”和“查两城市之间的航班”两种查询菜单里两个选项可以共用一个函数。返回值found是实际命中的条数调用方用它在屏幕上输出“共找到 X 班”这个数字也可以直接写进实验报告的测试用例里。这个函数还是后面日期范围检索的模板遍历数组、过滤字段、累计命中三种查询的骨架完全一致只是比较的字段不同。3.3 按日期范围检索为什么 YYYY-MM-DD 才能直接比较日期范围查询看起来和城市查询很像把条件改成字符串比较就行int search_by_date_range(Flight flights[], int n, const char *start, const char *end) { int found 0; for (int i 0; i n; i) { // 只有严格 YYYY-MM-DD 格式时才可以用 strcmp 比较 if (strcmp(flights[i].dep_date, start) 0 strcmp(flights[i].dep_date, end) 0) { print_flight(flights[i]); found; } } return found; }但这段代码有一个硬前提dep_date、start、end三个字符串必须全部是YYYY-MM-DD这种“年四位、月两位、日两位”的定长格式。因为strcmp比的是字典序而YYYY-MM-DD按字典序排列恰好等价于按日期先后排列先比年年相同比月月相同比日每一位字符位置固定比较方向完全一致。一旦数据里出现2025-1-2这种省略前导零的写法字典序就失效了。举一个具体例子strcmp(2025-1-2, 2025-10-1)会返回正数因为两个字符串前 6 个字符2025-1相等然后来到第 7 位左串是2右串是0而字符2的 ASCII 码大于0于是 1 月 2 日被判断成大于 10 月 1 日。这就是字符串日期最典型的翻车现场。稳妥的做法是读入数据后顺手把日期转成可比较的整数int date_to_int(const char *s) { int y, m, d; if (sscanf(s, %d-%d-%d, y, m, d) ! 3) return -1; // 2000 年前后数据用 (y-2000) 足够日期序等价于数值序 return (y - 2000) * 10000 m * 100 d; }转完后范围查询就变成纯整数比较任何输入格式都能正确排序。如果做排序也要按date_to_int的返回值排而不是直接 sort 字符串。提示如果源数据保证都是YYYY-MM-DD直接strcmp的版本代码最简洁不要画蛇添足只有数据格式不可控时才做转换。4. 检索加速qsort、二分查找与哈希索引的取舍4.1 qsort 给航班号排序比较函数要写对要让二分查找成立数组必须有序。C 标准库的qsort负责排序但要自定义比较函数// qsort 要求比较函数返回 负数 / 0 / 正数 int cmp_flight_no(const void *a, const void *b) { const Flight *fa (const Flight *)a; const Flight *fb (const Flight *)b; return strcmp(fa-flight_no, fb-flight_no); }调用时传入数组名、元素个数、单个元素字节数和比较函数qsort(flights, n, sizeof(Flight), cmp_flight_no);比较函数里直接 returnstrcmp的结果就够了。新手最常见的错误是想用“左减右”来比较写成return fa-flight_no - fb-flight_no编译直接报错因为字符数组不能相减就算用指针相减结果也没意义。字符串之间唯一可靠的比较就是strcmp或memcmp。qsort是不稳定排序但对航班号这种唯一字段做等值比较稳定性无所谓。如果按日期排序且两条日期相同它们之间谁先谁后不影响查询结果也不需要稳定。排序必须在查询之前做一次之后整个程序生命周期里数组保持有序。注意qsort会原地重排 Flight 数组如果你之前已经构建了哈希索引排序后这些索引会全部失效这点在第 5 章的避坑里还会再讲。4.2 有序数组上的二分查找把 O(n) 降成 O(log n)数组按航班号排好序后查找从顺序遍历变成折半比较int binary_search_by_no(Flight flights[], int n, const char *target) { int left 0, right n - 1; while (left right) { int mid left (right - left) / 2; // 防溢出写法 int cmp strcmp(flights[mid].flight_no, target); if (cmp 0) { print_flight(flights[mid]); return mid; } else if (cmp 0) { left mid 1; // 目标在右半区 } else { right mid - 1; // 目标在左半区 } } return -1; // 没找到 }mid的计算用left (right - left) / 2而不是(left right) / 2是防止两个 int 相加溢出。500 条数据当然溢出不了但这个写法能从习惯上避免将来数据量变大时的隐患答辩时被问到也能答得上来。每次比较后如果目标大于中间值查找区间收缩到右半部分小于则收缩到左半部分等于就命中返回。二分查找的整个前提是数组按“同一个 key”有序。按航班号查询做二分就必须先用cmp_flight_no按航班号排序如果先按日期排序再按航班号二分mid位置的航班号并不是整个数组的中位数查找必然失败。这个不变量是排序和检索配合的核心可以在代码注释里写清楚。时间复杂度从 O(n) 降到 O(log n)500 条数据时对比不明显但生成 50000 条数据的测试文件差距就能写进实验报告了。4.3 哈希索引空间换时间的课程设计加分项如果还想更进一步用哈希表把航班号查询变成近似 O(1)。一个常见做法是“数组存记录哈希表存下标”哈希结点只放航班号和它在flights数组里的位置#define HASH_SIZE 128 typedef struct HashNode { char flight_no[8]; int index; // 在 flights 数组中的下标 struct HashNode *next; } HashNode; // 经典乘法散列h h * 31 c最后对表长取模 unsigned int hash_str(const char *s) { unsigned int h 0; while (*s) { h h * 31 (unsigned char)(*s); } return h % HASH_SIZE; }构建索引void build_hash(Flight flights[], int n, HashNode *table[]) { for (int i 0; i n; i) { unsigned int h hash_str(flights[i].flight_no); HashNode *node (HashNode *)malloc(sizeof(HashNode)); strcpy(node-flight_no, flights[i].flight_no); node-index i; // 头插法挂到哈希桶链表上 node-next table[h]; table[h] node; } }查找int hash_search(const char *no, HashNode *table[]) { unsigned int h hash_str(no); for (HashNode *p table[h]; p ! NULL; p p-next) { if (strcmp(p-flight_no, no) 0) { return p-index; } } return -1; }哈希表里只存航班号和下标不复制整条 Flight 记录内存开销小。HASH_SIZE取 128500 条数据下每个桶平均 4 个结点查找时链很短。处理冲突用链地址法而不是开放寻址因为链地址法实现直观、删除结点容易课程设计代码量可控也正好是考研数据结构里散列查找那块知识点的落地版本。这里有一个关键约束哈希索引在构建时记录的是数组下标一旦再次调用qsort下标会变索引必须重建。提示三条检索路线可以都保留菜单里让用户选“顺序查找 / 二分查找 / 哈希查找”再配合一个计时器对比耗时这个设计在答辩和实验报告里都很加分。5. 常见问题与避坑读入、日期、排序、菜单的五个翻车记录这个课程设计跑通主流程不难真正让人卡住的是几个不起眼的细节。下面五条都是实际项目中反复出现的踩坑记录按“现象 → 原因 → 解决”写方便对照排查。5.1 中文城市名乱码或 strcmp 永远不相等现象数据文件里写“北京 上海”程序读出来后 printf 显示正常但strcmp(flights[i].dep_city, 北京)永远不相等或者屏幕上直接显示乱码。原因一是 Windows 记事本保存 UTF-8 文件时会写入 BOM 头前三个字节被当成城市名的一部分二是 Windows 控制台默认代码页 936GBK和 UTF-8 不匹配显示自然乱码。解决数据文件用记事本另存为时选“ANSI”或者“UTF-8 无 BOM”控制台里执行chcp 65001切到 UTF-8最省事的方案是城市名全用英文拼音如 Beijing、Shanghai彻底绕开编码问题。如果读入的是 Linux 下生成的文本还要注意行末可能带\r读入后做一次清理void trim_cr(char *s) { char *p s; while (*p) p; // 走到字符串末尾 if (p s *(p - 1) \r) *(p - 1) \0; // 去掉行尾回车 }5.2 最后一条数据丢失或读入错位现象文件里 10 条航班read_flights返回 9或者第 10 条记录城市名变成数字。原因最常见的是用while (!feof(fp))做循环文件读到末尾后feof才为真循环里多执行了一次fscanf此时没有读到有效数据却把上次残留内容或 -1 当成字段赋值另一种是fscanf格式串里写了逗号或分号但文件实际是空格分隔导致后几个字段匹配失败。解决循环条件严格写成fscanf(...) 7只有 7 个字段全部按格式读入才继续格式串和文件实际分隔符必须一致推荐统一用空格分隔的纯文本文件。改完后用 10 条数据自测确认返回 10。5.3 日期字符串比较结果反直觉现象按日期范围查询2025 年 1 月的航班排到了 10 月之后或者程序提示“开始日期大于结束日期”。原因字符串2025-1-2和2025-10-1按字典序比较时前 6 个字符都是2025-1第 7 位分别是2和0字符2的 ASCII 码大于0于是 1 月 2 日被判为大于 10 月 1 日。解决数据从源头就强制存成YYYY-MM-DD定长格式比较直接用strcmp如果数据格式不可控先调用date_to_int转成整数再比较排序时也按整数排。两种方案选一种不要两种格式混着存。5.4 排序后查询结果错乱现象先按航班号排序一切正常后来为了支持按日期查询又按日期排了一次序结果按航班号二分查找返回的记录张冠李戴哈希索引查到下标后打印出来的也不是目标航班。原因数组只有一份排序会原地重排整条记录哈希索引里存的下标是排序前的排序后下标语义改变了索引全部失效。解决必须明确数组在某个时刻只按一个主 key 有序排序之后依赖旧顺序的索引要重建。如果题目要求同时按航班号和日期检索就保留两份索引或者复制一份数组一份按航班号排序、一份按日期排序各查各的。5.5 菜单输入残留换行符导致死循环现象输入数字菜单选项后回车程序输出一次菜单就直接跳过输入或无限循环用scanf(%c)读选项时总是读到奇怪字符。原因scanf(%d)在成功读走数字后把换行符留在输入缓冲区下一次scanf(%c)立刻读到这个残留的\n根本没等用户输入。解决统一改用fgets读一行再做sscanf解析例如char line[32]; fgets(line, sizeof(line), stdin); if (sscanf(line, %d, opt) ! 1) continue;fgets会吃掉整行包括换行符sscanf再从缓冲区里解析整数残留问题从根上消失。这也是菜单循环最稳妥的写法。6. 从及格到优秀性能实测、边界用例与模块拆分如果这份课程设计想从“能跑”到“能拿高分”建议在实验报告里补三块内容性能实测、边界用例、模块拆分。先做性能实测。写一个生成随机航班文件的工具函数造出 1000、10000、50000 条数据然后分别用线性查找、二分查找、哈希查找各查同一个航班号重复 10 万次用clock()计时clock_t start clock(); for (int i 0; i 100000; i) { binary_search_by_no(flights, n, CA1835); } clock_t end clock(); printf(二分查找 10 万次: %.3f 秒\n, (double)(end - start) / CLOCKS_PER_SEC);把三组时间打印出来放进实验报告比任何文字描述都有说服力。数据量小时线性查找甚至更快这是正常的因为二分查找有比较和跳转开销数据量过万后差距才拉开报告里正好可以解释“为什么不能只看复杂度要结合数据规模”。再补一张边界用例表。空文件、只有一条数据、目标不存在、重复航班号、非法日期这五类用例的表现都应该在测试记录里写清楚每一条对应的预期行为是返回 0 或 -1 而不崩溃、不越界、提示明确。老师抽查时按这些用例跑一遍程序表现稳定比多做几个花哨功能都加分。用例预期行为空文件read_flights 返回 0提示无数据只有一条记录所有查询函数正常返回目标不存在返回 -1 或 0程序不崩溃重复航班号明确策略全部显示或只显示第一条非法日期date_to_int 返回 -1跳过或报错最后说模块拆分。所有查询函数都只做“查”和“计数”把打印交给print_flight把菜单逻辑留在main这样新增查询只要照抄search_by_date_range的骨架加一个条件就完事。我自己当年做这个题目时把所有查询都堆在main函数里答辩时老师临时让按价格区间再查一次我改了一下午最后不得不把main拆掉重写。那次之后我养成的习惯是数据读入一个函数、每种查询一个函数、打印一个函数main只负责调度。这个习惯帮我省了无数次返工希望也能帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站