LeetCode 20. 有效的括号:为什么「栈」是最自然的解法?

10 阅读3分钟

一、题目回顾

给定一个只包含以下字符的字符串:

'(' ')' '{' '}' '[' ']'

要求判断字符串是否是 有效的括号序列

有效的定义是:

  1. 左括号必须用 相同类型的右括号 闭合
  2. 左括号必须以 正确的顺序 闭合

示例:

"()"        -> true
"()[]{}"    -> true
"(]"        -> false
"([)]"      -> false
"{[]}"      -> true

二、这道题到底在考什么?

表面上看是字符串,
但核心问题其实是:

如何判断“后出现的右括号”,能否和“最近的左括号”匹配?

注意关键词:
最近

这意味着一种典型的结构:

  • 后进来的左括号
  • 必须先被匹配掉

这正好符合一个数据结构的特性:栈(Stack)


三、为什么用栈?直觉解释

我们从左到右遍历字符串:

  • 遇到左括号:
    暂时还不知道它什么时候会被匹配,先存起来
  • 遇到右括号:
    必须和 最近一个尚未匹配的左括号 配对

而“最近一个未处理的元素”——
正是栈的使用场景。

一句话总结:

左括号入栈,右括号出栈并校验


四、整体解题思路

整个算法可以拆成四步:

  1. 使用一个栈存放左括号

  2. 遍历字符串中的每一个字符

  3. 左括号直接入栈

  4. 右括号时:

    • 栈为空 → 非法
    • 栈顶不匹配 → 非法
  5. 遍历结束后:

    • 栈为空 → 合法
    • 栈非空 → 非法

五、完整代码

class Solution {
    public boolean isValid(String s) {
        if (s.length() == 0) {
            return true;
        }

        Stack<Character> stack = new Stack<>();

        for (char ch : s.toCharArray()) {
            if (ch == '(' || ch == '[' || ch == '{') {
                stack.push(ch);
            } else {
                if (stack.isEmpty()) {
                    return false;
                }

                char c = stack.pop();

                if (ch == ')' && c != '(') return false;
                if (ch == ']' && c != '[') return false;
                if (ch == '}' && c != '{') return false;
            }
        }

        return stack.isEmpty();
    }
}

下面按逻辑顺序拆解。


六、为什么一开始要判空字符串?

if (s.length() == 0) {
    return true;
}

空字符串没有任何未闭合的括号,
自然是一个有效括号序列

这一步不是必须,但能让语义更清晰。


七、栈里到底存的是什么?

Stack<Character> stack = new Stack<>();

栈中存放的是:

还没有被匹配的左括号

例如字符串:

"{[("

遍历到当前位置时,栈中内容为:

{ [ (

八、遇到左括号:为什么直接入栈?

if (ch == '(' || ch == '[' || ch == '{') {
    stack.push(ch);
}

原因很简单:

  • 左括号暂时无法判断是否合法
  • 必须等到未来某个右括号出现才能确定

所以策略是:

延迟处理,先保存状态


九、遇到右括号时,为什么要先判空?

if (stack.isEmpty()) {
    return false;
}

这是一个非常关键的边界判断。

情况示例:

")"

或者:

"]()"

右括号出现时,
如果栈为空,说明:

没有任何左括号可以与之匹配

这种情况一定是非法的,必须立刻返回 false。


十、为什么一定要 pop 再判断?

char c = stack.pop();

因为规则要求的是:

当前右括号,必须匹配最近的左括号

而栈顶元素,正是最近入栈、尚未匹配的左括号。


十一、逐种括号匹配判断

if (ch == ')' && c != '(') return false;
if (ch == ']' && c != '[') return false;
if (ch == '}' && c != '{') return false;

这一步做的是:

  • 类型是否一致
  • 顺序是否正确

例如:

"([)]"

执行过程:

  1. '(' 入栈
  2. '[' 入栈
  3. ')' 出现,pop 出 '['
  4. '[' != '(' → 直接非法

这正是题目要过滤掉的情况。


十二、为什么最后还要判断栈是否为空?

return stack.isEmpty();

考虑这种情况:

"((("

遍历结束后:

  • 没有遇到非法情况
  • 但仍然有左括号未被匹配

此时栈非空,说明:

有括号没有闭合

所以结果应为 false。


十三、复杂度分析

  • 时间复杂度:O(n)

    • 每个字符最多入栈、出栈一次
  • 空间复杂度:O(n)

    • 最坏情况下所有字符都是左括号

十四、总结

这道题的核心不是“括号”,而是:

如何用数据结构,保存「尚未完成的状态」