题目看是算法题,说白了就是给定一个只包含 ( ) { } [ ] 的字符串,让你判断括号的嵌套顺序合不合法。这类问题在 LeetCode 上编号 20,属于栈结构最经典的入门题,很多人把这个当作“每日一练”的第一题,我当初也是这样。今天就把这题的完整拆解思路、多种代码写法、踩坑记录和可以扩散的考点串起来聊聊,文章不是只给你贴一段能过测试的答案,而是希望你看完之后,以后再遇到“符号配对”“嵌套校验”这类问题时能直接迁移思路。
先说清楚,这道题的目标是“有效括号”。有效不只是左右括号数量对得上,顺序也得对。比如 () 有效,但 (] 无效;{[]} 有效,但 ([)] 就是无效的。判断逻辑本身不复杂,难点在于你怎么把“嵌套顺序”翻译成代码逻辑,而这背后牵扯出一个很核心的工具:栈。
1. 题目本质:表面考配对,实际考栈
1.1 先明确题目要求与输入特征
原题是这样描述的:给定一个只包括 (、)、{、}、[、] 的字符串 s,判断字符串是否有效。有效字符串需满足:左括号必须用相同类型的右括号闭合;左括号必须以正确的顺序闭合。这里有个很容易被忽略的点:题目只给了这六种字符,不会出现小写字母、数字、空格,所以不需要做字符过滤。
输入可能的各种形态可以分成几类,我实际测试时会刻意构造这些用例:
- 空字符串:
""是有效的,因为不存在未闭合的括号。 - 单种连续型:
"((((("、")))",这个主要测计数和栈顶判断。 - 简单嵌套型:
"([])"、"{[]}"是有效,因为括号一层套一层,顺序合理。 - 交叉型:
"([)]",这个最迷惑人,左右数量各两个,但顺序不对,必须判无效。 - 边界堆叠型:
"(([]){})"这种混合嵌套,需要程序一步一步追踪。 - 干扰型:很多变体题会加入字母,比如
"a(b[c]d)",不过原题没有,如果想扩写,可以在代码里加字符过滤。
为什么先要讲输入特征?因为写判断逻辑前,你把所有可能形态列清楚,后面设计循环分支会轻松很多。
1.2 为什么第一直觉一定是栈
很多没有系统学过数据结构的人,第一想法是用三个计数器分别记录三种括号的数量,左括号加一,右括号减一,最后三个都归零就认为合法。这个思路用来解只有一种括号的简化版是没问题的,但对于三种括号同时存在的情况,它完全不能处理排序关。举个最直接的例子:输入是 ([)],用计数器看,左括号数量等于右括号数量,三个类型各自配对,如果只按计数器判断会误判为有效,但实际上右括号 ) 出现时,最近一个未匹配的左括号是 [,类型对不上,所以这个字符串本质上就成了括号交叉嵌套,当然非法。
栈为什么适合?因为它的特性是后进先出。括号配对本身就有对称性:最早出现的左括号往往要等最晚出现;而最后一个出现的左括号,会在下一个右括号出现时立刻被匹配。这恰恰符合栈的“弹栈顺序”。用行话说,处理字符串时,凡是遇到左括号就压栈,遇到右括号就出栈并对比“栈顶元素是否和当前右括号匹配”,如果匹配不上就直接返回错误,整个过程一次扫描就能完成。时间复杂度 O(n),空间复杂度最坏也是 O(n)。
栈解决括号匹配,几乎就是教科书级别的“数据结构与场景匹配”案例。我在实际做这种题时,第一直觉不是去想各种奇技淫巧,而是先问自己:当前这个问题的状态,天然叠了一层“后出现先匹配”的结构,那不管题目怎么包装,本质上就是在考栈。你看正则表达式里括号捕获、编译器检查括号配对、XML/HTML 标签闭合,全是一个逻辑:遇到开始符号入栈,遇到结束符号出栈。理解这道题,就相当于理解了一批工程场景。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 用栈实现的标准做法与代码拆解
2.1 算法流程和栈操作顺序
先给出一版最简单的流程,适合在纸上把所有细节推演清楚:
- 预处理:如果字符串长度是奇数,直接返回 false,因为合法成对括号总长度一定是偶数。
- 初始化一个空栈。
- 从左到右遍历字符串的每个字符
c。 - 如果
c是左括号,即{、[、(中的任意一个,就把它压入栈。 - 如果
c是右括号,分两种情况:- 如果此时栈为空,说明当前右括号没有对应的左括号,直接返回 false。
- 如果栈非空,弹出栈顶元素
top,检查top是否与c匹配。
- 字符串遍历完成后,如果栈为空,说明所有左括号都有了匹配,返回 true;如果栈里还有残留左括号,说明有些括号没闭合,返回 false。
看上去很简单,容易写挂的点其实不少。最经典的就是只记得弹栈,却不做栈空检查,代码写成 if (stack.peek() != pair) return false,一旦遇到 "]" 开头且栈为空,就抛 EmptyStackException。还有一个点是匹配表的方向写反,比如把右括号当 key、左括号当 value,然后弹栈后去查,逻辑也能成立,但前提是你查的是当前右括号,不是弹出来的栈顶,这个顺序一定要捋顺。
我这里推荐的流程是“右括号查栈顶”,意思是当前字符是右括号时,用 map 查出它期望匹配的左括号,再和栈顶的左括号对比。也有人反过来用左括号查右括号,就是把压栈动作改成压右括号,遇到右括号再弹出来对比,这两种写法最后都能过,根据习惯来即可,关键是别混。
2.2 配合哈希映射的 Java 实现
第一种是 Java 实现,配合 HashMap 存储左右括号匹配关系,代码清晰度最高,推荐阅读时使用。
java复制import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
class Solution {
public boolean isValid(String s) {
if (s.length() % 2 == 1) {
return false;
}
Map<Character, Character> pairs = new HashMap<Character, Character>() {{
put(')', '(');
put(']', '[');
put('}', '{');
}};
Deque<Character> stack = new ArrayDeque<Character>();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (pairs.containsKey(ch)) {
// 栈为空说明右括号多了,直接无效
if (stack.isEmpty() || !stack.peek().equals(pairs.get(ch))) {
return false;
}
stack.pop();
} else {
stack.push(ch);
}
}
return stack.isEmpty();
}
}
这里有几个工程习惯值得讲。栈不要用 Stack 类,Java 官方更推荐用 ArrayDeque,因为 Stack 继承自 Vector,所有方法都加了同步锁,单线程环境反而拖慢性能,而且它保留了大量不属于栈语义的方法,容易误用。Deque 接口里的 push、pop、peek 语义更纯粹,代码写出来也更贴近算法表达。
还有一个小细节是判断长度奇偶。如果不做这个判断,代码也能靠后续逻辑判断,但加一个长度检查可以省掉一半无意义的扫描。比如输入长度为 100001,不可能配对成功,直接返回 false,这算是一个微小的剪枝优化。字符串运算很便宜,大多数在线判题系统里这步带来的提升有限,但是引导自己养成“提前考虑不可能情况”的习惯,对后面写复杂算法帮助很大。
2.3 可读性更高的 Python 实现
用 Python 实现“有效的括号”,因为 Python 本身表达力强,代码会非常接近伪代码。
python复制def is_valid(s: str) -> bool:
if len(s) % 2 == 1:
return False
pairs = {
")": "(",
"]": "[",
"}": "{",
}
stack = []
for ch in s:
if ch in pairs:
if not stack or stack[-1] != pairs[ch]:
return False
stack.pop()
else:
stack.append(ch)
return not stack
Python 的 list 自带 append 和 pop,天然可以当栈用。注意不要用 stack[-1] 取栈顶后忘记比较,这里的比较有两种习惯写法:
- 写法 A:
if stack[-1] != pairs[ch],读取栈顶但不弹,判断不匹配就返回。 - 写法 B:
if stack.pop() != pairs[ch],先弹栈再比较,如果栈为空会报IndexError,所以必须先把not stack检查放在前面。
我个人更推荐写法 A,因为栈顶元素在当前循环里有多次比较的可能性,如果后续要扩展逻辑(比如记录弹出次数),不提前弹栈更灵活。
还有一种更规避 map 的写法,是用 ASCII 值匹配:( 的 ASCII 是 40,) 是 41,差 1;[ 是 91,] 是 93,差 2;{ 是 123,} 是 125,差 2。理论上可以用 ord(ch) - ord(stack[-1]) 是否落在 1 或 2 里来判断,但它依赖字符编码,可读性和可扩展性都很差,面试时也容易触发追问,不推荐。
2.4 不用哈希表:字符串替换思路与性能对比
如果你用的是 JavaScript 这类数组和字符串操作很灵活的语言,有人会采用“循环消除相邻括号对”的做法:不断把 "()"、"[]"、"{}" 从字符串里删掉,如果最后剩下空字符串,说明有效。代码长这样:
javascript复制function isValid(s) {
let stack = [];
const map = {
')': '(',
']': '[',
'}': '{'
};
for (let ch of s) {
if (map[ch]) {
if (stack[stack.length - 1] !== map[ch]) return false;
stack.pop();
} else {
stack.push(ch);
}
}
return stack.length === 0;
}
另一种属于“现象级写法”的做法是直接在字符串上做替换:
javascript复制function isValid(s) {
while (s.includes('()') || s.includes('[]') || s.includes('{}')) {
s = s.replace('()', '').replace('[]', '').replace('{}', '');
}
return s === '';
}
这种写法看起来很短,实际运行效率非常差。每一次 replace 都需要扫描整个字符串,假设最坏情况是 ((((....)))),每轮只能消除最里面的一对,总共要循环 n/2 次,每次扫描 O(n),复杂度退化到 O(n²),面对超长字符串会直接超时。而且 replace 在 JavaScript 中默认只替换第一个匹配项,需要连续拼接多次才能删除三种括号,逻辑繁琐,边界容易漏。所以“字符串替换法”只适合当脑洞看看,真写代码入栈法才是正解。
3. 从该题延伸出的各类变式与常见误区
3.1 提前剪枝:为什么奇数长度可以直接否定
很多初学者会忽略预判这一环。拿一个长度为 100001 的字符串来举例,不管内容是什么,要配对成功,括号必须成双出现,所以总长度一定是偶数。这个性质用到题里,可以在遍历开始前就筛掉一半的输入,连栈都不用初始化。虽然现代语言里字符串取长度是 O(1) 操作,字符串处理本身后续扫描是 O(n),剪枝对这个题的整体复杂度没有性质上的改变,但它能培养一个重要的习惯:在着手进行主逻辑前,先想一想问题空间有哪些不可能的集合,提前排除,能节省后续不必要的计算。
这类预判在工程中同理。做输入校验时,如果用户传入的参数为空、类型不对、长度超限,很多校验框架都建议在业务核心代码之前直接拒绝,而不是等核心逻辑执行到一半再抛异常。你会发现“有效的括号”的奇数判断跟接口层的白名单校验非常像——将不必要的计算挡住,在算法题中的应用就是剪枝,而在系统设计中的应用就是 fail-fast。
3.2 最容易被忽视的“栈空检查”
代码写多了以后会形成肌肉记忆:遇到右括号就弹栈比对,但很多人会忘记一种情况,字符串开头就是右括号,比如 ")("。此时栈是空的,如果直接弹栈,Java 的 ArrayDeque 会抛异常,Python 的列表越界会报 IndexError,JavaScript 则返回 undefined,比较时的表现是 undefined !== '[' 为 true,于是误判 false。恰巧在这道题里误判不会被测试发现,因为 ")(" 本来就是 false,但如果输入是 "])" 后面还有合法括号,有些错误写法因为提前 return false 反而结果正确,导致你根本没察觉栈空检查缺失的隐患。
真正会在某次测试中暴露的问题是另一种情况:栈空时却想通过匹配 map 继续查。比如错误代码写成 if (pairs.get(ch) != stack.peek()),在 Java 里空栈直接异常,这类测试用例会直接报运行时错误。所以标准写法必须把 stack.isEmpty() 放在前面,用短路或逻辑阻止后续取栈顶。
我自己在实际做题时,甚至会刻意在每次弹栈前都写一个哨兵注释,提醒自己“这里可能有空栈”。时间久了,这种对边界条件的敏感会平移到真实系统开发中,比如处理消息队列里不完整的数据包,处理 XML 文档里缺失的闭合标签,处理 JSON 解析时报错时栈的状态。
3.3 只用计数器判断为什么出错
很多初学选手想这么解:统计三个左括号类型的数量和三个右括号的数量,如果对应类型数量相等,再确保遍历过程里右括号数量不超过左括号总量,认为就可以通过了。对单一种类括号而言,这个思路确实正确,因为只看开闭合计数就能判断。但多类型问题,计数器丢失了“先后顺序”和“相邻层级”信息。
用 ([)] 字符串推演一遍:
- 遇到
(,左圆括号计数 1。 - 遇到
[,左方括号计数 1。 - 遇到
),它是右圆括号,对应左圆括号计数 1,减去后归 0。 - 遇到
],它是右方括号,对应左方括号计数 1,减去后归 0。 - 最终计数都归零,计数器解法返回 true。
实际上 ([)] 是错的:右圆括号 ) 出现时,最近未闭合的左括号是 [,不是 (。圆形括号和方括号交叉嵌套了,“对齐规则”被破坏。计算机要识别这种错误,必须随时记住“最近一个未闭合的左括号是什么”,而不能只知道“还有哪些括号没闭合”。只有后进先出的栈能做到,计数器根本无法维护顺序状态。
一句话总结这个误区:计数只能表达“数量平衡”,不能表达“结构正确”。而括号匹配的题眼恰恰是结构正确。想想 DNA 碱基配对或化学方程式,任何需要层级合法的符号串校验,几乎都是栈,这也是为什么“有效的括号”被放进所有数据结构教材的栈章节。
3.4 类型区分与扩展:包含自定义符号怎么处理
基于栈的解法有一个天然优点:支持任意成对符号,只需扩展匹配表即可。比如待校验的是 < 和 >,或者自定义的 BEGIN、END 文本标签,你只要把它加进 map,后面的逻辑完全不用变。
我在做企业级应用时遇到过一个需求,要校验一段模板语言里的标签是否闭合,比如模板里有 [% if %]、[% endif %]。这类标签就不是单字符了,但思路完全可以从括号题复用:遇到开始标签,整个字符串入栈;遇到结束标签,弹出栈顶,比对标签名是否一致。这时原题里“字符必须相等”的判断就扩展成“标签名必须相等”。
再举一个常见面试延伸:有些题会带上通配符,比如字符串里允许出现 *,* 可以当作 (、) 或空字符,问最终是否有效。这种情况下单栈就不够用了,得维护左括号栈和星号栈两个栈。虽然这一步听起来复杂,但如果你先把原始版本的“有效的括号”理解得非常深刻,你自然就知道为什么要保留左括号索引的栈,而不是简单的计数——所有复杂的扩展题都建立在基础栈操作之上。
4. 实操过程:从读题到提交的完整复盘
4.1 在 IDE 中构建最小可运行 Demo
很多刷题新手有个问题,直接在在线编辑器里写函数,缺少本地的调试能力,一旦报错只能靠肉眼看代码,特别痛苦。我的建议是先在本地 IDE 里构建一个最小的可运行版本,让自己可以通过打印、断点、观察栈的变化来理解整个流程。以 Java 为例,我会建一个本地 Main 类,加上 main 方法跑几个测试用例:
java复制public class Main {
public static void main(String[] args) {
Solution solution = new Solution();
String[] cases = {
"()", "()[]{}", "(]", "([)]", "{[]}", "", ")", "((((",
"(([]){})", "(((())))", "[({})](]"
};
for (String test : cases) {
System.out.println(test + " => " + solution.isValid(test));
}
}
}
打印结果时注意观察哪些用例和预期不符。如果 ([)] 被错误判为 true,说明你没有维护栈序;如果 ")" 爆异常,说明少了栈空检查;如果 "(((( " 返回 true,说明最后少了栈空判断,残留左括号没处理。这个过程逼着你去验证自己的算法在每一步状态是否正确。调试工具里观察栈的内容也很直观:遇到左括号后,栈里会累积一串字符;遇到配对的右括号,栈顶会被弹出,栈体积收缩,这种“涨落”你见过一次,就永远理解了栈为什么适合这个场景。
4.2 分步骤推演一个复杂用例
拿 "(([]){})" 举例。这个字符串是合法嵌套,可以串成三层。
第1步:读入第一个字符 (,它是左括号,入栈。栈内容 ["("]。
第2步:读入 (,入栈。栈内容 ["(", "("]。
第3步:读入 [,入栈。栈内容 ["(", "(", "["]。
第4步:读入 ],它是右括号,栈非空,弹出栈顶 [,与 ] 配对,匹配,continue。栈内容 ["(", "("]。
第5步:读入 ),右括号,弹出栈顶 (,配对成功。栈内容 ["("]。
第6步:读入 {,入栈。栈内容 ["(", "{"]。
第7步:读入 },右括号,弹出栈顶 {,配对成功。栈内容 ["("]。
第8步:读入 ),右括号,弹出栈顶 (,配对成功。栈为空。
字符串遍历完成,栈为空,返回 true。
手推这八步的作用,是让你真正理解“栈顶永远对应最新未闭合左括号”。如果第4步读到的不是 ] 而是 ),那么弹出的栈顶就是 [,类型不匹配,可以立刻判断整个字符串无效,不需要再往后看。这就是为什么这个算法只需要一次遍历——很多非法状态其实在第一次越界时就暴露了。
4.3 复杂度分析的另一种推算方式
推算复杂度时不要只背结论。先看循环次数,程序对每个字符做了常量次数的操作,因此时间复杂度为 O(n),n 是字符串长度。再看空间占用,栈中最多保存所有左括号,极端情况如 "((((((((((..." 长度为 n,左括号就有 n 个,栈的空间占用 O(n)。如果你从“每个字符都可能入栈”的角度理解,就不会只记结论而不知道为什么。
有一种空间优化方法可以应用,注意到合法输入中,左括号和右括号的数量不会相差太多。但最坏情况满左括号时栈仍要 O(n),没有任何办法在保持功能不变的情况下降低空间复杂度。所以这道题的标准答案就是时间 O(n)、空间 O(n)。它比很多 O(n log n) 的算法都简单,是初学者体会复杂度分析的好素材。
4.4 用表格快速对照不同写法的优劣
| 实现方式 | 时间复杂度 | 空间复杂度 | 可读性 | 典型坑 |
|---|---|---|---|---|
| 栈 + 哈希表 | O(n) | O(n) | 高,推荐 | 栈空检查、方向写反 |
| 栈 + if-else 分支 | O(n) | O(n) | 中 | 多个分支容易漏匹配 |
| 字符串替换消除 | O(n²) | O(n) | 低,不推荐 | 超时、替换不彻底 |
| 三个计数器 | O(n) | O(1) | 低(看似高) | 无法判断交叉顺序,误判 |
实际项目如果需要性能,栈加哈希表是主流。if-else 分支在一些内存极受限的环境(比如嵌入式)也许能省掉 map 的开销,但代码冗长,而且括号类型一多就会非常难看。字符串替换法只适合字符串长度很短的场景,比如文字编辑器校验用户输入,如果明确限制长度在 20 以内,那可以用,但通用解法不建议。
5. 常见问题与排查技巧实录
5.1 符号匹配方向写反
具体表现是对 "()" 也返回 false。看代码时你会发现 map 里写的是 ( -> ),右括号时想要用当前字符查对应的左括号,结果查出来是右括号,就拿右括号跟栈顶左括号比较,必然不相等。解决办法是把 map 的键设为右括号,值设为左括号;或者在遇到右括号时用 stack.pop() 出的左括号去查左括号到右括号的 map。两种方式二选一,不要穿插用。
我自己在纸上设计阶段一般把映射表写成“右括号 -> 左括号”的形式,因为代码里的触发条件本来就是“遇到右括号”,用触发条件的字符做 key 最自然。如果你不确定,可以在代码里加一行注释,把括号方向写清楚,防止过几天再看时绕进去。
5.2 ArrayDeque 不允许放入 null
如果你在实现时用 ArrayDeque<Character>,并且尝试 stack.push(null),Java 会直接抛 NullPointerException,因为 ArrayDeque 内部不允许 null 元素。一般刷题输入只含括号字符,不会遇到这个情况,但如果你扩展逻辑,遇到 null 字符时直接 push,那就有坑。最简单的规避是判断字符非空后再 push,或者在题目前提下默认不会出现 null。
另一个细节是 stack.pop() 和 stack.poll() 的区别。pop() 在栈为空时会抛异常,poll() 在栈为空时返回 null,如果用 poll() 就不用检查空栈,但之后拿 null 和非空字符比较时结果可控,逻辑等效于发现不匹配。不过官方题解普遍用 pop() 加空栈检查,可读性更好,也更能显式表达“栈为空即非法”这一分支。
5.3 遍历结束后忘记判断栈空
如果输入是 "(((",遍历完三个左括号后没有右括号可匹配,代码如果只判断遍历中是否出现错误,那就会返回 true。可实际上三个左括号都没有闭合。这个 bug 的隐蔽程度取决于测试用例覆盖。审查代码时,我会在函数末尾特意写一行 return stack.isEmpty(),并且把它的语义理解为“所有的左括号都必须被消费干净”。在同事做代码评审时,这也是一个非常常见的 review 点:遍历完成不代表匹配完成,栈是残留状态的第一现场。
5.4 使用 HashMap 的 get 与 containsKey 的区别
实际开发中有人喜欢这样写:if (pairs.get(ch) != null),用返回值判断 key 是否存在。问题是 map 的 value 可能本身是 null,但在这里 value 是 Character,不会是 null,所以这种写法勉强可行。可读性仍然不如 containsKey(ch),也会让读代码的人疑惑。更重要的是,如果后来你把 value 改成可空类型,这处判断就会意外失效。所以刷题时也要养成分层的好习惯:判断是否命中用 containsKey,取值用 get,职责清晰。
5.5 调试建议和打印技巧
建议关键时刻打印三个内容:当前读取的字符、当前栈内容、当前操作类型。我有一段调试简版:
java复制System.out.println("ch=" + ch + ", stack=" + stack + ", action=" + (pairs.containsKey(ch) ? "pop" : "push"));
把这段输出对应到手推的每一步,你可以直观看到栈的变化是否和预期一致。当输出流很长时,也可以缩减成只打印栈的 size,比如始终 System.out.println(stack.size()),这样能快速定位是在某个右括号阶段报错还是最后残留阶段报错。
6. 做题之外:此题背后的工程启示与学习路径
6.1 真实开发中的括号匹配场景
“有效的括号”并不是纯粹的面试八股。编译器词法分析阶段就要识别括号是否匹配,比如你写 if (x > 0 { 编译器会基于栈机制给出 “missing ‘)’” 的提示。IDE 在你输入右括号时高亮匹配的左括号,用的就是栈顶回溯。JSON 解析器和 XML 解析器要校验结构闭合,类似逻辑随处可见。你只是把字符串里的 [] 换成 HTML 标签的 </div>,就是一个小型“标签合法性校验器”。
既然真实系统中有这么多应用,那日常练习的价值就不只是在 LeetCode 上增加通过数,而是训练一种“把结构嵌套问题抽象成栈”的直觉。一旦遇到新的校验需求,可以直接在脑内匹配模式:判断一个栈是否能支撑语义上的最近匹配。
6.2 每日一练的学习方法:从一题挖一类题
“每日一练”的核心不在于一天做多少新题,而在于用一道题牵引出一类知识。做完“有效的括号”后,建议马上补齐下面几个方向:
- 栈的经典应用:后缀表达式求值、中缀转后缀、浏览器前进后退、函数调用栈。
- 括号相关变体题:最长有效括号子串、删除无效括号、括号生成、标签闭合校验。
- 单调栈启动:下一个更大元素、柱状图中最大矩形,这些是栈中另一个分支。
建议把 LeetCode 题号为 20 的原题做透后,在笔记里记录“为什么用栈”,然后带着这类疑问去做 32 题(最长有效括号)和 22 题(括号生成)。两道题做完,你对括号结构的“层级状态”“最近匹配”理解会立体很多。
学习路径上不用贪多。单个知识点练深,比一天快速刷十道同质题有效。“每日一练”的意义在于持续调用大脑的模式识别系统,让“看到有效括号题就条件反射地想栈”成为一种直觉。坚持三个月后,你会发现读题时的第一反应从“这是什么语法”变成“这题的状态维护用什么结构”,这个转变本身就是训练目标。
6.3 如果为这道题写测试用例,应该覆盖哪些
在工程代码里撸算法,只有能写单元测试才有真实价值。我会建议至少用参数化测试覆盖这些用例:
| 用例 | 预期结果 | 设计目的 |
|---|---|---|
"" |
true | 空字符串边界 |
"()" |
true | 最小有效对 |
"()[]{}" |
true | 多类型并列 |
"(]" |
false | 类型不匹配 |
"([)]" |
false | 顺序交叉错误 |
"{[]}" |
true | 多层嵌套正确 |
"(" |
false | 只有左括号 |
")" |
false | 只有右括号 |
"(((" |
false | 未闭合残留 |
"(([]){})" |
true | 复杂混合嵌套 |
"((" + "))" |
true | 长嵌套压栈稳定 |
这些用例覆盖了空输入、最短有效、并列、类型错、交叉错、嵌套、左右单边缺失、遍历后残留、复杂混合、长压栈等多个维度。把这些测试用例固定好,跑一次代码,通过不代表思维完满,但至少说明代码的常用边界都覆盖到了。
6.4 关于代码整洁度的小建议
刷题代码往往只追求通过,但如果你在训练时能把代码写得整洁,进入实际项目后会少走很多弯路。函数的命名用 isValid,不要写 check;栈变量名用 stack 而不是 stk;map 变量名用 pairs 而不是 map,因为你可能后面还有别的 map;条件判断统一用“正面优先”原则,主逻辑处理正常分支,异常分支尽快返回。这些看似的细节在代码评审中被反复提到,早一点养成,写“每日一练”帖子时才更有沉淀价值。
还有人会问:要不要为解题写注释?我的建议是给有转折逻辑的地方加注释,比如“判断右括号时,最近匹配应是栈顶的左括号”“遍历结束后栈必须为空”。不要把思路解释写得像教科书,只注释“为什么”而不注释“是什么”。这样代码既能自己看懂,也能在复盘时快速唤醒当时的思考过程。即便在 LeetCode 上只贴一个函数,没有上下文,这种注释也能让其他读者不误解你的实现思路。
做题这件事,最忌只看别人的答案不自己手动推演。我后来训练新人时,都会让他们先不碰编辑器,手写遍历步骤,把一个复杂字符串的栈变化全部写在纸上,再打开代码验证。写过一遍状态表之后,这个题的理解深度会远超看十遍题解。希望你从这道“每日一练”开始,也拿张纸,把 "(([]){})" 的入栈、弹栈过程画一遍,再回来敲代码。栈这种结构本身虽然简单,但能用它对真实嵌套信息建模,才是在这道题里真正需要巩固的能力。
