Skip to content

停车场系统设计 — OOP 设计 + 空位分配算法

哈希 ⭐⭐ 中级 🔥 经典 OOP

💡 核心要点

HashMap 定位车 + TreeMap/PriorityQueue 找最近空位 + 锁防止重复分配。这是面试中"设计一个 XXX"OOP 题的祖师爷题——多车型分级、入口出口并发、车位定位、计费规则一次性考完。LC1603 是它的最小版本。

为什么单独讲它

停车场是最经典的 OOP 设计题之一,它考察的不是某个具体算法,而是:

  1. 抽象能力:怎么划分 Vehicle / ParkingSpot / Ticket / Floor 等类
  2. 数据结构选择:HashMap、TreeMap、PriorityQueue、BitSet 各有什么用
  3. 并发设计:多入口同时进车,怎么防止两辆车分到同一个位
  4. 可扩展性:未来加电动桩 / 充电费 / 月卡 / 预约怎么改

类似的"设计 XXX"题还有:自动售货机、ATM、酒店预订、共享单车锁车桩、餐厅订座——骨架完全一样,套路通用。

需求澄清(必问)

问题常见答案
多大规模?多少层?中型 1000 位、3 层;超大型按机场 10w 位算
几种车型?摩托 / 小车 / 大车(卡车),对应不同位
收费规则?按小时 / 阶梯 / 月卡 / 首小时免费
入口出口几个?并发吗?4 个入口同时进车要并发安全
找位方式?自动分配最近空位 vs 用户自选
是否要"找车功能"?用车牌反查车位(VIP 服务)
电动桩?加 ChargingSpot 子类
预约?月卡?影响数据模型

核心数据模型

java
enum VehicleSize { MOTORCYCLE, COMPACT, LARGE }        // 三类车

class Vehicle {
    String licensePlate;
    VehicleSize size;
}

class ParkingSpot {
    int floor;
    int row;
    int spotNumber;
    VehicleSize size;                                   // 该位最大支持
    Vehicle currentVehicle;                             // null = 空位

    boolean canFit(Vehicle v) {
        return currentVehicle == null && size.ordinal() >= v.size.ordinal();
    }
}

class Ticket {
    String id;
    String licensePlate;
    int spotId;
    LocalDateTime entryTime;
    LocalDateTime exitTime;                             // null 直到出场
    BigDecimal fee;
}

找位算法 5 选 1

策略数据结构优点缺点
任意分配Stack<ParkingSpot>O(1),最简单不是最近,用户体验差
顺序遍历找最近List<ParkingSpot> 按距离排好O(n) 简单1w 位时慢
TreeMap 按距离TreeMap<Integer, Queue<Spot>> distance → spotsO(log n) 找最近实现稍复杂
PriorityQueue 每个 size 一队Map<VehicleSize, PriorityQueue<Spot>> 按距离O(log n),按车型分桶生产标配
BitSet 按层位图扫描BitSet[] 每层一个内存极省(10w 位 12KB)自定义距离函数复杂

生产推荐:PriorityQueue 按车型分桶

java
class ParkingLot {
    // ★ 每种车型一个最小堆(按到入口的距离排序)
    private final Map<VehicleSize, PriorityQueue<ParkingSpot>> availableSpots = new HashMap<>();
    // ★ 车牌 → 车位(用于反查 "我的车在哪")
    private final Map<String, ParkingSpot> vehicleLocations = new ConcurrentHashMap<>();
    // ★ 车牌 → ticket(出场计费要查)
    private final Map<String, Ticket> activeTickets = new ConcurrentHashMap<>();

    private final ReentrantLock lock = new ReentrantLock();  // ★ 多入口并发锁

    public Ticket park(Vehicle vehicle) {
        lock.lock();
        try {
            ParkingSpot spot = findSpot(vehicle);
            if (spot == null) throw new ParkingLotFullException();

            spot.currentVehicle = vehicle;
            availableSpots.get(spot.size).remove(spot);  // ★ 从可用池移除
            vehicleLocations.put(vehicle.licensePlate, spot);

            Ticket ticket = new Ticket(/* ... */);
            activeTickets.put(vehicle.licensePlate, ticket);
            return ticket;
        } finally {
            lock.unlock();
        }
    }

    private ParkingSpot findSpot(Vehicle v) {
        // ★ 先找完全匹配车型的位
        PriorityQueue<ParkingSpot> exact = availableSpots.get(v.size);
        if (exact != null && !exact.isEmpty()) return exact.peek();

        // ★ 找更大的位(小车可以停大位,反过来不行)
        for (VehicleSize size : VehicleSize.values()) {
            if (size.ordinal() <= v.size.ordinal()) continue;
            PriorityQueue<ParkingSpot> q = availableSpots.get(size);
            if (q != null && !q.isEmpty()) return q.peek();
        }
        return null;
    }

    public BigDecimal exit(String licensePlate) {
        lock.lock();
        try {
            Ticket ticket = activeTickets.remove(licensePlate);
            ParkingSpot spot = vehicleLocations.remove(licensePlate);
            if (ticket == null || spot == null) throw new IllegalArgumentException();

            spot.currentVehicle = null;
            availableSpots.get(spot.size).offer(spot);   // ★ 还回池子

            ticket.exitTime = LocalDateTime.now();
            ticket.fee = calculateFee(ticket);
            return ticket.fee;
        } finally {
            lock.unlock();
        }
    }
}

并发优化:分段锁 / 无锁化

