多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

数据结构大作业:停车场管理系统中的栈与队列应用与实现

数据结构大作业:停车场管理系统中的栈与队列应用与实现 简介面向数据结构课程设计的停车场管理程序项目包含完整工程解决方案与可执行程序适合高校学生参考算法设计与模块化编码。压缩包共42个文件以cpp源文件、vcxproj工程配置和sln解决方案为主另含exe可执行文件、pdb调试符号、obj中间文件及tlog构建日志等整体约15MB。当前已有507人学习/下载。项目演示了数组、链表、栈、队列及哈希表等结构在停车位状态管理、车辆进出场记录和快速查询中的应用并涉及排序、搜索与动态规划分配策略同时体现了按模块拆分代码、降低耦合的设计思路。源码按功能拆分为多个模块配合pdb调试信息与可执行程序读者可一边运行一边对照代码深入理解不同数据结构在具体场景下的选型与优化也可直接用于课程设计报告和答辩素材。1. 数据结构大作业里的停车场管理程序为什么说它难不在写代码数据结构大作业里的停车场管理程序几乎是每个学数据结构的人都要过的一道坎。看起来需求很简单——记录车牌、分配车位、算钱、放行——但真正动手写的人会发现难点从来不在“会不会写代码”而在于怎么把“一辆车进来、车位满了排队、车要开走还得挪车”这些真实动作翻译成栈、队列、线性表能处理的操作。选错结构后面每一步都在打补丁选对结构整个程序从进场到出场就是一条顺畅的流水线。我见过不少同学花一星期写完这个程序最后栽在“车位满了之后等待区的车怎么按顺序补位”这种细节上——恰恰是数据结构课程最想考你的地方。这篇就按我习惯的做法把从需求拆解到核心代码、再到踩坑修复的完整路径讲清楚。2. 把停车场需求拆成数据结构栈、队列、再加一张表2.1 先拆需求把自然语言翻译成三个容器拿到题目别急着敲代码。先把需求里的动作逐条列出来再看每个动作对应哪种数据结构。一个标准的停车场管理程序核心动作不外乎这几个车辆进场、车辆出场、查询车辆位置、计算停车费用。把动作拆开之后你会发现它天然对应三个容器。车位区是第一个容器。车一辆接一辆停进去开走的时候如果停的是那种“巷子式”车位——最里面的车必须等外面的车先走——那后进的车先出这是典型的栈特征。很多教材版本的题目就是这么设计的用栈来模拟停车场的物理结构。第二个容器是等待区车位满了新来的车在入口排队先来的先补位这是标准队列。第三个容器是车辆台账每辆车的车牌、入场时间、停放位置都要记录查询的时候要在里面查找这可以用顺序表、链表或者更简单的结构数组。拆完你会发现这个题目其实是在考一件事你能不能把一个现实系统抽象成三种基本结构的组合。而且这三个容器不是孤立的——车辆出场时栈要弹出等待队列要出队台账要更新三个操作必须在一个流程里协同完成这才是大作业真正的分水岭。2.2 车位建模的岔路口用栈还是顺序表这里有一个很多人忽略的设计岔路。题目里如果明确说停车场是“一条窄道里面的车要等外面的车开走才能出来”那毫不犹豫用栈。但也有很多题目描述的是“一排车位哪个空着停哪个先来先走”这种实际上是先进先出要用顺序表或队列来模拟。我一般建议先去翻题目原文看有没有“后进入的车辆必须先离开”“停车场为单车道”这类关键词。有就按栈做没有就按顺序表做。按栈做的好处是和数据结构课程匹配度高栈的操作、遍历、弹出都是考点坏处是出场时要处理“挪车”——被堵住的车得先临时挪到别处再按原序开回来这一段逻辑写起来容易乱。按顺序表做就简单很多出场直接删除记录剩下的车保持相对顺序不变但缺点是它考不到栈在大作业评分里反而可能显得“太简单”。我的建议是如果题目没强制要求优先用栈然后在文档里说明“本程序以栈模拟单车道停车场体现了后进先出的特性”。这样评分老师一眼就能看到你用了栈后续的挪车逻辑又能展示你对栈操作的理解一举两得。2.3 等待队列与车辆台账队列和线性表的选型等待队列的选择相对简单。等待区的车只做两件事入队和出队最多再加一个遍历查看排队位置。循环队列是这里最稳的选择因为栈的车位容量固定等待队列长度也不会超过总车辆数循环队列能利用好数组空间而且实现代码量小、不容易出错。链式队列也可以但链表要动态分配节点写起来长一些对大作业来说没必要。车辆台账是这个程序里最灵活的部分选型取决于你要不要做“按车牌查询”这个功能。如果只是顺序打印所有车辆信息结构数组就够了。如果要按车牌快速查找那就得排序后二分或者用哈希表。大作业的常见做法是用一个简单的结构体数组每一项记录车牌号和入场时间再用一个整型标记当前有效车辆数。插入时顺序追加删除时把最后一个元素挪过来覆盖这是一种“数组内维护集合”的经典做法代码量小且不容易出错。3. 程序骨架与核心结构先把三个容器立起来3.1 菜单循环与模块划分停车场程序这种交互式控制台程序骨架一定是“菜单 循环 switch 分支”。这个骨架固定但建议把每个功能封装成独立函数不要全部堆在 main 里。常见的模块划分是入场处理、出场处理、查询处理、费用计算、数据展示。main 只负责打印菜单、读入选择、调用对应模块。int main() { int choice; initParkingLot(); // 初始化车位栈、等待队列、车辆台账 while (1) { printMenu(); scanf(%d, choice); switch (choice) { case 1: handleArrival(); break; // 车辆进场 case 2: handleDeparture(); break; // 车辆出场 case 3: handleQuery(); break; // 查询车辆 case 4: handlePrintAll(); break; // 打印场内车辆 case 0: saveAndExit(); return 0; default: printf(无效选择\n); } } }这段代码的逻辑核心是“初始化——循环——分发”。initParkingLot 负责把栈指针、队列头尾、车辆计数全部清零避免程序启动时读到垃圾值这一步容易被忽略但不做的话程序跑起来就会出现随机数据。菜单循环用 while(1) 加 break 条件退出这是控制台程序的通用写法。每个功能做成独立函数后续调试时可以直接单独调用不用整个程序跑一遍才能验证某个功能。3.2 数据结构定义栈、队列、车辆表的核心代码结构体设计决定了整个程序的复杂度。我这里给出一种经过多次调整、适合大作业的写法——三个结构体分别对应车位栈、等待队列、车辆台账全局变量里放三个实例。#define MAX_CAPACITY 5 // 停车场最多可停 5 辆车 #define MAX_WAITING 10 // 等待队列最大长度 typedef struct { char plate[12]; // 车牌号如 A12345 int arrivalMinute; // 入场时间以分钟为单位从 0 开始计时 } Car; typedef struct { Car data[MAX_CAPACITY]; // 栈的存储区 int top; // 栈顶指针-1 表示空栈 } ParkingStack; typedef struct { Car data[MAX_WAITING]; // 队列的存储区 int front, rear; // 队头、队尾指针 } WaitingQueue; typedef struct { Car records[100]; // 台账最多记录 100 条 int count; // 当前有效记录数 } Ledger; ParkingStack lot; // 停车场车位区 WaitingQueue queue; // 等待区 Ledger ledger; // 全部车辆台账关键点在于 Car 结构体里的 arrivalMinute。很多第一次写这个程序的人会用一个字符串或结构体存“入场时间”但算费用的时候要转成数字非常麻烦。直接用整数分钟比如程序启动时设定当前时刻为第 0 分钟之后每过一分钟加一或者用一个全局 clock 变量累加出场时用当前分钟数减去入场分钟数再换算成小时计费运算会清爽很多。top 初始化为 -1 是栈的标准做法front 和 rear 初始化为 0 是循环队列的标准做法这两个值错了后续所有入栈出栈、入队出队都会错位。3.3 初始化与公共工具函数初始化函数虽然简单但建议专门写一个不要写在 main 的开头。因为后续如果要做文件持久化初始化时还要读文件、加载数据单独成函数改动起来更方便。公共工具函数里最常用的是根据车牌找台账下标后面出场、查询、挪车都要用它。int findByPlate(const char* plate) { for (int i 0; i ledger.count; i) { if (strcmp(ledger.records[i].plate, plate) 0) { return i; // 找到则返回台账中的下标 } } return -1; // 找不到则返回 -1 }这个函数的时间复杂度是 O(n)对 100 条记录以内的大作业来说完全够用。要注意 strcmp 的返回值不是简单的 0 或 1而是负值、零、正值三种可能判断相等必须写成 0这是新手最容易犯的错误之一。另一个建议是统一用函数封装“获取当前时间”的操作比如int getCurrentMinute()方便后续改成真实系统时间或者模拟时间不用满程序去找散落的 time 调用。4. 核心流程实现进场、出场、收费、查询4.1 进场流程车位空着就入栈满了进等待队列车辆到达时有两种情况车位没满直接进栈车位满了进等待队列。这个分支逻辑不复杂但要注意的是——进栈之后必须同时写入台账进等待队列之后也必须同时写入台账台账是整个程序的“单一数据源”三处不一致就会出各种奇怪问题。void handleArrival() { char plate[12]; int now getCurrentMinute(); printf(输入车牌号: ); scanf(%s, plate); if (findByPlate(plate) ! -1) { printf(该车已在场内禁止重复入场\n); return; } if (lot.top MAX_CAPACITY - 1) { // 车位有空闲入栈 记录台账 lot.top; strcpy(lot.data[lot.top].plate, plate); lot.data[lot.top].arrivalMinute now; ledger.records[ledger.count].arrivalMinute now; strcpy(ledger.records[ledger.count].plate, plate); printf(车辆 %s 进入停车场位置 %d\n, plate, lot.top 1); } else { // 车位已满入等待队列 记录台账 if ((queue.rear 1) % MAX_WAITING queue.front) { printf(等待队列已满请稍后再来\n); return; } queue.rear (queue.rear 1) % MAX_WAITING; strcpy(queue.data[queue.rear].plate, plate); queue.data[queue.rear].arrivalMinute now; ledger.records[ledger.count].arrivalMinute now; strcpy(ledger.records[ledger.count].plate, plate); printf(车辆 %s 进入等待队列当前等待位置 %d\n, plate, (queue.rear - queue.front MAX_WAITING) % MAX_WAITING); } }先查重再入库是一开始就要养成的习惯。有的版本不做查重结果同一车牌进场两次台账里出现两条记录出场时删了其中一条另一条就成了“幽灵车”怎么查都查不到但打印列表却能看到。入栈和写台账的顺序建议先写台账再入栈或者反过来都行但必须保证如果台账写入失败比如 count 超过 100栈操作也要回滚。大作业不要求事务机制但“要么都成功要么都不做”这个思路值得在代码注释里写出来评分老师看到会加分。4.2 出场流程挪车、出栈、队头车补位出场是这个程序里最复杂的一段也是最能体现出“有没有真正理解栈”的地方。一辆车要出场如果它正好在栈顶直接出栈如果不在栈顶它上面压着的车得先挪出去等它开走后再把挪出去的车按原序开回来。这个“原序”两个字是易错点——很多人的实现会把挪出去的车倒序压回来。void handleDeparture() { char plate[12]; int idx findByPlate(plate); // 先在台账里查这辆车 printf(输入出场车牌号: ); scanf(%s, plate); idx findByPlate(plate); if (idx -1) { printf(未找到该车辆\n); return; } int posInStack -1; for (int i 0; i lot.top; i) { if (strcmp(lot.data[i].plate, plate) 0) { posInStack i; // 找到它在车位栈中的下标 break; } } if (posInStack -1) { printf(该车还在等待队列中不能出场\n); return; } // 临时挪车数组保存被堵住的车 Car temp[MAX_CAPACITY]; int tempCount 0; while (lot.top posInStack) { temp[tempCount] lot.data[lot.top--]; // 从栈顶依次弹出 } // 此刻栈顶就是目标车弹出 int parkMinutes getCurrentMinute() - lot.data[lot.top].arrivalMinute; float fee calculateFee(parkMinutes); printf(车辆 %s 出场停车 %d 分钟费用 %.2f 元\n, plate, parkMinutes, fee); lot.top--; // 被挪的车按原序开回 for (int i tempCount - 1; i 0; i--) { lot.data[lot.top] temp[i]; } // 删除台账记录 ledger.records[idx] ledger.records[ledger.count - 1]; ledger.count--; // 等待队列队头车补位 if (queue.front ! queue.rear) { queue.front (queue.front 1) % MAX_WAITING; lot.top; lot.data[lot.top] queue.data[queue.front]; printf(等待队列中的 %s 已进入停车场\n, queue.data[queue.front].plate); } }这段代码里最值得注意的是挪车段的两个循环方向。弹出时从栈顶往下弹顺序是“后进的车先出来”放回时倒序循环从 temp 数组的末尾往前放保证最早被挪出去的车最后回来栈内顺序得以复原。如果把第二个循环写成正序车的相对位置就会翻转整个停车场逻辑随之崩掉。这个细节我要反复强调因为实际调试中遇到的“出场后剩下车辆顺序全乱了”的问题八成就出在这一行。补位逻辑放在台账删除之后是刻意的。先删台账再让等待队列的车进车位操作顺序是“先清理、再插入”避免出现台账里有一辆车既不在栈中也不在队列中的中间状态。补位时把队列数组里的 Car 直接整体赋值给栈顶元素因为 Car 是结构体而不是指针整包拷贝是安全的。4.3 收费计算按小时计费与不足一小时的取舍停车收费是另一个容易被算错的地方。常见的规则是“每小时 X 元不足一小时按一小时算”也有“按分钟累计”的还有“首小时免费之后每小时 X 元”的变体。大作业里一般会给定规则没有的话就选第一种最通用。#define HOURLY_RATE 5.0 // 每小时收费 5 元 float calculateFee(int minutes) { int hours minutes / 60; if (minutes % 60 ! 0) { hours; // 不足一小时按一小时算 } return hours * HOURLY_RATE; }这里的边界条件是 minutes 能不能等于 0。一辆车刚进场就出场minutes 为 0hours 为 0本段代码返回 0 元这符合“未满一小时按一小时计”的规则吗严格来说“不足一小时按一小时”意味着哪怕停了 0 分钟也算一小时。所以在实际实现里我习惯先判断if (minutes 0) return HOURLY_RATE;把等于 0 的情况也框进来。这个细节在实验报告里可以专门写一段“边界条件说明”很能体现严谨性。费率用宏定义而不是魔法数字好处是改需求时只改这一行。有的题目要求“晚上 8 点后收费减半”这类分时段规则那就要给 Car 结构体增加一个入场小时字段或者把 fee 计算改成查表。建议一开始就把calculateFee设计成只依赖 minutes 一个入参的纯函数之后要加分时段逻辑改这一个函数就够了其他地方不用动。4.4 查询与统计按车牌找车和场内车辆盘点查询功能的实现逻辑是先在台账里查车牌找到了再去车位栈遍历看它在第几个车位如果在栈里找不到再去等待队列里找报告排队位置。这两个查找都是线性遍历代码结构相似但要注意区分“查到”和“没查到”时的不同输出。void handleQuery() { char plate[12]; printf(输入查询车牌号: ); scanf(%s, plate); if (findByPlate(plate) -1) { printf(场内无此车辆\n); return; } // 先查车位栈 for (int i 0; i lot.top; i) { if (strcmp(lot.data[i].plate, plate) 0) { printf(车辆 %s 停在 %d 号车位已停 %d 分钟\n, plate, i 1, getCurrentMinute() - lot.data[i].arrivalMinute); return; } } // 再查等待队列 if (queue.front ! queue.rear) { int pos 1; int i (queue.front 1) % MAX_WAITING; while (i ! (queue.rear 1) % MAX_WAITING) { if (strcmp(queue.data[i].plate, plate) 0) { printf(车辆 %s 在等待队列第 %d 位\n, plate, pos); return; } pos; i (i 1) % MAX_WAITING; } } }查询输出里带上“已停 X 分钟”是个小技巧。这样每次调用查询其实就是在做一次出场计费的预计算用户能实时看到自己停了多久、大概要花多少钱。等待队列的遍历要小心循环队列的越界问题从 front1 开始到 rear 结束取模运算不能少否则数组越界读到随机内存程序会时好时坏非常难排查。5. 停车场程序的 5 个经典翻车点现象、原因与修复5.1 出场之后剩下车辆的停车顺序被打乱现象A、B、C 三辆车依次进入B 要出场代码跑完后停车场里的顺序变成了 A、C、B。原因挪车时把临时数组里的车正序放回而不是倒序放回导致被挪车辆的顺序发生了反转。解决放回时从临时数组的最后一个元素开始逐个压回栈。这是栈操作里最容易“看起来对、跑起来错”的问题建议在代码里把注释写成“倒序放回保持原相对顺序”避免自己过几天看代码也忘记当时的意图。5.2 等待队列里的车永远进不了停车场现象车满员后有车进入等待队列等车位空出来等待区的车却没有补位车位就一直空着。原因出场函数里忘了调用补位逻辑或者补位代码写在 return 语句之后成了死代码。解决在出场流程的“删除台账记录”之后紧跟一段判断 queue 是否非空的代码把队头车弹出并压入栈。我习惯的做法是把这个逻辑独立成一个函数void promoteFromQueue()在出场函数末尾调用——这样即使以后出场流程改动也不容易把这步漏掉。5.3 收费总是多算一小时或少算一小时现象停了 59 分钟的车被收了 2 小时的钱或停了 61 分钟的车只收了 1 小时的钱。原因分钟到小时的换算里整数除法直接截断余数没做补一小时的处理或者补一小时的条件写成了minutes % 60 1这种错误判断。解决统一用int hours minutes / 60; if (minutes % 60 ! 0) hours;再把这段逻辑单独封装成calculateFee函数写一个 3 行的自测传入 0、59、60、61、120打印输出确认结果符合预期。收费函数是整个程序里最容易碰“玄学”的地方用边界值自测是最快的排查路径。5.4 打印所有车辆信息时出现空记录或异常车牌现象打印台账出现车牌为空字符串、入场时间为 0 的记录。原因删除台账记录时用了简单的“把最后一个覆盖到删除位置”的做法但删除后没有对 count 做减一或者删除的其实是已经在等待队列里的车而台账里根本没有对应记录。解决删除前先确认idx 0 idx ledger.count删除后立即ledger.count--并且在删除的逻辑里写明注释“保持数组紧凑后续遍历只依赖 count 作为有效边界”。5.5 程序第二次运行时上一次的数据还在现象退出程序再打开场内车辆列表里依然有上一次的记录但又查不到这些车的具体位置出场也报错。原因全局变量在程序启动时没有被初始化而是沿用了上一次进程的内存残留值或者用了文件保存数据但没在启动时加载。解决在 main 里第一条有效代码就调用initParkingLot()把 top、front、rear、count 全部清零。如果做了文件持久化注意清零要在加载文件之前执行否则加载进来的数据会被清零覆盖等于存了个寂寞。6. 从能交差到能拿高分文件持久化与边界用例验证6.1 让程序记住“上次没停完的车”基础功能完成后想拿高分最直接的加分项是文件持久化。思路不复杂程序退出时把当前台账、栈内车辆、等待队列按固定格式写进一个文本文件程序启动时在初始化之后读这个文件把数据恢复到内存里。因为我们要写的是控制台教学程序不用纠结数据库一个文本文件就够。void saveToFile() { FILE* fp fopen(parking_data.txt, w); if (fp NULL) { printf(保存失败\n); return; } fprintf(fp, %d\n, ledger.count); for (int i 0; i ledger.count; i) { fprintf(fp, %s %d\n, ledger.records[i].plate, ledger.records[i].arrivalMinute); } fclose(fp); }恢复的时候逐行读回数据重新插入台账。这里有一个关键点恢复数据后栈和队列的结构也要同步重建否则台账里有记录但栈是空的查询和出场仍然对不上。一个稳健的做法是遍历台账把所有车辆标记为“在等待区”然后从等待区按序把车移入栈直到栈满剩下的留在队列——这样等价于程序退出后所有车重新排队进场一次状态就自动对齐了。6.2 用一组边界用例完成回归验证程序写完以后不要立刻交。花十分钟跑一遍下面的输入序列能覆盖 80% 的隐藏 bug连续入场 6 辆车第 6 辆进等待队列让第 3 辆车出场触发挪车逻辑让等待区的车自动补位马上再让补位的车出场确认它的时间戳正确让一个不存在的车牌出场让一辆已经在等待队列的车再进场。每一步都核对输出是否符合预期尤其是“等待区哪辆车补位了、它的停车时间从什么时候算起”这两个问题——很多人的补位车时间戳是从入队时记的而不是从进车位时记的导致费用计算凭空多出一大截。我的教训是这种程序最容易出错的不是某个单点功能而是“多个功能串起来之后的中间状态”。我从第一次写这个题目到现在每次重构完都会把 6.2 这组用例从头跑一遍跑通了再提交。这一遍跑下来可能只花五分钟但能省下交完作业后被老师追问“为什么你这辆车停了 3 小时却收了 8 小时的钱”的尴尬。希望你也能保留这个习惯先跑通边界再提交成果希望帮到你。本文还有配套的精品资源点击获取
返回列表