echo

任生命穿梭 时间的角落

0%

最长有效括号

32. 最长有效括号

给定一个只包含 '('')' 的字符串,找出最长的包含有效括号的子串的长度。

示例 1:

1
2
3
输入: "(()"
输出: 2
解释: 最长有效括号子串为 "()"

示例 2:

1
2
3
输入: ")()())"
输出: 4
解释: 最长有效括号子串为 "()()"

方法一:暴力法

我们直接枚举所有的字符串并判断其是否有效。这里我们使用一个窗口,窗口大小初始化为不大于字符串长度的最大偶数,每次循环都判断窗口内的字符串是否有效,如果有效则返回窗口大小,无效则将窗口大小减小 2 ,继续判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public int longestValidParentheses(String s) {
int len = s.length();
int window = (len & 1) == 1 ? len - 1 : len;
while(window != 0){
for(int i = 0; window >= 2 && i + window - 1 < len; i++){
if(isValid(s, i, i + window - 1)){
return window;
}
}
window -= 2;
}
return 0;
}

boolean isValid(String s, int start, int end){
LinkedList<Character> stack = new LinkedList<>();
for(int i = start; i <= end; i++){
if(s.charAt(i) == '('){
stack.add('(');
}else{
if(stack.size() != 0){
stack.removeLast();
}else{
return false;
}
}
}
return stack.size() == 0;
}
}

时间复杂度O(n^3),空间复杂度O(1)。

方法二:动态规划

定义 dp[i] 表示以下标 i 字符结尾的最长有效括号的长度。我们将 dp 数组全部初始化为 0。有效的子串一定以 ‘)’ 结尾,以 ‘(‘ 结尾的子串对应的 dp 值一定为 0 。

我们从前往后遍历字符串求解 dp 值,每两个字符检查依次:

  1. s[i] = ‘)’ 且 s[i - 1] = ‘(‘,形如 “………()”,我们可以推出:
    $$
    dp[i]=dp[i - 2] + 2
    $$

  2. s[i] = ‘)’ 且 s[i - 1] = ‘)’,形如 “……….))”,我们可以推出:

    如果 s[i - dp[i - 1] - 1] = ‘(‘ 那么
    $$
    dp[i]=dp[i-1] + 2 + dp[i - dp[i - 1] - 2]
    $$

image-20210824162120356

第一种情况为后面的两个字符 “()” 与前面的字符串构成一个更长的有效括号子串;

第二种情况为后面两个字符 “))” 中有两层子串,里面一个 ‘)’ 构成一个有效括号子串,再加上外层括号,然后看包括外层括号的子串能否与外层括号’(‘ 连接起来构成一个更长的有效括号子串,s[i - dp[i - 1] - 1] = '(' 判断外层左括号是否存在,dp[i - 1] + 2 表示内层子串加上外层两个括号构成更长的子串长度,dp[i - dp[i - 1] - 2] 表示外层 ‘(‘ 与其之前的字符串构成更长的有效括号子串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int longestValidParentheses(String s) {
int ans = 0;
int dp[] = new int[s.length()];
for(int i = 1; i < dp.length; i++){
if(s.charAt(i) == ')'){
if(s.charAt(i - 1) == '('){
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
}else if(i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '('){
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
}
ans = Math.max(ans, dp[i]);
}
}
return ans;
}
}

时间复杂度O(n),空间复杂度O(n)。

方法三:栈

通过栈,我们可以在遍历给定字符串的过程中去判断到目前为止扫描的子串的有效性,同时能得到最长有效括号的长度。

我们始终保持栈底元素为当前已经遍历过的元素中「最后一个没有被匹配的右括号的下标」,这样的作法考虑了边界条件的处理,栈中其他元素维护左括号的下标:

  • 对于遇到的每个 ‘(‘ ,我们将它的下标放入栈中
  • 对于遇到的每个 ‘)’,我i们先弹出栈顶元素表示匹配了当前右括号:
    • 如果栈为空,说明当前右括号为「没有被匹配的右括号」,我们将其下标放入栈中更新栈底元素。
    • 如果栈不为空,当前右括号的下标减去栈顶元素即为「以该右括号为结尾的最长有效括号的长度」

为了满足栈底一直是「最后一个没有被匹配的右括号的下标」,我们在一开始往栈中放入一个值为 - 1 的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public int longestValidParentheses(String s) {
int ans = 0;
LinkedList<Integer> stack = new LinkedList<>();
stack.add(-1);
for(int i = 0; i < s.length(); i++){
if(s.charAt(i) == '('){
stack.add(i);
}else{
stack.removeLast();
if(stack.size() == 0){
stack.add(i);
}else{
ans = Math.max(ans, i - stack.getLast());
}
}
}
return ans;
}
}

时间复杂度O(n),空间复杂度O(n)。

方法四:正向逆向结合

我们利用两个计数器 left 和 right 。首先,我们从左到右遍历字符串,对于遇到的每个 ‘(‘,我们增加 left 计数器,对于遇到的每个 ‘)’,我们增加 right计数器。每当 left 计数器与 right 计数器相等时,我们计算当前有效字符串的长度,记录目前为止找到的最长子字符串。当 right 计数器比 left 计数器大时,我们将 left 和 right 同时变回 0 。

这样的做法贪心地考虑了以当前字符下标结尾的有效括号长度,每次当右括号数量多于左括号数量的时候之前的字符我们都扔掉不再考虑,重新从下一个字符开始计算,但这样会漏掉一种情况,就是遍历的时候左括号的数量始终大于右括号的数量,即 ”(() “,这种时候最长有效括号是求不出来的。

我们只用从右往左遍历用类似的方法计算即可:

  • left 计数器比 right 计数器大时,将 left 计数器和 right 计数器同时变回 0 。
  • left 计数器与 right 计数器相等时,我们计算当前有效字符串的长度,并且记录目前为止找到的最长子字符串。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public int longestValidParentheses(String s) {
int left = 0, right = 0, maxLength = 0;
for(int i = 0; i < s.length(); i++){
if(s.charAt(i) == '('){
left++;
}else{
right++;
}

if(left == right){
maxLength = Math.max(maxLength, 2 * right);
}else if(right > left){
left = right = 0;
}
}

left = right = 0;
for(int i = s.length() - 1; i >= 0; i--){
if(s.charAt(i) == '('){
left++;
}else{
right++;
}

if(left == right){
maxLength = Math.max(maxLength, 2 * left);
}else if(left > right){
left = right = 0;
}
}
return maxLength;
}
}

时间复杂度O(n),空间复杂度O(1)。