简介:这份数据结构大作业资源面向计算机专业学生与数据结构初学者,围绕停车场管理程序展开,帮助读者把数组、链表、栈、队列、哈希表、平衡树、图等典型结构落到真实业务场景中,解决车辆进出管理、车位查询与状态更新等问题。压缩包共42个文件,约15.07MB,以cpp源码、sln与vcxproj工程文件、exe可执行程序为主,另含pdb、ilk、obj等调试与编译中间文件及db数据文件,构成一套可直接编译运行的完整工程。目前已有486人学习下载。资源涵盖单链表、循环链表等模块实现,读者可据此理解栈的“后进先出”如何管理进出场、队列如何模拟排队等待、哈希表如何以车牌号为键实现快速查找,并参考排序、搜索与动态规划思路优化车位分配策略,适合作为课程设计参考与数据结构综合练习的实战样本。
1. 停车场管理程序:从链表到哈希表的选型分水岭
很多同学拿到“停车场管理程序”这个数据结构大作业,第一反应是打开 Dev-C++ 就开始写struct Car,然后卡在“出场时怎么按车牌号快速找到那辆车”上。这个标题背后真正要解决的核心问题不是“怎么把车停进去”,而是有限车位下的动态调度与高效检索——车位是固定资源,车辆进出是动态事件,计费依赖精确的时间差,而车牌号是唯一的检索键。适合正在做课程设计、实验报告或期末项目的人,也适合想用 C/C++ 把链表、栈、队列、哈希表串起来练一遍的考研复习者。王道408里反复强调的“逻辑结构决定存储结构”,在这个题目里体现得特别明显:选错了底层结构,后面每加一个功能都是血泪经验。
2. 需求拆解与数据结构选型:为什么不能只用数组
2.1 停车场管理程序的四个刚性需求
先把需求拆干净,再谈结构。一个能拿得出手的停车场管理程序,通常要满足下面四条:
- 车辆入场:分配一个空闲车位,记录车牌、入场时间、车位号。
- 车辆出场:按车牌找到对应记录,计算停车时长和费用,释放车位。
- 查询状态:查看当前场内车辆列表、空闲车位数、某辆车的停放位置。
- 容量约束:车位满时拒绝入场,或引导到便道等待区。
这四条里,入场和查询是高频操作,出场是低频但要求精确。很多同学一上来用数组存车辆,入场时遍历找空位,出场时再遍历找车牌,时间复杂度都是 O(n)。车位少的时候看不出问题,一旦把车位调到 200 个以上,每次操作都扫一遍数组,实验报告里的“效率分析”就没法写了。
2.2 链表、栈、队列、哈希表各自能干什么
选型不是炫技,是看操作频率和约束。
| 结构 | 适合场景 | 在本程序中的角色 | 主要代价 |
|---|---|---|---|
| 数组 | 容量固定、随机访问 | 车位槽位映射 | 删除/查找 O(n) |
| 单链表 | 频繁插入删除 | 场内车辆动态列表 | 按车牌查找 O(n) |
| 栈 | 后进先出 | 便道等待区(若要求) | 不支持中间查找 |
| 队列 | 先进先出 | 出场排队或等待入场 | 同上 |
| 哈希表 | 按 key 快速检索 | 车牌号到车辆记录的映射 | 需要处理冲突 |
我一般会这样组合:用数组或位图管理车位占用状态,用哈希表做车牌到车辆信息的索引,用链表维护当前场内车辆列表用于遍历展示。这样入场是 O(1) 找空位加 O(1) 哈希插入,出场是 O(1) 哈希查找加 O(1) 释放车位。便道等待区如果题目要求,就用一个队列,先进先出补位。
提示:如果老师明确要求“必须用链表实现”,那就把哈希表换成按车牌排序的链表或二叉搜索树,但要在实验报告里写清楚查找复杂度从 O(1) 退化到 O(n) 或 O(log n) 的原因。
2.3 最小可行版本的结构定义
下面这个结构定义是我在多次课程设计里沉淀下来的,字段不多,但够用:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #define MAX_SPOTS 100 #define PLATE_LEN 16 #define HASH_SIZE 211 // 取大于最大车辆数的质数,减少冲突 typedef struct { char plate[PLATE_LEN]; int spot_id; // 车位号,从 1 开始 time_t enter_time; // 入场时间戳 int occupied; // 1 表示占用 } CarRecord; typedef struct HashNode { CarRecord car; struct HashNode *next; // 拉链法解决冲突 } HashNode; typedef struct { CarRecord spots[MAX_SPOTS]; // 车位槽位,下标 0 对应 1 号位 HashNode *hash_table[HASH_SIZE]; int occupied_count; } ParkingLot;MAX_SPOTS控制容量,HASH_SIZE取质数是为了让车牌字符串的哈希值分布更均匀。spots数组直接按车位号索引,出场时释放车位就是把这个下标的occupied置 0。哈希表只存指针,不重复存车辆数据,避免数据不一致。
2.4 车牌哈希函数怎么写才不容易翻车
车牌号是字符串,常见格式是“京A12345”这种。哈希函数不用太复杂,但要注意两点:不要用简单求和,因为“京A12345”和“京A54321”会撞;要控制哈希值范围,最后对HASH_SIZE取模。
unsigned int hash_plate(const char *plate) { unsigned int h = 5381; while (*plate) { // h * 33 + c,经典 djb2 算法,分布均匀且计算快 h = ((h << 5) + h) + (unsigned char)(*plate); plate++; } return h % HASH_SIZE; }h << 5等价于乘 32,再加h就是乘 33。这个算法在字符串哈希里很常见,冲突率低,代码也短。取模用HASH_SIZE而不是MAX_SPOTS,是因为哈希表大小和车位数解耦,后面调容量不用改哈希逻辑。
3. 核心操作实现:入场、出场与计费的代码落地
3.1 入场操作:先找空位,再插哈希
入场要做三件事:检查是否已存在同一车牌、找空闲车位、写入哈希表和车位数组。
int find_free_spot(ParkingLot *lot) { for (int i = 0; i < MAX_SPOTS; i++) { if (!lot->spots[i].occupied) return i; } return -1; // 满位 } HashNode* hash_find(ParkingLot *lot, const char *plate) { unsigned int idx = hash_plate(plate); HashNode *p = lot->hash_table[idx]; while (p) { if (strcmp(p->car.plate, plate) == 0) return p; p = p->next; } return NULL; } int car_enter(ParkingLot *lot, const char *plate) { if (hash_find(lot, plate)) { printf("车牌 %s 已在场内\n", plate); return -1; } int spot = find_free_spot(lot); if (spot < 0) { printf("车位已满,%s 无法入场\n", plate); return -1; } // 写入车位数组 strncpy(lot->spots[spot].plate, plate, PLATE_LEN - 1); lot->spots[spot].spot_id = spot + 1; lot->spots[spot].enter_time = time(NULL); lot->spots[spot].occupied = 1; lot->occupied_count++; // 插入哈希表 unsigned int idx = hash_plate(plate); HashNode *node = (HashNode*)malloc(sizeof(HashNode)); node->car = lot->spots[spot]; node->next = lot->hash_table[idx]; lot->hash_table[idx] = node; printf("车牌 %s 入场,车位 %d\n", plate, spot + 1); return spot + 1; }find_free_spot是 O(n) 扫描,但 n 是车位数,通常不超过几百,实际影响很小。如果追求极致,可以用一个空闲位栈或位图,但课程设计里没必要过度设计。hash_find先查重,避免同一车牌重复入场。插入哈希表时用头插法,O(1) 完成。
3.2 出场操作:哈希查找、计费、释放车位
出场是计费的关键,时间差算错,实验报告里的测试用例就对不上。
double car_exit(ParkingLot *lot, const char *plate) { HashNode *node = hash_find(lot, plate); if (!node) { printf("未找到车牌 %s\n", plate); return -1; } time_t now = time(NULL); double hours = difftime(now, node->car.enter_time) / 3600.0; if (hours < 0.01) hours = 0.01; // 防止刚入场就出场算出 0 double fee = hours * 5.0; // 假设每小时 5 元 int spot = node->car.spot_id - 1; lot->spots[spot].occupied = 0; lot->occupied_count--; // 从哈希表中删除节点 unsigned int idx = hash_plate(plate); HashNode **pp = &lot->hash_table[idx]; while (*pp) { if (strcmp((*pp)->car.plate, plate) == 0) { HashNode *del = *pp; *pp = del->next; free(del); break; } pp = &(*pp)->next; } printf("车牌 %s 出场,车位 %d,停车 %.2f 小时,费用 %.2f 元\n", plate, spot + 1, hours, fee); return fee; }difftime返回秒数,除以 3600 得到小时。hours < 0.01的兜底是为了防止测试时刚入场就出场导致费用为 0,有些老师会检查这个边界。删除哈希节点用二级指针,避免单独处理头节点。释放车位只改occupied标志,不一定要清空车牌,因为下次入场会覆盖。
3.3 查询与遍历:怎么展示当前场内车辆
查询分两种:按车牌查单车,和列出全部在场车辆。前者走哈希,后者走spots数组。
void list_all(ParkingLot *lot) { printf("当前在场 %d 辆,空闲 %d 位\n", lot->occupied_count, MAX_SPOTS - lot->occupied_count); for (int i = 0; i < MAX_SPOTS; i++) { if (lot->spots[i].occupied) { printf("车位 %3d 车牌 %-10s 入场时间 %s", i + 1, lot->spots[i].plate, ctime(&lot->spots[i].enter_time)); } } }ctime返回的字符串自带换行,所以printf里不用再加\n。遍历spots数组是 O(MAX_SPOTS),但只读不写,速度很快。如果实验报告要求统计“平均停车时长”,可以在出场时累加hours和出场次数,最后算平均值。
3.4 主函数与测试用例怎么组织
主函数用菜单驱动,方便演示和截图。
int main() { ParkingLot lot; memset(&lot, 0, sizeof(lot)); int choice; char plate[PLATE_LEN]; while (1) { printf("\n1.入场 2.出场 3.列表 4.退出\n请选择:"); scanf("%d", &choice); switch (choice) { case 1: printf("输入车牌:"); scanf("%s", plate); car_enter(&lot, plate); break; case 2: printf("输入车牌:"); scanf("%s", plate); car_exit(&lot, plate); break; case 3: list_all(&lot); break; case 4: return 0; default: printf("无效选择\n"); } } }测试时至少覆盖:空场入场、满场拒绝、重复车牌入场、正常出场计费、未入场车牌出场、刚入场就出场。每个用例在实验报告里贴输入输出截图,比写一堆文字描述管用。
4. 避坑与排查:课程设计里最容易翻车的五个点
4.1 哈希表删除后指针悬空,遍历时崩溃
现象:出场操作后调用list_all或再次查询,程序直接段错误。原因:删除哈希节点时只free了节点,但spots数组里对应车位的occupied没置 0,或者哈希表里还有指针指向已释放内存。解决:删除顺序必须是先改spots[spot].occupied = 0,再从哈希表摘链,最后free。摘链时用二级指针,确保头节点也能正确删除。
4.2 车牌字符串未初始化,strcmp 读到乱码
现象:明明没入场的车牌,查询时却提示“已存在”。原因:CarRecord里的plate数组没有清零,strcmp比较时读到了栈上的随机值。解决:ParkingLot lot;之后立刻memset(&lot, 0, sizeof(lot));,或者用calloc分配。入场时用strncpy并手动补\0。
4.3 时间差算成负数或超大值
现象:停车费用显示为负数,或者几万小时。原因:time_t在有些编译器里是 32 位,2038 年问题先不说,测试时如果手动改了系统时间,difftime就会异常。解决:出场时判断now < enter_time就按 0 处理,并打印警告。费用计算前先做hours = hours < 0 ? 0 : hours。
4.4 车位号与数组下标差一,释放错车位
现象:出场后空闲车位数没变,或者释放了别人的车位。原因:车位号从 1 开始展示,但数组下标从 0 开始,spot_id - 1写成了spot_id。解决:统一约定:spots数组下标i对应车位号i + 1。所有对外展示用i + 1,所有内部访问用i。在代码里加注释,避免自己忘。
4.5 满场时便道等待区逻辑写反
现象:车位满了之后,新车直接覆盖了场内车辆,或者便道车辆永远进不来。原因:等待区用队列实现时,入场判断顺序错了——先入队再检查车位,或者出场时忘了从队列取车补位。解决:入场时先查哈希表判重,再查空闲车位;有车位直接停,无车位才入等待队列。出场释放车位后,立刻检查等待队列是否非空,非空则出队并调用入场逻辑补位。
5. 进阶技巧:用位图加速空闲车位查找与实验报告加分项
如果想让实验报告里的“效率分析”更有说服力,可以把find_free_spot的 O(n) 扫描换成位图或空闲位栈。位图用一个unsigned long数组,每个 bit 表示一个车位是否占用,找空闲位就是找第一个 0 位。C 语言里可以用__builtin_ctzl(GCC/Clang)或手写循环,但课程设计里手写循环更稳妥。
unsigned long free_bitmap[(MAX_SPOTS + 63) / 64]; int find_free_spot_fast(ParkingLot *lot) { for (int i = 0; i < (MAX_SPOTS + 63) / 64; i++) { if (free_bitmap[i] != ~0UL) { // 还有 0 位 int bit = __builtin_ctzl(~free_bitmap[i]); int spot = i * 64 + bit; if (spot < MAX_SPOTS) return spot; } } return -1; }~free_bitmap[i]把 0 变 1,__builtin_ctzl返回最低位 1 的位置,也就是第一个空闲车位。这个操作是 O(1) 级别(按机器字长),比遍历数组快得多。入场时把对应 bit 置 1,出场时置 0。位图不依赖spots数组,两者可以并行维护,但要注意同步。
另一个加分项是把操作日志写到文件,方便实验报告里贴“运行记录”。用fprintf追加到parking.log,记录时间、车牌、操作类型、车位号、费用。这样即使程序崩溃,日志还在,排查问题有据可查。
void log_operation(const char *op, const char *plate, int spot, double fee) { FILE *fp = fopen("parking.log", "a"); if (!fp) return; time_t now = time(NULL); fprintf(fp, "%s | %s | 车位%d | %.2f元\n", ctime(&now), op, spot, fee); fclose(fp); }ctime返回的字符串带换行,写进日志后每条记录占两行,读的时候注意。如果嫌换行碍事,可以用strftime格式化。
最后说一个我自己的习惯:先写测试用例,再写主逻辑。把“京A11111 入场、京A11111 重复入场、京A22222 入场、京A11111 出场、京A33333 入场直到满位、京A44444 入场被拒”这一串操作写成test_cases.txt,用脚本或手动跑一遍,确认每个输出都对得上,再往实验报告里贴。这样比写完代码再想测试用例省时间,也不容易漏边界。希望帮到你。
本文还有配套的精品资源,点击获取