☰
航班信息查询与检索系统:数据结构与算法实战
2026/10/9 9:16:58 网站建设 项目流程

简介:这份文档是面向计算机专业学生的《数据结构》课程设计报告,主题为航班信息查询与检索,适合正在完成数据结构课设或需要参考排序查找综合案例的学习者。资源包共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即可。

索引持久化稍微复杂一点,思路是把哈希表和城市索引序列化到二进制文件,启动时先尝试读索引文件,读不到再重建。课程设计里不强制要求,但做了能体现你对文件存储的理解。我一般会把顺序表、哈希桶数组、城市索引分别写三个文件,读的时候按同样顺序恢复。注意指针不能直接写文件,要写下标或偏移量。

最后说一个我自己的习惯:每次改完查询函数,不要只测一条数据,至少构造三组边界数据——空结果、单条结果、满结果。空结果验证不会崩,单条结果验证指针有效,满结果验证缓冲区不溢出。这个习惯帮我省了很多答辩时的尴尬。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询