题目描述
给你一个只包含 '(' 和 ')' 的字符串,找出最长有效括号子串的长度。
有效括号要求左右括号正确匹配,并且子串必须是连续的。
例如:
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
最初思路
一开始想到的是双指针:
让 l 指向合法括号区间里的 '('
让 r 指向可以匹配的 ')'
但实际写的时候会发现,l 怎么移动并不好处理。因为有效括号不仅要求数量匹配,还要求中间的结构也匹配,比如 ")()"、"())()" 这类情况会打断连续区间。
后面改成用栈记录左括号下标,匹配成功后标记有效位置,再统计最长连续有效区间。
问题出在哪里
第一版栈写法中,遇到 ')' 时直接执行匹配:
isValid[i] = isValid[st[top--]] = true
但如果当前右括号前面没有可以匹配的左括号,top 就是 -1。
例如:
s = ")()"
第一轮遍历时:
i = 0
s[i] = ')'
top = -1
这时访问 st[top] 就会访问非法下标。
所以遇到 ')' 时,必须先判断栈里是否还有未匹配的 '('。
正确思路
这个解法分成两步:
1. 用栈找出所有能成功匹配的括号位置
2. 扫描标记数组,统计最长连续 true 的长度
栈里保存的是还没有被匹配的 '(' 的下标。
遍历字符串时:
遇到 '(':
把当前下标入栈
遇到 ')':
如果栈非空,说明可以和最近的一个 '(' 匹配
把当前 ')' 和栈顶 '(' 的位置都标记成有效
然后弹出这个 '('
最后,所有被标记为 true 的位置都属于某个有效括号片段。最长有效括号子串,就是最长连续 true 的长度。
关键不变量
栈中的下标始终表示:
已经遍历过,但还没有被匹配的 '(' 的下标。
因此,当遇到一个 ')' 且 top >= 0 时:
st[top] 就是当前 ')' 能匹配到的最近一个未匹配 '(' 的下标。
匹配成功后执行 top--,这个 '(' 就从栈里弹出,后面不会再被别的 ')' 重复匹配。
手推过程
以这个字符串为例:
s = "())()"
遍历过程:
i = 0, '(' 入栈,栈中有 [0]
i = 1, ')' 匹配 st[top] = 0,标记 0 和 1
i = 2, ')' 但栈空,无法匹配
i = 3, '(' 入栈,栈中有 [3]
i = 4, ')' 匹配 st[top] = 3,标记 3 和 4
标记数组可以理解为:
下标: 0 1 2 3 4
字符: ( ) ) ( )
标记: T T F T T
最长连续 true 的长度是 2,所以答案是 2。
边界处理
需要特别注意两类边界:
1. 当前字符是 ')',但栈为空:不能匹配,直接跳过
2. 字符串为空,或者没有任何有效括号:答案应该是 0
所以 ans 初始值应该设为 0,而不是 -1。
伪代码
下面的伪代码与后面的 Java 实现一一对应:
n = 字符串长度
创建数组 st 作为栈,保存 '(' 的下标
top = -1
创建布尔数组 isValid,标记每个位置是否属于有效括号
遍历字符串中的每个下标 i:
如果 s[i] 是 '(':
top 加 1
st[top] = i
否则如果 top >= 0:
把 i 标记为有效
把 st[top] 标记为有效
top 减 1
ans = 0
cnt = 0
遍历 isValid:
如果当前位置为 true:
cnt 加 1
用 cnt 更新 ans
否则:
cnt = 0
返回 ans
Java 代码
class Solution {
public int longestValidParentheses(String s) {
int n = s.length();
int[] st = new int[n];
int top = -1;
boolean[] isValid = new boolean[n];
char[] c = s.toCharArray();
for (int i = 0; i < n; i++) {
if (c[i] == '(') {
st[++top] = i;
} else if (top >= 0) {
isValid[i] = isValid[st[top--]] = true;
}
}
int ans = 0;
int cnt = 0;
for (boolean v : isValid) {
if (v) {
cnt++;
ans = Math.max(ans, cnt);
} else {
cnt = 0;
}
}
return ans;
}
}
易错点
1. 空栈时不能匹配右括号
遇到 ')' 时,不能直接访问 st[top]。
必须先判断:
top >= 0
否则像 ")()" 这种以右括号开头的字符串,会在第一轮就访问非法下标。
2. st[top] 保存的是尚未匹配的左括号
st[top] 不是任意一个左括号,而是最近的、还没有被匹配的 '(' 下标。
匹配成功后:
top--
这个左括号就已经被使用,后面不能再匹配。
3. 最长的是连续有效区间
标记为 true 的位置必须连续,才属于同一个有效括号子串。
例如:
s = "())()"
标记:T T F T T
虽然一共有四个位置被标记为有效,但中间被 F 断开,所以最长长度是 2,不是 4。
4. 答案初始值应该是 0
如果字符串为空,或者没有任何有效括号,比如:
s = ""
s = ")))"
答案都应该是 0。
建议测试用例
s = ")()"
输出:2
s = "())()"
输出:2
s = ")()())"
输出:4
s = ""
输出:0
s = ")))"
输出:0
复杂度分析
- 时间复杂度:
O(n),一次遍历字符串做匹配,一次遍历标记数组统计最长连续有效区间。 - 空间复杂度:
O(n),使用了栈数组和isValid标记数组。
总结
这题的关键是先把"括号是否匹配"和"有效子串是否连续"拆开。
栈负责解决匹配问题:
当前 ')' 只能匹配最近的、尚未匹配的 '('
isValid 数组负责解决连续长度问题:
被标记的位置连续多少个,最长有效括号子串就是多少。
这份写法虽然不是最省空间的栈写法,但思路清晰:先标记所有有效位置,再统计最长连续有效区间。