最小栈(Min Stack)详解:用辅助栈实现 O(1) 获取最小值 ✨

81 阅读4分钟

在算法与数据结构中,最小栈(Min Stack)是一个经典问题 🧠。它要求我们设计一个栈,除了支持常规的 pushpoptop 操作外,还能以 O(1) 时间复杂度获取当前栈中的最小元素。

本文将从问题出发,逐步引导你理解如何通过 辅助栈(Auxiliary Stack)高效实现最小栈,并深入探讨其背后的原理——单调栈思想 ⚙️。


一、问题描述 📌

设计一个栈,支持以下操作:

  • push(x) ➕:将元素 x 压入栈
  • pop() ➖:移除栈顶元素
  • top() 🔝:获取栈顶元素
  • getMin() 📉:获取栈中的最小元素

✅ 所有操作的时间复杂度应为 O(1)


二、朴素解法(O(n) 获取最小值)⚠️

最直接的想法是:每次调用 getMin() 时遍历整个栈,找出最小值。

// ES5 构造函数实现(朴素版)
const MinStack = function () {
  this.stack = [];
};

MinStack.prototype.push = function (x) {
  this.stack.push(x);
};

MinStack.prototype.pop = function () {
  return this.stack.pop();
};

MinStack.prototype.top = function () {
  if (this.stack.length === 0) return undefined;
  return this.stack[this.stack.length - 1];
};

MinStack.prototype.getMin = function () {
  let min = Infinity;
  for (let i = 0; i < this.stack.length; i++) {
    min = Math.min(min, this.stack[i]);
  }
  return min;
};

优点:逻辑简单,空间占用少(仅一个栈)
缺点getMin() 时间复杂度为 O(n) ,不满足题目要求 ❌


三、优化方案:引入辅助栈(O(1) 获取最小值)🚀

为了实现 O(1)getMin(),我们需要 空间换时间 💾 —— 使用一个 辅助栈minStack)来实时维护当前栈中的最小值。

🔑 核心思想

  • 主栈 stack:正常存储所有元素
  • 辅助栈 minStack单调非递增栈,栈顶始终是当前主栈中的最小值

📌 单调栈:一种特殊的栈结构,其中元素按某种顺序(如非递增或非递减)排列。这里我们维护一个 非递增 的辅助栈。

🛠️ 操作规则

操作主栈行为辅助栈行为
push(x)压入 xminStack 为空 x <= minStack.top(),则压入 x
pop()弹出栈顶若弹出的元素等于 minStack.top(),则 minStack 也弹出
getMin() 📉直接返回 minStack.top()

⚠️ 注意:使用 <= 而非 < 是为了处理 重复最小值 的情况。例如连续 push 两个 1,若只用 <,第二个 1 不会进入辅助栈,导致 pop 第一个 1 后最小值丢失!


四、代码实现(ES5 构造函数)💻

const MinStack = function () {
  this.stack = [];      // 主栈
  this.minStack = [];   // 辅助栈(单调非递增)
};

MinStack.prototype.push = function (x) {
  this.stack.push(x);
  // 当 minStack 为空,或 x 小于等于当前最小值时,入辅助栈
  if (this.minStack.length === 0 || x <= this.minStack[this.minStack.length - 1]) {
    this.minStack.push(x);
  }
};

MinStack.prototype.pop = function () {
  const top = this.stack.pop();
  // 如果弹出的元素等于当前最小值,则辅助栈也要弹出
  if (top !== undefined && top === this.minStack[this.minStack.length - 1]) {
    this.minStack.pop();
  }
  return top;
};

MinStack.prototype.top = function () {
  if (this.stack.length === 0) return undefined;
  return this.stack[this.stack.length - 1];
};

MinStack.prototype.getMin = function () {
  if (this.minStack.length === 0) return undefined;
  return this.minStack[this.minStack.length - 1];
};

五、示例演示 🎯

假设执行以下操作序列:

const ms = new MinStack();
ms.push(-2);
ms.push(0);
ms.push(-3);
console.log(ms.getMin()); // → -3 📉

ms.pop();
console.log(ms.top());    // → 0 🔝
console.log(ms.getMin()); // → -2 📉

内部状态变化:

操作stackminStackgetMin()
push(-2)[-2][-2]-2
push(0)[-2, 0][-2]-2
push(-3)[-2, 0, -3][-2, -3]-3
pop()[-2, 0][-2]-2

可以看到,minStack 始终保持非递增,且栈顶即为当前最小值 🎯。


六、为什么这是“单调栈”?🧩

辅助栈 minStack 实际上是一个 单调非递增栈(Monotonic Non-Increasing Stack):

  • ✅ 每次只有当新元素 不大于 当前栈顶时才入栈
  • ✅ 这保证了栈中元素从底到顶 不增加
  • ✅ 因此,栈顶永远是最小值,且在主栈弹出最小值时,辅助栈同步弹出,下一个最小值自然浮出

这种设计巧妙地利用了栈的 后进先出(LIFO)特性与单调性,实现了高效的最小值维护 💡。


七、复杂度分析 📊

操作时间复杂度空间复杂度
pushO(1)O(1)(均摊)
popO(1)O(1)
top 🔝O(1)O(1)
getMin 📉O(1)O(1)
  • 总空间复杂度:O(n),最坏情况下(如输入序列单调递减),辅助栈与主栈大小相同。

八、常见误区与注意事项 ⚠️

  1. 比较条件必须用 <=
    🔸 若用 <,重复最小值会导致辅助栈提前弹空,后续 getMin() 出错!
  2. 边界判断不可少
    🔸 在 poptop 中需检查栈是否为空,避免访问 undefined
  3. 不要混淆“最小值”和“全局最小”
    🔸 getMin() 返回的是 当前栈内 的最小值,不是历史最小。

九、总结 🎉

最小栈问题通过引入 辅助栈,将 getMin() 从 O(n) 优化到 O(1),是 空间换时间 的经典范例 💎。其核心在于利用 单调栈 的思想,让辅助栈始终保持非递增结构,从而在任意时刻都能快速访问最小值。

掌握这一技巧,不仅能解决 LeetCode 第 155 题,还能为后续学习更复杂的单调栈应用(如滑动窗口最大值、柱状图最大矩形等)打下坚实基础 🚀。

💡 思考题:能否只用一个栈实现最小栈?(提示:可以,但需要对入栈元素进行编码,牺牲数值范围换取空间)