📌 题目链接:20. 有效的括号 - 力扣(LeetCode)
🔍 难度:简单 | 🏷️ 标签:栈、字符串、哈希表
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(n)
🔍 题目分析
本题要求判断一个仅由 '(', ')', '{', '}', '[', ']' 组成的字符串是否为有效括号字符串。有效性需满足两个核心条件:
- 类型匹配:每个左括号必须有相同类型的右括号闭合;
- 顺序正确:括号必须按“最近打开、最先关闭”的原则闭合(即 LIFO 原则)。
例如:
"()"✅"()[]{}"✅"([)]"❌(虽然数量匹配,但顺序错误)"((("❌(未闭合)
💡 关键洞察:这种“后进先出”的匹配特性,天然适合用 栈(Stack) 来处理。
此外,题目隐含一个快速剪枝条件:有效括号字符串长度必为偶数。若输入长度为奇数,可直接返回 false,无需后续处理。
🧠 核心算法及代码讲解
✅ 算法思想:栈 + 哈希映射
我们使用 栈 来暂存遇到的左括号,并借助 哈希表 快速判断右括号是否与栈顶左括号匹配。
步骤详解(C++ 视角):
-
预判奇数长度:若
s.length()为奇数,直接返回false。 -
构建映射关系:建立右括号 → 左括号的映射(如
')' → '('),便于快速查找。 -
遍历字符串:
-
若当前字符是 左括号(不在哈希表中),压入栈;
-
若是 右括号(在哈希表中):
- 检查栈是否为空(无左括号可匹配)→ 无效;
- 检查栈顶是否等于该右括号对应的左括号 → 不等则无效;
- 匹配成功,弹出栈顶。
-
-
最终检查:遍历结束后,栈必须为空,否则存在未闭合的左括号。
📜 C++ 核心代码(带行注释):
bool isValid(string s) {
int n = s.size();
// 🚩 剪枝:奇数长度不可能有效
if (n % 2 == 1) return false;
// 🔑 哈希表:右括号 -> 对应左括号
unordered_map<char, char> pairs = {
{')', '('},
{']', '['},
{'}', '{'}
};
stack<char> stk; // 📦 用于存储左括号
for (char ch : s) {
// 🔍 如果是右括号(存在于哈希表中)
if (pairs.count(ch)) {
// ❌ 栈空 或 栈顶不匹配 → 无效
if (stk.empty() || stk.top() != pairs[ch]) {
return false;
}
stk.pop(); // ✅ 匹配成功,弹出
} else {
// 📥 左括号,直接入栈
stk.push(ch);
}
}
// 🎯 最终栈必须为空
return stk.empty();
}
💡 为什么用“右→左”映射?
因为我们遇到右括号时,需要知道它“期待”的左括号是什么。这样设计逻辑更自然,避免对每个字符做多重 if 判断。
🧩 解题思路(分步拆解)
-
Step 1:边界处理
- 字符串为空?✅ 有效(题目允许,且符合定义)。
- 长度为奇数?❌ 直接返回
false。
-
Step 2:初始化数据结构
- 创建
unordered_map存储括号对; - 创建
stack存储待匹配的左括号。
- 创建
-
Step 3:逐字符扫描
-
对每个字符
ch:-
若
ch是右括号(通过pairs.count(ch)判断):- 检查栈是否非空且栈顶 ==
pairs[ch]; - 否则立即返回
false。
- 检查栈是否非空且栈顶 ==
-
否则(左括号):压栈。
-
-
-
Step 4:最终验证
- 所有字符处理完后,栈必须为空,否则有未闭合括号。
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(n):每个字符最多入栈、出栈一次,哈希查找 O(1)。 |
| 空间复杂度 | O(n + |
| 面试考点 | • 栈的典型应用场景 • 哈希表优化查找 • 边界条件处理(奇数长度、空串) • 代码鲁棒性 |
💬 面试加分点:
- 主动提出“奇数长度剪枝”;
- 解释为何用“右→左”而非“左→右”映射;
- 讨论其他解法(如计数器?不行!因为顺序重要,如
"([)]"会误判)。
💻 代码
✅ C++ 完整代码(严格遵循模板)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
bool isValid(string s) {
int n = s.size();
if (n % 2 == 1) return false;
unordered_map<char, char> pairs = {
{')', '('},
{']', '['},
{'}', '{'}
};
stack<char> stk;
for (char ch : s) {
if (pairs.count(ch)) {
if (stk.empty() || stk.top() != pairs[ch]) {
return false;
}
stk.pop();
} else {
stk.push(ch);
}
}
return stk.empty();
}
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
// 🧪 测试用例
cout << boolalpha;
cout << isValid("()") << "\n"; // true
cout << isValid("()[]{}") << "\n"; // true
cout << isValid("(]") << "\n"; // false
cout << isValid("([])") << "\n"; // true
cout << isValid("([)]") << "\n"; // false
cout << isValid("{}") << "\n"; // true
cout << isValid("") << "\n"; // true
cout << isValid("(((") << "\n"; // false
return 0;
}
✅ JavaScript 完整代码
/**
* @param {string} s
* @return {boolean}
*/
const leftToRight = {
'(': ')',
'[': ']',
'{': '}'
};
const isValid = function(s) {
if (!s) return true;
const stack = []; // 栈
const len = s.length; // 缓存长度
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;
// 或者 return stack.length === 0;
};
🔁 JS 与 C++ 思路对比:
- JS 版使用 左→右 映射,将期望的右括号压栈,匹配时直接比较
pop() === ch;- C++ 版使用 右→左 映射,匹配时比较
top() === pairs[ch];
两种方式等价,各有风格偏好。
🌟 结语 & 下期预告
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!