bigywater.tech
算法与数据结构 · LeetCode 20

LeetCode 20. 有效的括号

给定一个只包含 ()[]{} 六种字符的字符串,判断括号是否有效:左括号要用同类型的右括号闭合,并且要以正确的顺序闭合。最后一条"顺序"是这道题的核心:最近打开的括号必须最先被关闭,这正是栈的后进先出。本文用真实数据展示"只数每种括号数量"这个常见的错误想法为什么不行,实测栈解法和"反复删除相邻括号对"这种暴力做法的耗时,给出五种语言的实现,并把栈的变化过程做成分步演示。

20,000
组随机对拍,与暴力解的结果全部一致
9 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。基准解不用栈:反复把串里相邻的 ()、[]、{} 删掉,直到删不动,最后剩下空串就是有效的,这是一种独立的写法。一共 14 组用例,覆盖:官方示例、空串、只有一个字符、全是左括号、全是右括号、括号类型交错、右括号在左括号前面。下面是前 6 组的输入与基准答案:

真实实测 · 本机执行输出
输入 ["()"]
输出 true
输入 ["()[]{}"]
输出 true
输入 ["(]"]
输出 false
输入 ["([)]"]
输出 false
输入 ["{[]}"]
输出 true
输入 [""]
输出 true

陷阱:只数数量不够,顺序也要对

