最长有效括号
方法一:暴力法
类似于使用两个指针,一个指向当前的字符,一个指向当前字符的之后的字符,然后再判断这两个指针之间的字符串是否是有效的。但是会超时。
1 | class Solution{ |
2 | public static int longestValidParentheses(String s) { |
3 | int max = 0; |
4 | for (int i = 0; i < s.length() - 1; i++) { |
5 | if (s.charAt(i) == ')') |
6 | continue; |
7 | for (int j = i + 1; j < s.length(); j++) { |
8 | if (s.charAt(j) == '(' || ((j - i + 1) & 1) == 1) |
9 | continue; |
10 | if (isValid(s.substring(i, j + 1))) { |
11 | max = Math.max(max, j - i + 1); |
12 | } |
13 | } |
14 | } |
15 | return max; |
16 | } |
17 | |
18 | public static boolean isValid(String s) { |
19 | Stack<Character> stack = new Stack<>(); |
20 | for (char c : s.toCharArray()) { |
21 | if (c == '(') |
22 | stack.push(')'); |
23 | else if (stack.isEmpty() || stack.pop() != c) |
24 | return false; |
25 | } |
26 | return stack.isEmpty(); |
27 | } |
28 | } |
复杂度分析:
- 时间复杂度:$O(n^3)$。
- 空间复杂度:$O(n)$。
方法二:栈
遇到左右括号匹配的题目,我们一般都会用栈解决,这里也是可以的。我们始终保持栈底元素为当前已经遍历过的元素中「最后一个没有被匹配的右括号的下标」,这样的做法主要是考虑了边界条件的处理,栈里其他元素维护左括号的下标:
- 对于遇到的每个$($ ,我们将它的下标放入栈中。
- 对于遇到的每个$)$ ,我们先弹出栈顶元素表示匹配了当前右括号:
- 如果栈为空,说明当前的右括号为没有被匹配的右括号,我们将其下标放入栈中来更新我们之前提到的「最后一个没有被匹配的右括号的下标」
- 如果栈不为空,当前右括号的下标减去栈顶元素即为「以该右括号为结尾的最长有效括号的长度」
我们从前往后遍历字符串并更新答案即可。
需要注意的是,如果一开始栈为空,第一个字符为左括号的时候我们会将其放入栈中,这样就不满足提及的「最后一个没有被匹配的右括号的下标」,为了保持统一,我们在一开始的时候往栈中放入一个值为$-1$ 的元素。
总之,两种索引会入栈:等待匹配的左括号索引、充当“分隔符”的右括号索引。后者入栈因为:当左括号匹配光了,栈还需要留一个“垫底”的“参照物”。
1 | class Solution { |
2 | public int longestValidParentheses(String s) { |
3 | Stack<Integer> stack = new Stack<>(); |
4 | stack.push(-1); |
5 | int ans = 0; |
6 | for(int i=0; i<s.length(); i++){ |
7 | if(s.charAt(i) == '('){ |
8 | stack.push(i); |
9 | }else{ |
10 | stack.pop(); |
11 | if(stack.empty()){ |
12 | stack.push(i); |
13 | }else{ |
14 | ans = Math.max(ans, i - stack.peek()); |
15 | } |
16 | } |
17 | } |
18 | return ans; |
19 | } |
20 | } |
复杂度分析:
- 时间复杂度:$O(n)$
- 空间复杂度:$O(n)$
方法三:动态规划
我们定义 $dp[i]$ 表示以下标 $i$ 字符结尾的最长有效括号的长度。我们将 $dp$ 数组全部初始化为 $0$ 。显然有效的子串一定以 $)$ 结尾,因此我们可以知道以 $($ 结尾的子串对应的 $dp$ 值必定为 $0$ ,我们只需要求解 $)$ 在 $dp$ 数组中对应位置的值。
我们从前往后遍历字符串求解 $dp$ 值,我们每两个字符检查一次:
如果$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] + dp[i-dp[i-1]-2]+2
$$
我们考虑如果倒数第二个 $)$ 是一个有效子字符串的一部分(记作 $sub_s$ ),对于最后一个 $)$ ,如果它是一个更长子字符串的一部分,那么它一定有一个对应的 $($ ,且它的位置在倒数第二个 $)$ 所在的有效子字符串的前面(也就是 $sub_s$ 的前面)。因此,如果子字符串 $sub_s$ 的前面恰好是 $($ ,那么我们就用 $2$ 加上 $sub_s$ 的长度($dp[i-1]$)去更新 $dp[i]$。同时,我们也会把有效子串 $”(sub_s)”$ 之前的有效子串的长度也加上,也就是再加上 $dp[i−dp[i−1]−2]$。
最后答案即为$dp$数组中的最大数。
1 | class Solution { |
2 | public int longestValidParentheses(String s) { |
3 | int ans = 0; |
4 | int[] dp = new int[s.length()]; |
5 | for(int i=1;i<s.length();i++){ |
6 | if(s.charAt(i) == '('){ |
7 | dp[i] = 0; |
8 | }else{ |
9 | if(s.charAt(i-1) == '('){ |
10 | dp[i] = (i>=2 ? dp[i-2]:0) + 2; |
11 | }else{ |
12 | if(i-dp[i-1]>0 && s.charAt(i-dp[i-1]-1)=='('){ |
13 | dp[i] = dp[i-1] + (i-dp[i-1]-2>=0 ? dp[i-dp[i-1]-2]:0) + 2; |
14 | } |
15 | } |
16 | } |
17 | ans = Math.max(ans, dp[i]); |
18 | } |
19 | return ans; |
20 | } |
21 | } |
复杂度分析:
- 时间复杂度:$O(n)$
- 空间复杂度:$O(n)$