力扣Hot100之栈的经典应用1:有效括号与最小栈

64 阅读4分钟

栈的经典应用:有效括号与最小栈

栈作为一种 “先进后出”(LIFO)的数据结构,在处理具有 “匹配关系” 或 “最值追踪” 需求的问题时展现出极高的效率。本文将聚焦两道 LeetCode 高频题目 ——“有效括号” 和 “最小栈”,通过拆解解题思路、剖析代码细节,带你掌握栈在实际场景中的核心用法。

155. 最小栈

力扣(LeetCode)题目链接

解题思路

为什么不能每次遍历找最小值?

若在 getMin() 中遍历整个栈,时间复杂度为 O (n),不满足题目要求。因此,必须在插入和删除时同步维护最小值信息

基础实现(O (n) 时间复杂度 getMin)

// es5 构造函数
// 函数表达式
const MiniStack = function() {
    this.stack = [];    //数组
}
MiniStack.prototype.push = function(x) {
    this.stack.push(x);
}
MiniStack.prototype.pop = function(){
    return this.stack.pop()
}
MiniStack.prototype.top = function() {
    if(!this.stack || this.stack.length === 0) {
        return undefined;
    }
    return this.stack[this.stack.length-1];
}
// O(n)
MiniStack.prototype.getMin = function (){
    // 遍历一遍
    // Infinity 无穷大
    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) 时间复杂度 getMin)

核心策略:辅助栈

引入一个辅助栈 minStack,用于记录 “到当前为止” 的最小值。其维护规则如下:

  • 入栈时:如果新值 val 小于或等于 minStack 的栈顶(即当前最小值),则也将 val 压入 minStack
  • 出栈时:如果主栈弹出的值等于 minStack 的栈顶,则同步弹出 minStack 的栈顶;
  • 查询最小值:直接返回 minStack 的栈顶。

注意:使用 小于等于(≤)  而非小于(<),是为了正确处理重复的最小值。例如连续压入两个 -2,若只记录一次,第一次 pop 后最小值就会丢失。

优化代码

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

/** 
 * @param {number} val
 * @return {void}
 */
MinStack.prototype.push = function(val) {
    this.stack.push(val);
    if(this.stack2.length === 0 || this.stack2[this.stack2.length - 1] >= val){
        this.stack2.push(val);
    }
};

/**
 * @return {void}
 */
MinStack.prototype.pop = function() {
    const pop = this.stack.pop();
    if(this.stack2[this.stack2.length - 1] === pop){
        this.stack2.pop();
    }
};

/**
 * @return {number}
 */
MinStack.prototype.top = function() {
    return this.stack[this.stack.length - 1];
};

/**
 * @return {number}
 */
MinStack.prototype.getMin = function() {
    return this.stack2[this.stack2.length - 1];
};

/** 
 * Your MinStack object will be instantiated and called as such:
 * var obj = new MinStack()
 * obj.push(val)
 * obj.pop()
 * var param_3 = obj.top()
 * var param_4 = obj.getMin()
 */

20. 有效的括号

力扣(LeetCode)题目链接

解题代码

const leftToRight = {
    "(" : ")",
    "{" : "}",
    "[" : "]"
};

const isValid = function(s){
    if(!s) return true;
    
    const stack = [];
    const len = s.length;
    if(len % 2 != 0){
        return false;
    }
    
    for(let i = 0; i < len; i++){
        const ch = s[i];
    
        if (ch ==="(" || ch === "{" || ch ==="["){
            stack.push(leftToRight[ch]); // 关键技巧:存入预期的右括号
        } else {
            if(!stack.length || stack.pop() !== ch){
                return false;
            }
        }
    }
    return !stack.length;
}

内存中的括号追踪器

让我们走进代码执行的微观世界,看每个字符如何在内存中触发一场精准的 “括号追捕行动”。

const leftToRight = { '(': ')', '{': '}', '[': ']' };

→ 先建立一张 “通缉画像表”:每个左括号的 “同伙” 是谁,一目了然。

if (!s) return true;

→ 空字符串?直接返回 true!没有括号,自然无需匹配,视为有效。

if (len % 2 !== 0) return false;

→ 字符数为奇数?直接判定无效!括号必须成对出现,单数肯定配不齐

const stack = []; // 创建一个“待验证嫌疑人名单”

现在,开始逐字扫描字符串,指针如探照灯扫过每一个字符:

  • 遇到左括号({[):

    • 不存它自己,而是把它的 “通缉目标”(对应右括号)推入 stack 栈顶。
    • 比如看到 [,立刻 push(']') —— 记住:我们等的是 ],不是 [
  • 遇到右括号)}]):

    • 第一问:stack 是否为空?若空,说明没人预告你要来 → 非法入侵!

    • 第二问:stack.pop() 弹出的期待值是否等于当前字符?

      • 相等?✅ 匹配成功,继续;
      • 不等?❌ 身份不符,立即终止!

循环结束后:

return !stack.length;

→ 最终审判:如果 “嫌疑人名单” 清空,说明所有期待都被满足;若还有残留,说明有左括号 “悬案未破”。

💡 关键洞察:栈中存储的不是历史,而是未来预期。这是本算法高效的核心!


从 “匹配” 到 “追踪”:栈的另一核心用法 —— 最小栈

如果说 “有效括号” 利用栈的 “后进先出” 特性解决了 “成对匹配” 问题,那么 “最小栈” 则需要在此基础上,额外实现 “快速查询最小值” 的功能,这就需要我们对栈的结构进行优化设计。

总结

通过两道经典题目,我们见证了栈的核心应用场景:

  1. 匹配问题(有效括号):利用栈 “后进先出” 的特性,存储 “未来预期的匹配项”,实现高效的成对校验,时间复杂度 O (n)、空间复杂度 O (n);
  2. 最值追踪问题(最小栈):通过 “主栈 + 辅助栈” 的设计,在不影响栈基本操作的前提下,将最小值查询优化至 O (1) 时间复杂度,核心是 “同步维护最值信息”。

栈的本质是对 “顺序依赖” 问题的高效解决 —— 无论是括号的 “先开后闭”,还是最小值的 “动态更新”,都可以通过栈的结构特性简化逻辑。掌握这两道题的解题思路,你将能快速应对各类栈相关的算法题目,理解 “用数据结构优化时间复杂度” 的核心思想。