一个直觉的想法:三种括号各自的左右数量相等就行。但 ([)] 里每种括号的左右数量都是相等的,顺序却是错的:( 后面接的是 [,而 ) 出现时最近打开的却是 [,没有正确嵌套。下面是只数数量的写法与栈解法的真实结果:

真实实测 · 本机执行输出
# 只比较三种括号各自左右数量是否相等的写法
    '([)]': 只数数量 -> True    solve -> False
      ')(': 只数数量 -> True    solve -> False
    '[(])': 只数数量 -> True    solve -> False
    '{[}]': 只数数量 -> True    solve -> False
  '()[]{}': 只数数量 -> True    solve -> True

前四行只数数量都给出了 True,而正确答案是 False:([)]、)(、[(])、{[}] 数量相等但顺序错误。最后一行 ()[]{} 两种写法都对,说明这个错误只在括号交错或颠倒时暴露。栈解法保证的是"最近打开的括号最先关闭",这才是正确的判定。

栈:五种语言的实现

从左到右扫描字符:遇到左括号就入栈;遇到右括号,栈必须非空,且栈顶必须是与它配对的左括号,否则直接返回无效,配对成功就弹出。扫描结束时,栈必须为空,否则还有左括号没有被闭合。Python、Java、Go、JavaScript、C++ 都是真实编译运行的。

Python · 解法代码def solve(s): pairs = {")": "(", "]": "[", "}": "{"} st = [] for ch in s: if ch in pairs: # 右括号:栈顶必须是与它配对的左括号 if not st or st[-1] != pairs[ch]: return False st.pop() else: # 左括号:入栈,等待以后被配对 st.append(ch) return not st # 栈必须为空,否则还有左括号没配对
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
["()"]  →  true
["()[]{}"]  →  true
["(]"]  →  false
["([)]"]  →  false
["{[]}"]  →  true
[""]  →  true
Java · 解法代码class Solution { public boolean isValid(String s) { Deque<Character> st = new ArrayDeque<>(); for (char ch : s.toCharArray()) { if (ch == ')' || ch == ']' || ch == '}') { // 右括号:栈顶必须是与它配对的左括号 if (st.isEmpty()) return false; char top = st.pop(); if ((ch == ')' && top != '(') || (ch == ']' && top != '[') || (ch == '}' && top != '{')) return false; } else { st.push(ch); // 左括号:入栈 } } return st.isEmpty(); // 栈必须为空 } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
["()"]  →  true
["()[]{}"]  →  true
["(]"]  →  false
["([)]"]  →  false
["{[]}"]  →  true
[""]  →  true
Go · 解法代码func isValid(s string) bool { pairs := map[byte]byte{')': '(', ']': '[', '}': '{'} st := []byte{} for i := 0; i < len(s); i++ { ch := s[i] if open, ok := pairs[ch]; ok { // 右括号:栈顶必须是与它配对的左括号 if len(st) == 0 || st[len(st)-1] != open { return false } st = st[:len(st)-1] } else { // 左括号:入栈 st = append(st, ch) } } return len(st) == 0 // 栈必须为空 }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
["()"]  →  true
["()[]{}"]  →  true
["(]"]  →  false
["([)]"]  →  false
["{[]}"]  →  true
[""]  →  true
JavaScript · 解法代码function isValid(s) { const pairs = { ")": "(", "]": "[", "}": "{" }; const st = []; for (const ch of s) { if (ch in pairs) { // 右括号:栈顶必须是与它配对的左括号 if (st.length === 0 || st[st.length - 1] !== pairs[ch]) return false; st.pop(); } else { st.push(ch); // 左括号:入栈 } } return st.length === 0; // 栈必须为空 }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
["()"]  →  true
["()[]{}"]  →  true
["(]"]  →  false
["([)]"]  →  false
["{[]}"]  →  true
[""]  →  true
C++ · 解法代码#include <string> using namespace std; class Solution { public: bool isValid(string s) { string st; for (char ch : s) { if (ch == ')' || ch == ']' || ch == '}') { // 右括号:栈顶必须是与它配对的左括号 if (st.empty()) return false; char top = st.back(); st.pop_back(); if ((ch == ')' && top != '(') || (ch == ']' && top != '[') || (ch == '}' && top != '{')) return false; } else { st.push_back(ch); // 左括号:入栈 } } return st.empty(); // 栈必须为空 } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
["()"]  →  true
["()[]{}"]  →  true
["(]"]  →  false
["([)]"]  →  false
["{[]}"]  →  true
[""]  →  true
真实实测 · 本机执行输出
共 14 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 14/14
  Java        通过 14/14
  Go          通过 14/14
  JavaScript  通过 14/14
  C++         通过 14/14

复杂度实测:一遍扫描对反复删除

数据是深度嵌套的 ((((…)))),这是反复删除法最坏的情形:每一轮只能删掉最中间的一对,需要 n/2 轮。每个规模取三次中最快的一次:

真实实测 · 本机执行输出
字符串长度(嵌套的 ((((…))))) | 暴力解 | 本文解法
      1000 |     1.287 ms | 0.035 ms
      2000 |     3.184 ms | 0.071 ms
      4000 |     9.695 ms | 0.134 ms
      8000 |    33.196 ms | 0.259 ms

栈解法从 1000 到 8000(8 倍规模)耗时增长约 6.6 倍,接近线性。暴力解增长约 25 倍,理论上的平方增长应当是 64 倍,实测低于理论值,我推测是因为 Python 的字符串 replace 是 C 实现,每一轮的常数很小,但我没有进一步验证这个原因。8000 个字符时,暴力解是 34.0ms,栈解法是 0.25ms,差约 135 倍。

随机对拍

Python 版本另外和暴力解做了随机对拍,其中一半数据是随机拼出的合法串再改动一个字符,这样能覆盖"几乎有效"的边界,纯随机串大多在第一个字符就无效,测不到深处的逻辑:

真实实测 · 本机执行输出
随机对拍 20000 组(一半是长度 0 到 12、字符取自 ()[]{} 的纯随机串(大多不合法),另一半是随机拼出的合法串再随机改动一个字符),solve 与 brute 的结果全部一致。

另外四种语言只在上面 14 组固定用例上验证过,没有做大规模随机对拍。

分步演示:栈怎么变化

下面的演示用 {[()]}[:前面 6 个字符是合法的嵌套,最后多了一个没有闭合的 [。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示的最终结果与 solve 的返回值一致。

有效的括号:s = "{[()]}["
当前字符已配对的字符

    参考与说明

    • 官方题目页:LeetCode 20. 有效的括号。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚;耗时对比只用 Python 测量。
    • 没有覆盖:不用栈、只用计数器加特殊处理的做法(只对一种括号成立);递归下降的判定方法。
    京ICP备2026067502号-1京公网安备11010502063487号