最小栈的 O(1) 之道:辅助栈与重复值的正确打开方式

71 阅读3分钟

从 O(n) 到 O(1):最小栈的优化之路

最小栈(Min Stack)是一个经典的数据结构面试题,要求我们设计一个栈,除了支持常规的 pushpoptop 操作外,还能以 常数时间 获取当前栈中的最小值。本文将带你从最直观但效率较低的 O(n) 实现出发,逐步理解如何通过“辅助栈”技巧将其优化为真正的 O(1) 时间复杂度。

一、朴素实现:每次遍历找最小值(O(n))

首先,我们最容易想到的方法是——用一个普通数组模拟栈,然后在需要获取最小值时遍历整个栈:

// 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 || !this.stack.length) return;
  return this.stack[this.stack.length - 1];
};

// O(n) 时间复杂度
MinStack.prototype.getMin = function() {
  let minValue = Infinity;
  const { stack } = this;
  for (let i = 0; i < stack.length; i++) {
    minValue = Math.min(minValue, stack[i]);
  }
  return minValue;
};

这个实现非常直观,但问题也很明显:每次调用 getMin() 都要遍历整个栈,时间复杂度为 O(n)。如果频繁调用 getMin(),性能会成为瓶颈。

那么,有没有办法让 getMin() 也达到 O(1) 呢?

二、优化思路:用空间换时间 —— 辅助栈

答案是肯定的。我们可以引入一个辅助栈(minStack) ,专门用来记录当前主栈中的最小值。

核心思想:

  • 主栈 stack 负责正常入栈出栈。
  • 辅助栈 minStack 的栈顶始终保存当前主栈中的最小值
  • 入栈时:如果新元素 ≤ minStack 栈顶,则也将其压入 minStack
  • 出栈时:如果主栈弹出的元素等于 minStack 栈顶,则同步弹出 minStack 的栈顶。

这样,getMin() 只需返回 minStack 的栈顶,时间复杂度为 O(1)!

看完你是不是觉得似乎并不难理解?,然而自信满满的你可能已经落入陷阱!!!
有时候一行代码足以让你的整个逻辑毁于一旦-----> 一行代码的代价:最小栈为何要用 >= 而不是>

代码实现(O(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 || this.minStack[this.minStack.length - 1] >= x) {
    this.minStack.push(x);
  }
};

MinStack.prototype.pop = function() {
  const popped = this.stack.pop();
  // 如果弹出的值等于当前最小值,辅助栈也要同步弹出
  if (popped === this.minStack[this.minStack.length - 1]) {
    this.minStack.pop();
  }
};

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

// O(1) 时间复杂度!
MinStack.prototype.getMin = function() {
  return this.minStack[this.minStack.length - 1];
};

关键点解析:
对于入栈操作时,使用>=而不是>,这是因为考虑到当我们连续压入相同的元素入栈时,如果使用>那么就会导致,只有第一个可能会入辅助栈,而后续的无法进入。这会导致一个什么问题呢?--->当我们执行出栈操作时,会导致辅助栈提前清空,因为存在多个相等的元素,这就有可能提取弹出了最小元素,导致结果出错。所以使用>=是为了处理重复最小值的情况

三、为什么这个方法有效?

关键在于单调性minStack 是一个非严格递减栈(单调栈的一种)。它只在遇到更小或相等的值时才增长,因此栈顶永远代表“从栈底到当前位置”的最小值。

这正是“最小栈”问题的经典解法,也是面试中高频考察的思维模型——用额外空间换取时间效率

四、总结

方法getMin() 时间复杂度空间复杂度适用场景
朴素遍历O(n)O(1)数据量小、getMin 调用少
辅助栈O(1)O(n)高频查询最小值,追求性能

通过引入辅助栈,我们将最小值查询从线性时间优化到了常数时间,完美满足了题目要求。这种“空间换时间”的思想,在算法设计中极为常见,值得深入掌握。


希望这篇文章能帮你清晰理解最小栈的优化过程!如果你正在准备面试,不妨动手实现一遍,并思考边界情况(如空栈、重复最小值等)。

Happy coding! 🚀