☰
LeetCode 1172餐盘栈:多栈管理与优先队列应用
2026/11/5 2:41:16 网站建设 项目流程

1. 问题背景与需求分析

LeetCode 1172题"餐盘栈"是一个经典的栈结构变种问题,它模拟了一种特殊的餐厅餐具堆放场景。题目要求我们设计一个DinnerPlates类,该类能够支持以下操作:

  • 初始化指定容量的栈(capacity)
  • push(val):将元素压入最左侧未满的栈
  • pop():弹出最右侧非空栈的栈顶元素
  • popAtStack(index):弹出指定索引栈的栈顶元素

这个数据结构问题在实际工程中有诸多应用场景,比如内存分页管理、分布式存储系统中的数据分片、多线程任务调度等。理解并掌握这类数据结构的设计思想,对于提升算法能力和系统设计思维都大有裨益。

2. 核心数据结构设计

2.1 基础数据结构选择

解决这个问题需要同时考虑以下几个关键因素:

  1. 多个栈的动态管理
  2. 快速定位最左侧未满的栈
  3. 快速定位最右侧非空的栈
  4. 支持随机访问任意栈

基于这些需求,我们可以选择以下数据结构组合:

  • 一个ArrayList来存储所有的栈(便于随机访问)
  • 一个最小堆(优先队列)来记录有空位的栈索引
  • 一个变量记录最右侧非空栈的索引
class DinnerPlates { private List<Stack<Integer>> stacks; private PriorityQueue<Integer> available; private int capacity; private int rightMost; public DinnerPlates(int capacity) { this.capacity = capacity; this.stacks = new ArrayList<>(); this.available = new PriorityQueue<>(); this.rightMost = -1; } }

2.2 关键操作的时间复杂度分析

操作时间复杂度说明
push(val)O(log k)k为有空位的栈数量
pop()O(1)直接访问rightMost指向的栈
popAtStack()O(log k)可能需要将空栈索引加入优先队列

3. 详细实现与边界处理

3.1 push操作的实现细节

push操作需要处理以下几种边界情况:

  1. 当前没有任何栈有空位时,需要新建一个栈
  2. 优先队列中的栈索引可能已经失效(该栈已被填满)
  3. 更新rightMost指针
public void push(int val) { // 处理没有可用栈的情况 if (available.isEmpty()) { stacks.add(new Stack<>()); available.add(stacks.size() - 1); } // 获取最小索引的空栈 int index = available.peek(); Stack<Integer> stack = stacks.get(index); // 压入元素 stack.push(val); // 更新rightMost rightMost = Math.max(rightMost, index); // 如果栈已满,从可用栈中移除 if (stack.size() == capacity) { available.poll(); } }

3.2 popAtStack的特殊处理

popAtStack(index)操作有几个需要注意的细节:

  1. 索引越界检查
  2. 指定栈为空的情况
  3. 弹出元素后该栈变为有空位,需要加入优先队列
  4. 可能需要更新rightMost指针
public int popAtStack(int index) { // 边界检查 if (index < 0 || index >= stacks.size()) { return -1; } Stack<Integer> stack = stacks.get(index); if (stack.isEmpty()) { return -1; } // 弹出元素 int val = stack.pop(); // 如果栈从满变为未满,加入可用栈 if (stack.size() == capacity - 1) { available.offer(index); } // 更新rightMost指针 while (rightMost >= 0 && stacks.get(rightMost).isEmpty()) { rightMost--; } return val; }

4. 性能优化与工程实践

4.1 内存管理优化

在实际工程实现中,我们可以考虑以下优化点:

  1. 当多个栈连续为空时,可以释放这些栈的内存
  2. 使用更高效的数据结构替代Stack类(如ArrayDeque)
  3. 对优先队列进行容量限制和动态调整
// 优化后的pop操作示例 public int pop() { // 处理所有栈都为空的情况 if (rightMost == -1) { return -1; } Stack<Integer> stack = stacks.get(rightMost); int val = stack.pop(); // 栈从非空变为空,需要处理 if (stack.isEmpty()) { available.offer(rightMost); // 更新rightMost指针 while (rightMost >= 0 && stacks.get(rightMost).isEmpty()) { rightMost--; } } else if (stack.size() == capacity - 1) { available.offer(rightMost); } return val; }

4.2 并发安全考虑

如果这个数据结构需要在多线程环境下使用,我们需要考虑:

  1. 对关键操作加锁
  2. 使用线程安全的数据结构
  3. 减少锁的粒度以提高并发性能
// 线程安全版本的push操作 public synchronized void push(int val) { // 原有实现... }

5. 测试用例设计与验证

5.1 基础功能测试

DinnerPlates D = new DinnerPlates(2); // 初始化容量为2的餐盘栈 D.push(1); D.push(2); D.push(3); D.push(4); D.push(5); // 栈状态: [ [1,2], [3,4], [5] ] assert D.popAtStack(0) == 2; // 返回2 // 栈状态: [ [1], [3,4], [5] ] assert D.pop() == 5; // 返回5 // 栈状态: [ [1], [3,4] ] assert D.pop() == 4; // 返回4

5.2 边界条件测试

  1. 测试capacity为1的极端情况
  2. 测试连续popAtStack操作
  3. 测试混合push/pop操作序列
  4. 测试大量数据下的性能表现

6. 实际应用场景扩展

这种数据结构模式在以下场景中有实际应用价值:

  1. 内存分页管理:操作系统需要管理多个内存页,每个页有固定容量,类似于我们的栈
  2. 分布式存储:数据分片存储在多个节点上,每个节点有容量限制
  3. 任务调度系统:多个工作队列,每个队列有最大任务数限制
  4. 数据库连接池:管理多个连接池,每个池有最大连接数限制

理解这类问题的解决方案,可以帮助我们在实际工程中设计更高效的系统架构。例如,在实现一个分布式缓存系统时,我们可以借鉴这种多栈管理的思路来设计数据分片和负载均衡策略。

7. 算法优化与变种思考

7.1 替代数据结构方案

除了使用优先队列,我们还可以考虑以下方案:

  1. 使用TreeSet维护可用栈索引(同样O(log n)时间复杂度)
  2. 使用双向链表维护非空栈(优化pop操作)
  3. 使用位图记录栈的空满状态(空间优化)

7.2 问题变种与扩展

  1. 如果capacity非常大(如10^9),如何优化空间?
  2. 如果支持动态调整capacity,如何设计?
  3. 如果push/pop操作有优先级概念,如何扩展?
  4. 如果栈可以自动合并/分裂,如何实现?

这些扩展问题可以帮助我们更深入地理解数据结构的灵活应用。在实际面试中,面试官可能会基于你的初始解决方案提出这些变种问题,考察你的思维灵活性和算法设计能力。

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

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

立即咨询