一、题目描述
给定一个只包括 '(',')','{','}','','' 的字符串 s,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合
- 左括号必须以正确的顺序闭合
- 每个右括号都有一个对应的相同类型的左括号
示例:
| 示例 | 输入 | 输出 |
|---|---|---|
| 示例1 | s = "()" | true |
| 示例2 | s = "()\[\]{}" | true |
| 示例3 | s = "(]" | false |
| 示例4 | s = "(\[\])" | true |
| 示例5 | s = "()" | false |
提示:
- 1 <= s.length <= 10^4
- s 仅由括号 '()\[\]{}' 组成
二、解题思路总览
拿到这道题,首先思考:什么时候括号是"有效的"?
想象一下括号匹配的过程:
- 遇到左括号 → 它期待一个配对的右括号
- 遇到右括号 → 必须和最近的那个左括号配对
这正是 栈(Stack) 的经典应用场景:后进先出(LIFO),最近未匹配的那个左括号,恰好应该和当前遇到的右括号配对。
核心思路:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 遍历字符串s的每个字符 | 从左到右依次处理 |
| 2 | 如果是左括号 '(' '[' '{' | 入栈,等待配对 |
| 3 | 如果是右括号 ')' ']' '}' | 栈顶元素出栈并检查是否匹配 |
| 4 | 遍历结束 | 栈为空 → true;栈非空 → false |
为什么用栈?
| 对比 | 不用栈(错误思路) | 用栈(正确思路) |
|---|---|---|
| 匹配方式 | 盲目查找相同字符 | 只看最近的一个左括号 |
| 顺序要求 | 无法保证"正确的顺序" | 栈天然保证LIFO,匹配顺序正确 |
| 时间复杂度 | O(n^2) 或更差 | O(n) |
三、完整代码(方法一:栈 + STL)
cpp
class Solution {
public:
bool isValid(string s) {
int n = s.size();
// 奇数长度不可能配对成功
if (n % 2) return false;
stack<char> st;
for (int i = 0; i < n; i++) {
// 左括号:入栈
if (s[i] == '(' || s[i] == '[' || s[i] == '{') {
st.push(s[i]);
} else {
// 右括号:栈空则匹配失败
if (st.empty()) return false;
char top = st.top();
// 检查栈顶左括号是否与当前右括号配对
if ((s[i] == ')' && top == '(') ||
(s[i] == ']' && top == '[') ||
(s[i] == '}' && top == '{')) {
st.pop(); // 配对成功,出栈
} else {
return false; // 配对失败
}
}
}
// 栈空说明全部配对成功
return st.empty() ? true : false;
}
};
四、其他解法
方法二:数组模拟栈(手动实现,消除STL开销)
cpp
class Solution {
public:
bool isValid(string s) {
int n = s.size();
if (n % 2) return false;
// 用数组模拟栈,手动管理指针
char st[n];
int top = 0; // 栈顶指针,指向下一个待写入位置
for (int i = 0; i < n; i++) {
if (s[i] == '(' || s[i] == '[' || s[i] == '{') {
st[top++] = s[i]; // 入栈
} else {
if (top == 0) return false; // 栈空
char c = st[--top]; // 出栈
if ((s[i] == ')' && c != '(') ||
(s[i] == ']' && c != '[') ||
(s[i] == '}' && c != '{')) {
return false;
}
}
}
return top == 0;
}
};
改进点: 用数组替代 std::stack,避免STL的函数调用开销和额外的内存分配。
方法三:栈 + HashMap 配对(代码更简洁)
cpp
class Solution {
public:
bool isValid(string s) {
int n = s.size();
if (n % 2) return false;
stack<char> st;
// 用哈希表存储配对关系,代码更清晰
unordered_map<char, char> pair = {
{')', '('},
{']', '['},
{'}', '{'}
};
for (char c : s) {
if (pair.count(c)) {
// c 是右括号,检查栈顶是否匹配
if (st.empty() || st.top() != pair[c]) {
return false;
}
st.pop();
} else {
// c 是左括号,入栈
st.push(c);
}
}
return st.empty();
}
};
改进点: 用 unordered_map 替代硬编码的 if-else,配对关系一目了然,扩展性更好(比如增加其他括号类型)。
方法四:计数法(仅适用于相邻括号配对)
cpp
class Solution {
public:
bool isValid(string s) {
int n = s.size();
if (n % 2) return false;
// 统计左括号数量,用于剪枝
int cnt_p = 0, cnt_s = 0, cnt_c = 0;
for (char c : s) {
if (c == '(') cnt_p++;
else if (c == '[') cnt_s++;
else if (c == '{') cnt_c++;
else if (c == ')') cnt_p--;
else if (c == ']') cnt_s--;
else if (c == '}') cnt_c--;
// 剪枝:任意计数器为负,说明右括号多于左括号
if (cnt_p < 0 || cnt_s < 0 || cnt_c < 0) return false;
}
return cnt_p == 0 && cnt_s == 0 && cnt_c == 0;
}
};
适用范围: 此方法只能检测"相邻括号配对"(如 "()[]{}"),无法处理嵌套和交叉 (如 "([])" 正确返回 true,但 "(]" 和 "([)]" 也会返回 true)。仅作思路拓展,生产代码不推荐。
方法五:字符串替换(直观但低效)
cpp
class Solution {
public:
bool isValid(string s) {
int n = s.size();
if (n % 2) return false;
// 不断删除相邻的有效括号对,直到无法删除
while (s.find("()") != string::npos ||
s.find("[]") != string::npos ||
s.find("{}") != string::npos) {
s.erase(s.find("()"), 2);
s.erase(s.find("[]"), 2);
s.erase(s.find("{}"), 2);
}
return s.empty();
}
};
问题: find() 是 O(n),erase() 是 O(n),整体时间复杂度退化到 O(n^3) ,不可用于生产环境。仅作思路展示。
五、算法流程图
以输入 s = "([])" 为例,逐步展示算法执行过程:
Step 0: 初始状态
s = "( [] )"
stack: [空]
Step 1: 遇到 '(',左括号入栈
s = "( [] )"
^
stack: [ ( ]
Step 2: 遇到 '[',左括号入栈
s = "( [] )"
^
stack: [ ( , [ ]
Step 3: 遇到 ']',右括号
- 栈顶是 '[',匹配成功
- pop
s = "( [] )"
^
stack: [ ( ]
Step 4: 遇到 ')',右括号
- 栈顶是 '(',匹配成功
- pop
s = "( [] )"
^
stack: [空]
Step 5: 遍历结束
stack为空 → 返回 true
再来看一个失败案例 s = "([)]":
Step 0: 初始状态
s = "( [ ) ]"
stack: [空]
Step 1: 遇到 '(',入栈
s = "( [ ) ]"
^
stack: [ ( ]
Step 2: 遇到 '[',入栈
s = "( [ ) ]"
^
stack: [ ( , [ ]
Step 3: 遇到 ')',右括号
- 栈顶是 '[',但 ')' 应该匹配 '('
- 不匹配!返回 false
s = "( [ ) ]"
^
stack: [ ( , [ ]
最终结果: false
六、逐行解析(方法一)
cpp
int n = s.size();
if (n % 2) return false;
剪枝优化: 如果字符串长度是奇数,必然有未配对的括号,直接返回 false。这一步看似简单,但可以跳过大量无效计算。
cpp
stack<char> st;
数据结构选择: 使用 std::stack,只关心栈顶元素和入栈/出栈操作。
cpp
for (int i = 0; i < n; i++) {
if (s[i] == '(' || s[i] == '[' || s[i] == '{') {
st.push(s[i]);
}
左括号处理: 直接入栈,等待之后可能的配对。
cpp
} else {
if (st.empty()) return false;
右括号处理的第一步: 如果栈已经空了,说明没有对应的左括号,直接失败。
cpp
char top = st.top();
if ((s[i] == ')' && top == '(') ||
(s[i] == ']' && top == '[') ||
(s[i] == '}' && top == '{')) {
st.pop();
} else {
return false;
}
}
}
配对检查: 核心逻辑。当前右括号必须与栈顶的左括号类型一致,才算配对成功。
cpp
return st.empty() ? true : false;
最终判断: 遍历结束后,栈空说明所有左括号都成功配对;栈非空说明有左括号没被匹配。
七、复杂度分析
各方法复杂度对比
| 方法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 方法一:STL栈 | O(n) | O(n) | 标准解法,稳定高效 |
| 方法二:数组模拟栈 | O(n) | O(n) | 省去STL开销,极致性能 |
| 方法三:栈+HashMap | O(n) | O(n) | 代码简洁,配对关系清晰 |
| 方法四:计数法 | O(n) | O(1) | 错误解法,仅适用于相邻配对 |
| 方法五:字符串替换 | O(n^3) | O(n) | 不可用,复杂度太高 |
方法一详细复杂度分析
时间复杂度:O(n)
- 只需遍历字符串一次
- 每个字符最多入栈一次、出栈一次
空间复杂度:O(n)
- 最坏情况:全部是左括号,如 "((((("
- 栈中最多存放 n/2 个元素(严谨分析),但渐进表示为 O(n)
补充说明:
| 情况 | 栈中元素数量 | 说明 |
|---|---|---|
| 全部左括号 | O(n) | "(((((" |
| 全部右括号 | O(1) | 第一步就返回false |
| 交替出现 | O(1) | "()()()" 每次配对后栈即空 |
八、面试追问 FAQ
| 问题 | 回答要点 |
|---|---|
| Q1: 为什么不使用队列? | 队列是FIFO(先进先出),而括号匹配需要LIFO。最近的左括号才应该被匹配,队列无法提供这个能力。 |
| Q2: 可以不用栈吗? | 理论上可以用递归模拟栈,但会增加函数调用开销,且有栈溢出风险。不推荐。 |
| Q3: 如果要返回具体错误信息怎么办? | 可以在返回false时增加额外逻辑:记录是"多余右括号"、"多余左括号"还是"类型不匹配"。 |
| Q4: 如何处理带优先级的表达式求值? | 这是另一个经典问题,通常用两个栈:一个存操作数,一个存操作符。括号匹配只是其中一步。 |
| Q5: 如果字符串很长(10^6级别)怎么办? | 当前算法O(n)已经最优,主要瓶颈可能在I/O。可以用内存映射或流式处理,但核心算法不变。 |
| Q6: 三种栈的实现哪个更好? | 生产环境优先用方法一(STL栈),代码可读性高;如果在竞赛或极端性能场景,用方法二(数组模拟);方法三适合需要频繁扩展配对规则的场景。 |
九、相关题目
| 题号 | 题目 | 难度 | 关联点 |
|---|---|---|---|
| 32 | 最长有效括号 | 困难 | 栈的延伸应用 |
| 921 | 使括号有效的最少添加 | 中等 | 栈的变形 |
| 1021 | 删除最外层的括号 | 简单 | 栈的变形 |
| 1541 | 平衡括号的最少插入 | 中等 | 栈的变形 |
推荐刷题顺序:
- 本题(20.有效的括号)→ 基础
- 921(使括号有效的最少添加)→ 变形
- 1021(删除最外层的括号)→ 变形
- 32(最长有效括号)→ 综合
十、总结
| 维度 | 内容 |
|---|---|
| 考察知识点 | 栈(Stack)的应用 |
| 难度 | 简单 |
| 代码框架 | 遍历 + 栈操作 |
| 关键技巧 | 利用栈的LIFO特性,保证匹配顺序 |
| 边界情况 | 奇数长度、栈空、右括号不匹配 |
| 变形题 | 返回最小插入数、最长有效长度 |
| 推荐解法 | 方法一(STL栈)或方法二(数组模拟) |
一句话总结: 括号匹配是栈的入门经典题,核心就是"最近未匹配的左括号 = 当前右括号应该匹配的对象",用栈的LIFO特性天然实现这个逻辑。生产代码推荐使用 方法一(STL栈),代码可读性高且性能足够。