目标
实现一个"停车场管理"调度系统:停车场内部车道用栈模拟(后进的车堵住先进的车,取车要倒出来),门外便道用队列模拟(先进先出排队等候)。本 Lab 是栈与队列联动的超级大综合,要求你把 LIFO 与 FIFO 两种语义放进同一个业务流程,并完成倒车、补位、中间删除、统计四个关键环节。
背景阐述
某停车场只有一条狭长内部车道,车辆只能排成一列、先进后出:停在里侧的车要出来,必须先把外侧挡路的车一辆辆倒出去,再倒回原处。停车场门口还有一条便道,供暂时进不了场地的车辆按到达顺序等待;自动补位时始终让队头车辆先入场。车辆也可以主动离开便道,因此“离开系统”和“按 FIFO 补位入场”是两种不同操作。
你要实现它的完整调度内核,并输出每一步的场内、便道状态与最终统计。
前置知识
- 第 2.1 节栈的 ADT 与 LIFO 语义、顺序栈 / 链栈实现;
- 第 2.2 节队列的 ADT 与 FIFO 语义、循环队列 / 链队列实现;
- 第 1 章线性表的顺序 / 链式存储(用于理解"中间删除"为何需要临时结构)。
建议用时
300~420 分钟(5~7 小时),建议分 3 次提交:先搭栈 + 队列骨架,再做倒车与补位,最后做中间删除与统计。
场景与数据结构选型
| 场景 | 数据结构 | 语义 | 容量 |
|---|---|---|---|
| 停车场内部车道 | 栈(lot) | 后进先出,里侧车被外侧车挡住 | 上限 N |
| 门外便道 | 队列(waiting) | 先进先出,先到先入场 | 上限 M |
| 取车倒车 | 临时栈(temp) | 暂存被倒出的车,保持原顺序 | 临时 |
关键约束:lot 和 waiting 请你自己实现(不要直接用 std::stack/std::queue),复用 2.1 / 2.2 学到的顺序或链式实现,并让它们暴露 push / pop / front / size / is_empty / is_full 等接口。倒车与补位用到的临时结构同样如此。
车辆与状态机
每辆车用 车牌号(字符串)唯一标识。车辆经历以下状态:
ARRIVING(到达)
├─ 场内未满 ──► IN_LOT(在场内,栈中)
└─ 场内已满 ──► IN_WAITING(在便道,队列中)
IN_LOT / IN_WAITING ──► DEPARTED(离开)维护一张"车辆 → 到达时刻"的记录表(用 std::vector<pair<车牌, 时刻>> 即可,不要求哈希),用于统计停留时间。
任务
模块一:到达与入场(必做)
实现 arrive(plate):
- 先检查车牌是否已存在于场内或便道;若已存在则拒绝本次到达、拒绝计数加 1,状态不变;
- 若
lot.size() < N→ 记录到达时刻并执行lot.push(plate),打印"进入停车场"; - 否则若
waiting.size() < M→ 记录到达时刻并执行waiting.push(plate),打印"进入便道等候"; - 否则 → 打印"停车场与便道均已满,拒绝入场",只增加拒绝计数,不创建车辆记录。
每次成功进入场内或便道都计为一次“成功接纳”;便道中的等待时间也属于停留时间。车辆离开后删除其活动记录,因此同一车牌之后可以再次到达,并按新的一次接纳统计。
模块二:离开与倒车(必做,核心)
实现 depart(plate),分三种情况:
情况 A:车在停车场内(栈中)
用临时栈倒车:
temp 清空
moved_count = 0 // 本次操作的局部计数
while lot 非空 且 lot.top() != plate:
temp.push(lot.top()); lot.pop()
moved_count += 1
若 lot 空(没找到):
while temp 非空: // 先恢复原状态
lot.push(temp.top()); temp.pop()
报错并返回 // 不修改全局倒车统计
否则 lot.pop() // plate 出场
总倒车次数 += moved_count // 只在确认找到后提交统计
while temp 非空:
lot.push(temp.top()); temp.pop(); // 按原顺序倒回情况 B:车在便道中(队列中)
队列不支持直接删除中间元素,可以用临时队列重建便道:
temp 清空
removed = false
q = waiting.size()
重复 q 次:
car = waiting.front(); waiting.pop()
若 !removed 且 car == plate:
removed = true // 跳过目标车辆
否则:
temp.push(car)
while temp 非空:
waiting.push(temp.front()); temp.pop() // 按原顺序重建 waiting
若 !removed → 报错;此时 waiting 已恢复,状态不变若成功移除目标车辆,还必须按当前指令时刻计算其停留时间,增加“已离开车辆数”和总停留时间,并删除活动到达记录。便道车辆主动离开不会腾出场内车位,因此不触发自动补位;其余车辆的相对顺序必须保持不变。
情况 C:车既不在场内也不在便道
打印"车辆不存在",保持场内、便道和统计状态不变。
模块三:便道补位(必做)
每当有车从停车场离开、且 waiting 非空、且 lot.size() < N 时,让便道队头车辆自动补位进场:
while waiting 非空 且 lot.size() < N:
lot.push(waiting.front()); waiting.pop();这一步写成循环,直到“场内满”或“便道空”为止。按本 Lab 的标准 depart 操作,一辆场内车离开只会腾出一个位置,因此通常最多补位一辆;保留循环可让该逻辑在以后支持批量离场或容量调整时继续正确。
模块四:状态查询与统计(必做)
status():打印当前场内(从栈底到栈顶)与便道(队头到队尾)车辆列表;- 结束时统计:成功接纳次数、已离开车辆数、已离开车辆的总停留时间与平均停留时间、总倒车次数(取车时倒出的车辆数之和)、拒绝入场次数。
统计口径固定为:停留时间 = 离开时刻 - 到达时刻;总停留时间只累加已经执行成功 D 的车辆,平均值的分母也是已离开车辆数。仍在场内或便道中的车辆不计入停留时间;若尚无车辆离开,平均停留时间输出 0.00。
输入输出格式(命令行)
main() 从 stdin 读入:
第一行:两个整数 N M (停车场上限 N、便道上限 M,N≥1、M≥1)
后续每行一条指令,直到 EOF 或 E:
A <plate> 车辆到达
D <plate> 车辆离开
S 打印当前状态
E 结束并打印统计约定:每条指令占用一个时间单位,第 i 条指令对应时刻 i。车辆到达时刻 = 该条
A指令的序号。
示例输入
2 2
A 粤A0001
A 粤A0002
A 粤A0003
A 粤A0004
D 粤A0001
A 粤A0005
D 粤A0003
S
E示例输出
[时刻1] 粤A0001 进入停车场 | 场内=[粤A0001] | 便道=[]
[时刻2] 粤A0002 进入停车场 | 场内=[粤A0001, 粤A0002] | 便道=[]
[时刻3] 场内已满,粤A0003 进入便道 | 场内=[粤A0001, 粤A0002] | 便道=[粤A0003]
[时刻4] 场内已满,粤A0004 进入便道 | 场内=[粤A0001, 粤A0002] | 便道=[粤A0003, 粤A0004]
[时刻5] 粤A0001 离开,倒出 1 辆 [粤A0002],停留 4 | 场内=[粤A0002] | 便道=[粤A0003, 粤A0004]
便道补位:粤A0003 进入停车场 | 场内=[粤A0002, 粤A0003] | 便道=[粤A0004]
[时刻6] 场内已满,粤A0005 进入便道 | 场内=[粤A0002, 粤A0003] | 便道=[粤A0004, 粤A0005]
[时刻7] 粤A0003 离开,倒出 0 辆,停留 4 | 场内=[粤A0002] | 便道=[粤A0004, 粤A0005]
便道补位:粤A0004 进入停车场 | 场内=[粤A0002, 粤A0004] | 便道=[粤A0005]此时场内已经达到上限 N=2,因此补位循环立即停止,粤A0005 继续留在便道。
状态查询 S 输出
STATUS:
场内(栈底→栈顶): [粤A0002, 粤A0004]
便道(队头→队尾): [粤A0005]结束 E 输出
===== 统计 =====
成功接纳次数: 5
已离开车辆数: 2
总停留时间: 8
平均停留时间: 4.00
总倒车次数: 1
拒绝入场次数: 0错误样例测试(必须通过)
程序不得崩溃,必须给出明确错误信息:
| # | 错误场景 | 输入(N=2, M=2) | 预期行为 |
|---|---|---|---|
| 1 | 离开不存在的车 | D 粤X9999 | 打印"车辆不存在",状态不变 |
| 2 | 场内便道都满再入场 | 连续 5 次 A | 第 5 辆打印"拒绝入场",不崩溃 |
| 3 | 从便道中途离开 | A×4 后 D 第 3 辆(在便道) | 从队列中移除该车,其余车保持顺序 |
| 4 | 空场离开 | 启动后直接 D | 打印"车辆不存在",不崩溃 |
| 5 | 停车场只有 1 个车位取最里车 | N=1,A×1 后 D 该车 | 倒出 0 辆,正常出场 |
| 6 | 非法容量 | 首行 0 2 或 2 0 | 打印"容量必须≥1",退出 |
| 7 | 重复到达同一车牌 | 同一 plate 两次 A | 第二次拒绝(车牌已存在),不崩溃 |
| 8 | 未知指令 | X 123 | 提示"未知指令",跳过 |
提示点与易错点
- 倒车顺序:倒出时先 push 进
temp的,倒回时要先 pop 再 push 回lot,这样才能保持原顺序。倒反了会导致场内顺序错乱。 - 补位循环必须同时检查两个条件:使用
waiting 非空 && lot.size() < N。标准单车离场通常只补一辆,但循环写法可以安全覆盖未来的批量离场或容量调整;缺少容量判断会把车放进已满的停车场。 - 便道中间删除:标准队列只能操作队头队尾,删除队中元素必须用临时队列倒腾;倒腾时同样要保证其余车的相对顺序不变。
- 车牌唯一性:到达时要查重,避免同一辆车重复入场;离开后要把它的记录移除。
- 停留时间口径:
停留时间 = 离开时刻 − 到达时刻,单位是"指令序号差"。在便道等位的车,等待时间也算进停留时间。 - N=1 的退化情况:场内只能停一辆,取车永远不用倒车(倒出 0 辆),别让临时栈逻辑在空栈上出错。
- 栈与队列的分工:场内用栈是因为"堵车倒车"本质是 LIFO;便道用队列是因为"先到先进"本质是 FIFO。两者不可互换——想一想:如果场内也用队列、便道也用栈,调度会变成什么样?
复杂度分析要求
在说明文档里回答:
- 使用线性容器检查重复车牌时,一次
arrive的最坏时间复杂度是多少? - 一次
depart(车在停车场内、需倒出 辆)的栈操作复杂度与辅助空间是多少?若定位车辆和删除std::vector中的活动记录也采用线性扫描,完整业务操作的最坏复杂度又是多少? - 便道长度为
时,从中间删除一辆车的时间与辅助空间复杂度是多少?为什么复杂度不只取决于目标在第几位? - 一次标准
depart触发的补位最多移动多少辆车?为什么代码仍可以保留while? - 如果停车场内部改成队列(而非栈),取最里侧的车会付出什么代价?用具体例子说明栈语义为何更贴合该场景。
提交物
- 完整可运行源码,建议目录
labs/chapter-02/project/P-02-03-parking-lot-management/:stack.h/queue.h(自实现的栈与队列,可复用 2.1/2.2 代码)parking_lot.h/parking_lot.cpp(调度内核:到达 / 离开 / 补位 / 统计)main.cpp(命令行驱动)
- 运行结果输出:覆盖示例输入、错误样例测试表全部 8 条、验收清单全部场景。
- 设计说明(500~800 字),覆盖"复杂度分析要求"的 5 个问题。
- 单元测试:
arrive/depart(三种情况)/ 补位各至少一组正常用例 + 一组边界用例。
验收标准
到达与入场
离开与倒车
便道补位
统计与健壮性
加分项(任选 ≥ 1 项)
- 费用结算:按车型(小车 / 大巴)与停留时长收费,输出每辆车费用与总收入。
- 实时排队叫号:便道车辆按"预约时间"而非"到达顺序"入场(这会把队列换成优先级结构,注意可能超纲,可作为思考题而非必做)。
- 持久化:把场内 / 便道状态存文件,下次启动恢复。
- 可视化:每次状态变化输出一段 ASCII 示意图(如
[粤A1 | 粤A2] <- 场内入口),便于肉眼核对倒车与补位过程。
延伸思考
- 停车场内部车道为什么必须用栈?如果改造成"环形车道"(首尾相连),栈还能描述它吗?这引出了什么数据结构?(提示:循环队列)
- 便道如果要支持"预约插队",FIFO 队列还够用吗?需要什么结构?(提示:这指向后续章节的堆 / 优先队列,属于超纲内容,仅作思考)
- 本 Lab 的"倒车"和 Lab 02-11(可撤销浏览器)的"后退"都用到了栈,但语义不同——一个是为了取出中间元素,一个是为了记录历史。说说两者本质区别。