方案适用优势代价
ReentrantLock 全局锁(上面)入口数 ≤ 4简单正确高并发瓶颈
分段锁:每个 size 一把锁入口数 8–32不同车型并行实现复杂
无锁 ConcurrentLinkedQueue不要求"最近"高并发不是最优位
乐观锁 CASAtomicReference<Spot>极致性能真正并发重试逻辑
分布式 Redis ZSet多停车场连锁跨机房网络延迟

面试金句:单停车场用 ReentrantLock + PriorityQueue 足够(入口物理上就 2-4 个);连锁停车场(机场 / 商业综合体)才需要 Redis ZSet 做集中调度。

计费规则的设计模式

收费规则容易变化(首小时免费、阶梯计费、节假日加价),用策略模式

java
interface PricingStrategy {
    BigDecimal calculate(Duration duration, VehicleSize size);
}

class FlatHourlyPricing implements PricingStrategy { /* 5 元/小时 */ }

class TieredPricing implements PricingStrategy {
    // 首 1 小时 5 元,2-4 小时 8 元/小时,4+ 小时 6 元/小时
}

class WeekendPricing implements PricingStrategy {
    // 装饰器模式:周末 1.5 倍
    private final PricingStrategy base;
}

关键陷阱

陷阱后果正确做法
HashMap 不加锁两入口同时分到同一位ConcurrentHashMap + 找位时锁定
小车不能停大位大位浪费就近升级:找匹配位 → 找更大的位
PriorityQueue.remove(spot) O(n)出场慢接受 O(n)(停车场内存小);或用 TreeSet
计费规则硬编码改价要改代码策略模式 + 配置中心热更新
不存"哪个车在哪个位"找车功能做不了vehicleLocations: Map<plate, Spot>
activeTickets 内存泄漏不正常出场(车开走没扫码)TTL 24h + 异常告警

复杂度

  • 入场:O(log n) 找位 + O(1) 记录
  • 出场:O(log n) 还位 + O(1) 计费
  • 找车:O(1) HashMap 反查
  • 空间:O(总位数)

业界生产级架构

                    ┌──────────────────────┐
                    │   入口 / 出口 道闸   │   ← 车牌识别
                    └──────────┬───────────┘
                               │ HTTP API
                    ┌──────────▼───────────┐
                    │  Parking Service     │   ← 找位 / 计费
                    │  (单实例 + 锁)       │
                    └──────────┬───────────┘

              ┌────────────────┼────────────────┐
              ▼                ▼                ▼
        ┌──────────┐    ┌──────────┐    ┌──────────┐
        │  Redis   │    │  MySQL   │    │  Payment │
        │ (车位状态)│    │ (Ticket) │    │  (微信/Alipay)│
        └──────────┘    └──────────┘    └──────────┘


        车位电子显示屏 / 楼层引导灯

实际系统模块

模块实现
车牌识别OCR + AI 模型(YOLOv8、PaddleOCR)
车位检测地感线圈 + 摄像头视觉
预约 / 月卡占位逻辑 + TTL 释放
找车功能微信小程序输入车牌返回楼层 + 导航
反向寻车室内 BLE/UWB 定位精确到 1-2 米
电动桩占用充电中标识 + 充满后释放占位倒计时
拥堵均衡楼层人数分布 + 引导到空闲层

类比题 / 同模板

题目共同骨架
设计酒店预订系统房间 = ParkingSpot,订单 = Ticket,房型 = VehicleSize
设计共享单车车桩 = ParkingSpot,车 = Vehicle,骑行时间 = 计费
设计 ATM状态机 + 钞票库存 = ParkingSpot 池
设计自动售货机槽位 = ParkingSpot,商品 = Vehicle,找零 = 贪心
设计图书馆借阅书架位 = ParkingSpot,借书证 = Ticket
设计电影院选座区位定价 + 同行就近 = 找位算法变种

通用模板:枚举资源类型 + Map 反查 + PriorityQueue 或 TreeMap 找最优 + 锁 / 分段锁。

面试常问 & 怎么答

为什么用 PriorityQueue 而不是 List

要找"最近的空位",PriorityQueue 按距离自动排序,peek() O(1)。List 要 O(n) 遍历,或者保持有序 List 插入 O(n)。

多入口同时进车怎么防止分到同一个位

最小可用方案ReentrantLock 全局锁,找位 + 占位是一个原子操作。进阶:按 size 分段锁,三种车型互不阻塞。极致:CAS 乐观锁 + 重试。

小车能停大位吗

可以。找位策略先找完全匹配的位,没有再找更大的位(升级停)。反过来不行——大车进不去小位。

计费规则变化怎么办

策略模式 + 配置中心PricingStrategy 接口隔离不同计费规则(按时 / 阶梯 / 月卡 / 节假日),运营在后台改配置就生效,不用改代码。

找车功能怎么做

维护 Map<licensePlate, ParkingSpot> 反查表,O(1)。用户在小程序输入车牌返回楼层 + 区位号,加 BLE/UWB 室内导航。

如何应对停车场满

  1. 预约系统:未到预约时段保留位
  2. 错峰引导:到下一个有空位的停车场(连锁场景)
  3. 等待队列:通知列表 + 出场时按序通知
  4. 价格调节:满载时小幅涨价分流

看到什么就先想到这类

信号套用
资源池 + 类型分级停车场(车型→车位)
找"最近的可用资源"PriorityQueue + 距离函数
反查"X 在哪"Map<key, Resource>
多入口竞争资源锁 / 分段锁 / CAS
计费 / 价格易变策略模式 + 配置中心
OOP 设计题"设计 XX 系统"停车场模板:枚举 + Map + 锁 + 策略