算法精进:如何设计一个“最小栈” (Min Stack)?

35 阅读3分钟

在面试或日常算法练习中,栈(Stack) 是最基础的数据结构之一。但在实际应用中,我们经常遇到一个特殊需求:如何在 O(1)O(1) 的时间复杂度内,随时获取当前栈中的最小值?

本文将结合 JavaScript 代码,通过两种方案的对比,带你深入理解“辅助栈”的设计思想。

1. 基础方案:遍历查找(O(n)O(n)

最直观的想法是在需要最小值时,遍历整个栈来寻找。

代码实现 (1.js)

1.js 中,我们定义了一个基础栈,其 getMin 方法如下:

MiniStack.prototype.getMin = function () {
    let minValue = Infinity; 
    const { stack } = this;
    for (let i = 0; i < stack.length; i++) {
        if (stack[i] < minValue)
            minValue = stack[i];
    }
    return minValue;
}

优缺点分析

  • 优点:不需要额外的存储空间(空间复杂度 O(1)O(1))。
  • 缺点:每次调用 getMin 都需要遍历一次栈,时间复杂度为 O(n)O(n)。如果栈内数据量巨大,性能会显著下降。

2. 进阶方案:辅助栈(O(1)O(1)

为了将时间复杂度降到常数级,我们可以采用空间换时间的策略:引入一个辅助栈stack2),专门用于实时存储当前状态下的最小值。

核心逻辑 (readme.md)

  1. 入栈 (push) :判断入栈元素是否小于等于辅助栈顶。如果是,说明发现了一个更小(或相等)的最小值,将其也放入辅助栈。
  2. 出栈 (pop) :判断主栈弹出的元素是否等于辅助栈顶。如果是,说明当前的最小值被移除了,辅助栈也需要同步弹出。
  3. 获取最小值 (getMin) :辅助栈的栈顶永远是主栈中存在的最小值。

代码实现 (2.js)

const MinStack = function () {
    this.stack = [];
    this.stack2 = []; // 辅助栈:单调递减
}

MinStack.prototype.push = function (x) {
    this.stack.push(x);
    // 只有当辅助栈为空,或者新元素 <= 辅助栈顶时,才入辅助栈
    if (this.stack2.length === 0 || this.stack2[this.stack2.length - 1] >= x) {
        this.stack2.push(x);
    }
}

MinStack.prototype.pop = function () {
    // 如果主栈弹出的正好是当前的最小值,辅助栈同步弹出
    if (this.stack.pop() === this.stack2[this.stack2.length - 1]) {
        this.stack2.pop();
    }
}

MinStack.prototype.getMin = function () {
    // 辅助栈顶即为最小值,时间复杂度 O(1)
    return this.stack2[this.stack2.length - 1];
}

3. 深度对比

特性方案一:遍历法 (1.js)方案二:辅助栈 (2.js)
getMin 时间复杂度O(n)O(n)O(1)O(1)
空间复杂度O(1)O(1) (除原栈外)O(n)O(n) (最坏情况下辅助栈等大)
核心思想动态计算预处理 / 空间换时间
适用场景内存极度受限,查询频率低高频查询最小值

总结

辅助栈的设计其实是一种单调栈思想的变体。通过维护一个非严格单调递减的栈,我们确保了在任何时刻,最小值都能在 O(1)O(1) 时间内被“信手拈来”。

在实际开发中,如果对查询性能有要求,方案二显然是更优的选择。

源码

1.js

const MiniStack = function () {
    this.stack = [];
}

MiniStack.prototype.push = function (val) {
    this.stack.push(val);
}

MiniStack.prototype.pop = function () {
    if (this.stack.length === 0) return null;
    return this.stack.pop();
}

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

// O(n)
MiniStack.prototype.getMin = function () {
    let minValue = Infinity; // 无穷大
    const { stack } = this;
    for (let i = 0; i < stack.length; i++) {
        if (stack[i] < minValue)
            minValue = stack[i];
    }
    return minValue;
}

2.js

const MinStack = function () {
    this.stack = [];
    this.stack2 = [];
}

MinStack.prototype.push = function (x) {
    this.stack.push(x);
    if (this.stack2.length === 0 || this.stack2[this.stack2.length - 1] >= x) {
        this.stack2.push(x);
    }
}

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

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

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