
简介这份文档是面向计算机专业学生的《数据结构》课程设计报告主题为航班信息查询与检索适合正在完成数据结构课设或需要参考排序查找综合案例的学习者。资源包共1个doc文件约218KB内容为完整的课程设计报告涵盖概述、系统分析、概要设计、详细设计、测试数据、收获体会、参考文献与附录等章节。报告以链式基数排序为主线结合二分查找与顺序查找对航班号、起点站、终点站、班期、起飞时间、到达时间、机型及票价等八项记录进行排序与检索并给出数据类型定义、算法实现思路与测试数据。读者可借此理解如何将排序查找知识落地为可运行方案掌握静态链表构建、关键字分段处理及按不同关键字选择查找策略的完整设计流程。目前已有334人学习可作为课设选题、报告撰写与答辩准备的实用参考。1. 航班信息查询与检索从课程设计到能跑起来的检索系统课程设计里“航班信息查询与检索”这个题目几乎每年都会出现在数据结构课的选题清单上。很多同学第一反应是“不就是存几个航班号、查一下起降时间吗”真动手才发现数据量一上来线性查找慢得让人怀疑人生多条件组合查询更是写成一团乱麻。这个题目的核心不是界面好不好看而是你用什么数据结构组织航班记录、用什么算法支撑查询和检索。它适合正在做课程设计的学生也适合想复习查找结构、排序算法和文件存储的开发者。我见过太多版本把数据全塞进数组里硬遍历结果一千条记录查一次要等好几秒这种实现拿不到高分也经不起答辩追问。下面按“先选结构、再落代码、最后避坑”的顺序把这件事讲透。2. 航班记录怎么建模结构体、关键码与索引的取舍航班信息查询与检索的第一步不是写查询函数而是想清楚一条航班记录里哪些字段会被拿来查。常见字段包括航班号、起飞城市、降落城市、起飞时间、降落时间、机型、票价、剩余座位。如果把这些字段全当成查询条件每次查询都做全字段比对复杂度直接爆炸。所以建模阶段就要区分主关键码和次关键码。2.1 航班结构体的字段设计与内存布局我一般会把航班号设为主关键码因为它天然唯一适合做哈希或二叉排序树的键。起飞城市、降落城市、起飞时间作为次关键码用于组合查询。下面是一个能直接用的结构体定义字段顺序按查询频率从高到低排这样在顺序表中做局部性优化时命中率更高。#include stdio.h #include string.h #include stdlib.h #define MAX_CITY_LEN 32 #define MAX_FLIGHT_NO 16 /* 航班记录结构体主关键码 flightNo 放最前便于索引对齐 */ typedef struct { char flightNo[MAX_FLIGHT_NO]; /* 航班号主关键码唯一 */ char fromCity[MAX_CITY_LEN]; /* 起飞城市次关键码 */ char toCity[MAX_CITY_LEN]; /* 降落城市次关键码 */ int departTime; /* 起飞时间格式 HHMM如 0830 */ int arriveTime; /* 降落时间格式 HHMM */ int price; /* 票价单位元 */ int seatsLeft; /* 剩余座位 */ } Flight; /* 顺序表课程设计里最稳妥的底层存储 */ typedef struct { Flight *data; int length; int capacity; } FlightList;逻辑说明flightNo用定长字符数组而不是指针是为了后续把整条记录直接写入二进制文件时不会出现深拷贝问题。departTime用int存 HHMM 而不是字符串比较大小和排序都更快。FlightList用动态数组初始化时给一个容量插入时按需扩容避免固定数组在数据量未知时溢出。参数说明MAX_FLIGHT_NO取 16 足够容纳常见航班号格式MAX_CITY_LEN取 32 是为了兼容中文城市名转拼音或直接存中文的情况。如果你的课程设计要求支持中文城市名把char数组换成宽字符或 UTF-8 字符串即可但比较函数要相应调整。2.2 主关键码与次关键码的索引选择理由主关键码航班号适合建哈希表平均查找长度接近常数。次关键码起飞城市、降落城市适合建二叉排序树或按城市分桶的链表。课程设计里常见做法是主索引用哈希次索引用排序加二分。为什么不全部用哈希因为组合查询“从 A 城市到 B 城市且起飞时间在某个区间”这种条件哈希只能命中一个精确键范围查询和组合查询还是得靠有序结构。我一般会建两个索引一个哈希表以航班号为键指向FlightList中的下标一个按起飞城市排序的索引数组每个元素存城市名和该城市所有航班的下标链表。这样单条件精确查询走哈希城市相关查询走排序索引时间区间查询在结果集上做过滤。下面是对应的索引结构定义。/* 哈希表节点只存键和下标不存完整记录节省内存 */ typedef struct HashNode { char flightNo[MAX_FLIGHT_NO]; int index; /* 指向 FlightList.data 的下标 */ struct HashNode *next; /* 拉链法解决冲突 */ } HashNode; #define HASH_SIZE 211 /* 取素数减少冲突 */ typedef struct { HashNode *buckets[HASH_SIZE]; } HashTable; /* 城市索引按城市名分组每组存航班下标数组 */ typedef struct { char city[MAX_CITY_LEN]; int *flightIdx; /* 该城市对应的航班下标数组 */ int count; int capacity; } CityIndex;逻辑说明哈希表用拉链法而不是开放地址法因为航班号数量可能超过表长拉链法删除和扩容更简单。HASH_SIZE取 211 是经验值课程设计数据量通常几百到几千这个大小冲突率可接受。城市索引把同一城市的航班下标聚在一起查询时先二分找到城市再遍历该城市的下标数组避免全表扫描。参数说明哈希函数我一般用除留余数法把航班号字符的 ASCII 码累加后对HASH_SIZE取模。如果你想让分布更均匀可以用 BKDR 哈希乘子取 131。城市索引的capacity初始给 8插入时翻倍。2.3 从文件加载航班数据的完整流程课程设计通常要求数据从文件读取而不是硬编码在代码里。文件格式我建议用 CSV 或固定分隔符的文本一行一条记录字段之间用逗号或竖线分隔。下面是从文本文件加载并同时建索引的代码。/* 从文件加载航班数据同时构建哈希索引和城市索引 */ int loadFlights(const char *filename, FlightList *list, HashTable *ht, CityIndex **cityIdx, int *cityCount) { FILE *fp fopen(filename, r); if (!fp) { printf(无法打开文件: %s\n, filename); return -1; } char line[256]; *cityCount 0; *cityIdx (CityIndex *)calloc(64, sizeof(CityIndex)); /* 最多 64 个城市 */ while (fgets(line, sizeof(line), fp)) { if (list-length list-capacity) { list-capacity * 2; list-data (Flight *)realloc(list-data, list-capacity * sizeof(Flight)); } Flight *f list-data[list-length]; /* 按竖线分隔解析实际使用时可换成 strtok 或 sscanf */ sscanf(line, %15[^|]|%31[^|]|%31[^|]|%d|%d|%d|%d, f-flightNo, f-fromCity, f-toCity, f-departTime, f-arriveTime, f-price, f-seatsLeft); /* 插入哈希表 */ unsigned int h hashStr(f-flightNo) % HASH_SIZE; HashNode *node (HashNode *)malloc(sizeof(HashNode)); strcpy(node-flightNo, f-flightNo); node-index list-length; node-next ht-buckets[h]; ht-buckets[h] node; /* 插入城市索引先找城市找不到就新建 */ int found -1; for (int i 0; i *cityCount; i) { if (strcmp((*cityIdx)[i].city, f-fromCity) 0) { found i; break; } } if (found -1) { found (*cityCount); strcpy((*cityIdx)[found].city, f-fromCity); (*cityIdx)[found].capacity 8; (*cityIdx)[found].flightIdx (int *)malloc(8 * sizeof(int)); (*cityIdx)[found].count 0; } CityIndex *ci (*cityIdx)[found]; if (ci-count ci-capacity) { ci-capacity * 2; ci-flightIdx (int *)realloc(ci-flightIdx, ci-capacity * sizeof(int)); } ci-flightIdx[ci-count] list-length; list-length; } fclose(fp); return 0; }逻辑说明每读一行先扩容顺序表再解析字段然后同时更新哈希表和城市索引。哈希表插入用头插法O(1)。城市索引用线性查找定位城市城市数量通常不大这个开销可以接受如果城市很多可以把城市索引本身也做成哈希。参数说明sscanf里的%15[^|]表示最多读 15 个非竖线字符防止缓冲区溢出。calloc(64, sizeof(CityIndex))假设城市不超过 64 个超出会越界实际使用时应改成动态扩容。文件路径由调用方传入不要硬编码绝对路径。3. 查询与检索的实现哈希精确查、排序范围查、组合条件过滤有了索引查询函数就分成三类按航班号精确查、按城市或时间范围查、多条件组合查。这三类的实现策略完全不同混在一起写是课程设计里最常见的翻车点。3.1 按航班号精确查询的哈希实现精确查询走哈希表先算哈希值再遍历冲突链找到航班号后通过下标访问顺序表。平均时间复杂度 O(1)最坏 O(n) 但实际不会发生。/* 按航班号精确查询返回 Flight 指针找不到返回 NULL */ Flight *queryByFlightNo(HashTable *ht, FlightList *list, const char *flightNo) { unsigned int h hashStr(flightNo) % HASH_SIZE; HashNode *p ht-buckets[h]; while (p) { if (strcmp(p-flightNo, flightNo) 0) { return list-data[p-index]; } p p-next; } return NULL; } /* 字符串哈希BKDR乘子 131 */ unsigned int hashStr(const char *s) { unsigned int h 0; while (*s) { h h * 131 (unsigned char)(*s); } return h; }逻辑说明hashStr用 BKDR 哈希对短字符串分布均匀。查询时先定位桶再沿链比较。返回的是顺序表中的指针调用方可以直接读取字段不需要拷贝。参数说明乘子 131 是常用值你也可以用 31 或 1313效果差别不大。HASH_SIZE必须和插入时一致否则查不到。3.2 按城市和起飞时间范围检索的排序加二分城市查询走城市索引时间范围查询需要先对结果集按起飞时间排序再二分查找边界。下面是一个按起飞城市和起飞时间区间检索的函数。/* 比较函数按起飞时间升序 */ int cmpByDepartTime(const void *a, const void *b) { return ((Flight *)a)-departTime - ((Flight *)b)-departTime; } /* 查询从 fromCity 出发、起飞时间在 [t1, t2] 之间的航班 */ int queryByCityAndTime(FlightList *list, CityIndex *cityIdx, int cityCount, const char *fromCity, int t1, int t2, Flight **results, int maxResults) { /* 先定位城市 */ int cityPos -1; for (int i 0; i cityCount; i) { if (strcmp(cityIdx[i].city, fromCity) 0) { cityPos i; break; } } if (cityPos -1) return 0; /* 把该城市所有航班拷贝到临时数组按起飞时间排序 */ CityIndex *ci cityIdx[cityPos]; Flight *tmp (Flight *)malloc(ci-count * sizeof(Flight)); for (int i 0; i ci-count; i) { tmp[i] list-data[ci-flightIdx[i]]; } qsort(tmp, ci-count, sizeof(Flight), cmpByDepartTime); /* 二分找下界 */ int lo 0, hi ci-count; while (lo hi) { int mid (lo hi) / 2; if (tmp[mid].departTime t1) lo mid 1; else hi mid; } int start lo; /* 从 start 开始收集结果直到时间超过 t2 */ int n 0; for (int i start; i ci-count n maxResults; i) { if (tmp[i].departTime t2) break; results[n] list-data[ci-flightIdx[i]]; } free(tmp); return n; }逻辑说明先把该城市的航班拷贝到临时数组排序再二分找时间下界最后顺序收集。这里有个细节排序后临时数组里的元素是拷贝results里存的是原顺序表的指针所以返回的指针仍然有效。参数说明t1和t2用 HHMM 整数表示比如 0800 到 1200。maxResults由调用方指定防止结果数组溢出。如果城市航班数量很大每次查询都排序开销高可以在建索引时就对每个城市的航班按时间排好序查询时直接二分。3.3 多条件组合查询的过滤链设计组合查询比如“从 A 到 B、起飞时间在上午、票价低于 1000、还有座位”。这种查询没有单一索引能直接命中常见做法是先选一个选择性最高的条件走索引拿到候选集再在候选集上逐条过滤其余条件。/* 组合查询从 fromCity 到 toCity起飞时间 [t1,t2]票价上限 maxPrice */ int queryCombined(FlightList *list, CityIndex *cityIdx, int cityCount, const char *fromCity, const char *toCity, int t1, int t2, int maxPrice, Flight **results, int maxResults) { Flight *candidates[4096]; int n queryByCityAndTime(list, cityIdx, cityCount, fromCity, t1, t2, candidates, 4096); int m 0; for (int i 0; i n m maxResults; i) { Flight *f candidates[i]; if (strcmp(f-toCity, toCity) ! 0) continue; if (f-price maxPrice) continue; if (f-seatsLeft 0) continue; results[m] f; } return m; }逻辑说明先用城市加时间索引缩小候选集再对候选集做目的地、票价、座位过滤。过滤链的顺序按选择性从高到低排目的地比较比票价比较更可能快速排除所以放前面。参数说明candidates数组大小 4096 是经验值如果单城市航班可能超过这个数改成动态分配。maxResults控制最终返回数量避免调用方缓冲区溢出。4. 避坑与排查课程设计里最容易翻车的五个点这一章按“现象 → 原因 → 解决”写都是我见过或自己踩过的坑。4.1 哈希表查不到明明插入过的航班号现象插入函数调用成功但精确查询返回 NULL。原因哈希函数在插入和查询时不一致比如插入时用了hashStr(flightNo) % HASH_SIZE查询时忘了取模或者HASH_SIZE在两处定义不同。解决把哈希计算封装成一个函数插入和查询都调用它不要在两处各写一遍。4.2 城市索引里同一城市出现多次现象查询某个城市时结果重复或遗漏。原因插入城市索引时用strcmp比较但城市名前后有空格或换行符导致同一个城市被当成两个。解决解析文件时用trim去掉首尾空白或者在比较前统一做一次清洗。我一般会在loadFlights里加一个trim函数对fromCity和toCity都处理一遍。4.3 时间区间查询结果顺序错乱现象查询 0800 到 1200 的航班返回的顺序不是按起飞时间排的。原因queryByCityAndTime里排序的是临时数组但收集结果时用的是ci-flightIdx[i]这个下标对应的是原顺序表不是排序后的顺序。解决收集结果时用tmp[i]对应的原始下标或者在排序前把下标和记录一起排序。更简单的做法是排序后直接返回tmp里的记录拷贝但这样调用方拿到的是拷贝不是指针。4.4 文件读取时最后一行重复或丢失现象加载完文件发现记录数比实际多一条或少一条。原因fgets在文件末尾可能返回 NULL但循环条件写成了while (!feof(fp))导致最后一行被处理两次。解决直接用while (fgets(line, sizeof(line), fp))不要用feof做循环条件。另外如果文件最后一行没有换行符fgets仍然能读到但解析时要检查是否读到了有效字段。4.5 动态扩容后指针失效现象插入几条记录后之前保存的Flight *指针指向的数据变成乱码。原因realloc可能把顺序表搬到新地址之前通过list-data[i]拿到的指针全部失效。解决不要在插入过程中长期保存指向顺序表元素的指针查询结果要么存下标要么在全部插入完成后再取指针。如果必须在插入过程中用指针那就用链表而不是动态数组。5. 进阶技巧把检索做成可验证、可扩展的小系统课程设计做到这里基本能拿分了但如果你想再往前走一步有两个方向值得投入一是给检索加一个简单的命令行交互层二是把索引持久化到文件避免每次启动都重建。命令行交互层不需要图形界面用scanf或fgets读命令就行。我一般会实现这几个命令search flightNo精确查route from to查航线time from t1 t2查时间区间quit退出。每个命令对应一个查询函数输出用表格对齐。下面是一个最简交互循环的骨架。void repl(FlightList *list, HashTable *ht, CityIndex *cityIdx, int cityCount) { char cmd[64], arg1[64], arg2[64]; int t1, t2; while (1) { printf( ); if (scanf(%63s, cmd) ! 1) break; if (strcmp(cmd, quit) 0) break; if (strcmp(cmd, search) 0) { scanf(%63s, arg1); Flight *f queryByFlightNo(ht, list, arg1); if (f) printf(%s %s-%s %04d %d元 余%d\n, f-flightNo, f-fromCity, f-toCity, f-departTime, f-price, f-seatsLeft); else printf(未找到\n); } else if (strcmp(cmd, time) 0) { scanf(%63s %d %d, arg1, t1, t2); Flight *res[128]; int n queryByCityAndTime(list, cityIdx, cityCount, arg1, t1, t2, res, 128); for (int i 0; i n; i) { printf(%s %s-%s %04d %d元\n, res[i]-flightNo, res[i]-fromCity, res[i]-toCity, res[i]-departTime, res[i]-price); } } } }逻辑说明scanf(%63s, cmd)限制读入长度防止缓冲区溢出。每个命令分支调用对应的查询函数输出格式统一。这个循环可以直接嵌到main里加载数据后调用。参数说明res数组大小 128 是临时值实际可以按需调整。t1和t2用整数读入用户输入0800和1200即可。索引持久化稍微复杂一点思路是把哈希表和城市索引序列化到二进制文件启动时先尝试读索引文件读不到再重建。课程设计里不强制要求但做了能体现你对文件存储的理解。我一般会把顺序表、哈希桶数组、城市索引分别写三个文件读的时候按同样顺序恢复。注意指针不能直接写文件要写下标或偏移量。最后说一个我自己的习惯每次改完查询函数不要只测一条数据至少构造三组边界数据——空结果、单条结果、满结果。空结果验证不会崩单条结果验证指针有效满结果验证缓冲区不溢出。这个习惯帮我省了很多答辩时的尴尬。希望帮到你。本文还有配套的精品资源点击获取