0%

leetcode每日一题-最长有效括号

最长有效括号

image1

方法一:暴力法

类似于使用两个指针,一个指向当前的字符,一个指向当前字符的之后的字符,然后再判断这两个指针之间的字符串是否是有效的。但是会超时。

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. 如果栈为空,说明当前的右括号为没有被匹配的右括号,我们将其下标放入栈中来更新我们之前提到的「最后一个没有被匹配的右括号的下标」
    2. 如果栈不为空,当前右括号的下标减去栈顶元素即为「以该右括号为结尾的最长有效括号的长度」

我们从前往后遍历字符串并更新答案即可。

需要注意的是,如果一开始栈为空,第一个字符为左括号的时候我们会将其放入栈中,这样就不满足提及的「最后一个没有被匹配的右括号的下标」,为了保持统一,我们在一开始的时候往栈中放入一个值为$-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$ 值,我们每两个字符检查一次:

  1. 如果$s[i]==’)’ 且 s[i-1]==’(‘$ ,也就是形如$”……()”$的字符串,我们可以推出:
    $$
    dp[i]=dp[i-2]+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)$