【简单】20. 有效的括号

1 阅读3分钟

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

示例 1:

输入: s = "()"

输出: true

示例 2:

输入: s = "()[]{}"

输出: true

示例 3:

输入: s = "(]"

输出: false

示例 4:

输入: s = "([])"

输出: true

示例 5:

输入: s = "([)]"

输出: false

提示:

  • 1 <= s.length <= 104
  • s 仅由括号 '()[]{}' 组成

1. 生活案例:消消乐或吃掉火锅食材

想象你在玩一个括号版的“消消乐”或者吃火锅

  • 规则:左括号 ([{ 是你放进锅里的生食材。右括号 )]} 是对应的调料蘸料

  • 约束

    1. 你必须按照“后放进去的先吃掉”的顺序。比如你最后放了金针菇(左括号),你必须先拿金针菇的蘸料(对应的右括号)把它消掉。
    2. 如果你拿错了蘸料(比如拿了肥牛的蘸料去配金针菇),或者锅里没食材了你却拿了蘸料,这顿饭就“无效”了。
    3. 最后吃完时,锅里不能剩下任何没配对的生食材。

2. 代码实现与详细注释

这是你图片中的代码,我为你添加了详细的中文注释。这段代码用了一个很聪明的技巧:遇到左括号,就在栈里存它对应的右括号,这样后面比对时会非常直接。

JavaScript

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function (s) {
    // 1. 建立一个【配对手册】:看到生食材(左括号),查一下它对应的蘸料(右括号)是什么
    let mapping = {
        '{': '}',
        '[': ']',
        '(': ')'
    }

    let res = []; // 【火锅/栈】:存放等待配对的“期望右括号”

    for (let char of s) {
        // 如果在手册里能找到这个字符,说明它是“左括号”
        if (mapping[char]) {
            // 【技巧】:我们不存左括号,而是直接把“它期待的右括号”扔进栈里
            // 比如看到 '(',我们就往栈里扔一个 ')'
            res.push(mapping[char]);
        } else {
            // 否则,它就是“右括号”
            // 逻辑:把它和栈顶(最后放进去的那个期望)比一下
            // 如果【栈空了】或者【对不上号】,说明顺序乱了,直接判死刑
            if (char !== res.pop()) {
                return false;
            }
        }
    }

    // 最后检查:如果栈空了,说明大家都消掉了,有效!
    // 如果还剩下东西,说明有人没配上对
    return res.length === 0;
};

3. 核心原理解析

为什么这个“提前存入右括号”的方法很妙?

通常的做法是存入左括号,匹配时判断 if(left == '(' && right == ')')

但你代码里的写法更简洁:

  • 遇到 [ \rightarrow 栈里压入 ]
  • 下一步如果遇到右括号,直接看它是不是等于栈顶弹出的那个元素即可。
  • 代码逻辑从“判断是否配对”简化成了“判断是否相等”。

错误情况拆解:

  1. 类型不匹配([)]

    • 压入 ),再压入 ]
    • 遇到 ),但栈顶弹出的是 ]] 不等于 ),返回 false
  2. 左边多了(()

    • 遍历完后栈里还剩一个 )res.length !== 0,返回 false
  3. 右边多了())

    • 第二个 ) 匹配完后栈空了,第三个 ) 进来时 res.pop()undefined,返回 false

复杂度分析

  • 时间复杂度O(n)O(n)。只需要把字符串从头到尾摸一遍。
  • 空间复杂度O(n)O(n)。最坏的情况是全是左括号 (((((,全堆在栈里。