有效的括号
给定一个只包括 '(',')','{','}','[',']' 的字符串 s。
判断字符串是否有效。
有效字符串需要满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 1:
输入:s = "()"
输出:true
示例 2:
输入:s = "()[]{}"
输出:true
示例 3:
输入:s = "(]"
输出:false
示例 4:
输入:s = "([])"
输出:true
示例 5:
输入:s = "([)]"
输出:false
提示:
1 <= s.length <= 10^4s仅由括号'()[]{}'组成
栈:
class Solution {
public:
bool isValid(string s) {
stack<char> st;
for (char c : s)
{
if (c == '(' || c == '[' || c == '{')
{
st.push(c);
}
else
{
if (st.empty())
{
return false;
}
char top = st.top();
st.pop();
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{'))
{
return false;
}
}
}
return st.empty();
}
};
核心思想
这题要求括号不仅类型匹配,还必须按照正确顺序闭合。
最直接的想法是从左到右扫描字符串。
遇到左括号时,它暂时还没有被匹配,需要先保存起来。
遇到右括号时,它必须匹配最近一个还没有闭合的左括号。
这正好符合栈的特点:
后出现的左括号,必须先被匹配。
也就是“后进先出”。
因此使用栈保存还没有匹配的左括号。
每遇到一个右括号,就检查栈顶左括号是否和它是同一类型。
栈中存什么
栈中只保存还没有匹配的左括号:
stack<char> st;
遇到左括号:
st.push(c);
表示当前左括号等待之后的右括号来闭合。
遇到右括号时,需要拿栈顶元素来匹配。
因为栈顶就是最近出现、还没有被匹配的左括号。
匹配逻辑
当遇到右括号时,先判断栈是否为空。
如果栈为空,说明前面没有任何左括号可以和当前右括号匹配:
if (st.empty())
{
return false;
}
如果栈不为空,就取出栈顶左括号:
char top = st.top();
st.pop();
然后判断类型是否匹配:
(c == ')' && top != '(')
(c == ']' && top != '[')
(c == '}' && top != '{')
只要有一种不匹配,就说明字符串无效,返回 false。
如果匹配成功,就继续扫描后面的字符。
为什么最后要判断栈为空
扫描结束后,如果栈为空,说明所有左括号都已经被匹配。
如果栈不为空,说明还有左括号没有右括号闭合。
例如:
s = "(()"
扫描结束后,栈中还剩一个 '('。
所以这个字符串不是有效括号字符串。
因此最终返回:
return st.empty();
为什么不能只统计数量
只统计每种括号的数量是不够的。
例如:
s = "([)]"
它包含:
- 一个
'('和一个')' - 一个
'['和一个']'
数量看起来都匹配。
但是闭合顺序不对。
'[' 是后出现的左括号,应该先用 ']' 闭合。
实际字符串却先出现了 ')',所以无效。
栈可以准确检查这种“最近左括号必须最先闭合”的顺序要求。
边界情况
如果字符串长度是 1,它只可能是一个单独的左括号或右括号,无法形成有效字符串,算法会返回 false。
如果字符串一开始就是右括号,例如:
s = ")"
此时栈为空,直接返回 false。
如果字符串全是左括号,扫描过程中不会失败,但最后栈不为空,因此返回 false。
如果所有括号都能按正确类型和正确顺序匹配,最后栈一定为空,返回 true。
正确性证明
我们证明:算法返回 true 当且仅当字符串是有效括号字符串。
结论 1:栈中始终保存还没有匹配的左括号
从左到右扫描字符串。
遇到左括号时,算法把它压入栈,表示它等待之后的右括号匹配。
遇到右括号时,算法会弹出栈顶左括号和它匹配。
如果匹配成功,这一对括号就被完整闭合,不需要继续留在栈中。
因此任意时刻,栈中保存的都是已经出现但还没有匹配的左括号。
结论 2:如果算法返回 false,字符串一定无效
算法返回 false 有两种情况。
第一种是遇到右括号时栈为空。
这说明当前右括号没有对应的左括号,违反有效字符串要求。
第二种是栈顶左括号和当前右括号类型不匹配。
由于栈顶是最近出现且尚未闭合的左括号,如果它不能被当前右括号闭合,那么闭合顺序或括号类型一定错误。
所以算法返回 false 时,字符串一定无效。
结论 3:如果字符串无效,算法一定会返回 false
如果某个右括号没有对应的左括号,那么扫描到它时,栈会为空,算法返回 false。
如果某个右括号类型不匹配,扫描到它时,栈顶左括号不是对应类型,算法返回 false。
如果所有右括号扫描时都能匹配,但最后仍有左括号没有闭合,那么扫描结束后栈不为空,算法返回 false。
因此所有无效情况都会被算法发现。
结论 4:如果算法最终返回 true,字符串一定有效
算法最终返回 true 说明扫描过程中没有出现空栈匹配,也没有出现类型不匹配。
同时,最后栈为空,说明所有左括号都被匹配完成。
根据结论 1,每一对括号都是按照最近未闭合左括号与当前右括号匹配的。
所以括号类型和闭合顺序都正确,字符串有效。
得出结论
由结论 2 可知,算法不会把无效字符串误判为有效。
由结论 3 可知,算法不会漏掉任何无效情况。
由结论 4 可知,算法返回 true 时字符串一定满足题意。
因此算法正确。
举例理解
以:
s = "([])"
为例。
扫描过程如下:
| 当前字符 | 操作 | 栈 |
|---|---|---|
'(' |
左括号入栈 | ( |
'[' |
左括号入栈 | ([ |
']' |
匹配栈顶 '[' |
( |
')' |
匹配栈顶 '(' |
空 |
最终栈为空,所以返回:
true
再看:
s = "([)]"
扫描到 ')' 时,栈顶是 '['。
')' 不能和 '[' 匹配,所以直接返回:
false
复杂度分析
设字符串长度为 n。
每个字符只会被扫描一次。
每个左括号最多入栈一次,每个匹配到的左括号最多出栈一次。
- 时间复杂度:
O(n) - 空间复杂度:
O(n)
空间复杂度来自栈。
在最坏情况下,字符串全部是左括号,栈中会保存 n 个字符。