LeetCode-32 - 最长有效括号

题目

来源:LeetCode.

给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

示例 1:

输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"

示例 2:

输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"

示例 3:

输入:s = ""
输出:0

提示:

  • 0 < = s . l e n g t h < = 3 ∗ 1 0 4 0 <= s.length <= 3 * 10^4 0<=s.length<=3104
  • s [ i ] s[i] s[i] 为 ‘(’ 或 ‘)’

接下来看一下解题思路:

思路一:栈:

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

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

  • 如果是 ‘(’,将下标入栈;
  • 如果是 ‘)’,弹出栈顶元素,表示匹配当前右括号;
    • 如果栈为空,说明当前右括号没有匹配的左括号,将下标放入栈中来更新(最后一个没有被匹配的右括号下标)
    • 如果栈不为空,当前右括号下标减去栈顶元素即为(以该右括号结尾的最长有效括号长度);

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

    
注意,如果一开始栈为空,第一个字符为左括号的时候我们会将其放入栈中,这样就不满足提及的「最后一个没有被匹配的右括号的下标」,为了保持统一,我们在一开始的时候往栈中放入一个值为 − 1 −1 1 的元素。

public static int longestValidParentheses(String s) {
    if (s == null || s.length() <= 0) {
        return 0;
    }
    int n = s.length();
    if (n < 2) {
        return 0;
    }

    Stack<Integer> stack = new Stack<>();
    int max = 0;
    stack.push(-1);

    //1.如果是 '(',将下标入栈;
    //2.如果是 ')',弹出栈顶元素,表示匹配当前右括号;
    //  如果栈为空,说明当前右括号没有匹配的左括号,将下标放入栈中来更新(最后一个没有被匹配的右括号下标)
    //  如果栈不为空,当前右括号下标减去栈顶元素即为(以该右括号结尾的最长有效括号长度);
    for (int i = 0; i < n; ++i) {
        if (s.charAt(i) == '(') {
            stack.push(i);
        } else {
            stack.pop();
            if (stack.isEmpty()) {
                stack.push(i);
            } else {
                max = Math.max(max, i - stack.peek());
            }
        }
    }

    return max;
}
复杂度分析

时间复杂度: O ( n ) O(n) O(n) n n n 是给定字符串的长度。我们只需要遍历字符串一次即可。
空间复杂度: O ( n ) O(n) O(n)。栈的大小在最坏情况下会达到 n n n,因此空间复杂度为 O ( n ) O(n) O(n)

思路二:正向逆向结合法:

    可以用两个计数器 left \textit{left} left right \textit{right} right 来统计左括号和右括号出现的次数 。
    首先,我们从左到右遍历字符串,对于遇到的每个 ‘(’ \text{‘(’} ‘(’,增加 left \textit{left} left 计数器,对于遇到的每个 ‘)’ \text{‘)’} ‘)’ ,增加 right \textit{right} right 计数器。每当 left \textit{left} left 计数器与 right \textit{right} right 计数器相等时,计算当前有效字符串的长度,并且记录目前为止找到的最长子字符串。当 right \textit{right} right 计数器比 left \textit{left} left 计数器大时,将 left \textit{left} left right \textit{right} right 计数器同时变回 0 0 0
    这样的做法贪心地考虑了以当前字符下标结尾的有效括号长度,每次当右括号数量多于左括号数量的时候之前的字符都扔掉不再考虑,重新从下一个字符开始计算,但这样会漏掉一种情况,就是遍历的时候左括号的数量始终大于右括号的数量,即 ((),这种时候最长有效括号是求不出来的。

    解决的方法也很简单,只需要从右往左遍历用类似的方法计算即可,只是这个时候判断条件反了过来;
    完整思路如下:

  • 两遍扫描
    • 第一次:
      • 左到右扫描:遇到左括号,++left;遇到右括号 ++right;当 left = right,计算当前有效括号的长度;并记录到目前为止找到最长的子串;如果 right > left, left和right同时置零;
    • 第二次:
      • 右到左扫描:遇到左括号,++left;遇到右括号 ++right;当 left = right,计算当前有效括号的长度;并记录到目前为止找到最长的子串;如果 left > right, left和right同时置零;
public static int longestValidParentheses3(String s) {
    if (s == null || s.length() <= 0) {
        return 0;
    }

    int n = s.length();
    if (n < 2) {
        return 0;
    }

    int maxLen = 0;
    // 两遍扫描
    // 第一次:
    //    从左到右扫描:遇到左括号,++left;遇到右括号 ++right;当 left = right,
    //      计算当前有效括号的长度;并记录到目前为止找到最长的子串;如果 right > left, left和right同时置零;
    // 第二次:
    //    从右到左扫描:遇到左括号,++left;遇到右括号 ++right;当 left = right,
    //      计算当前有效括号的长度;并记录到目前为止找到最长的子串;如果 left > right, left和right同时置零;
    int left = 0;
    int right = 0;
    // 从左向右扫描
    for (int i = 0; i < n; ++i) {
        if (s.charAt(i) == '(') {
            ++left;
        } else {
            ++right;
        }
        if (right > left) {
            left = right = 0;
        }
        if (left == right) {
            maxLen = Math.max(maxLen, right + left);
        }
    }
    left = right = 0;
    // 从右向左扫描
    for (int i = n - 1; i >= 0; --i) {
        if (s.charAt(i) == ')') {
            ++right;
        } else {
            ++left;
        }
        if (left > right) {
            left = right = 0;
        }
        if (left == right) {
            maxLen = Math.max(maxLen, right + left);
        }
    }
    return maxLen;
}
复杂度分析

时间复杂度: O ( n ) O(n) O(n),其中 n n n 为字符串长度。我们只要正反遍历两边字符串即可。
空间复杂度: O ( 1 ) O(1) O(1)。我们只需要常数空间存放若干变量。

思路三:动态规划:

    定义 dp [ i ] \textit{dp}[i] dp[i] 表示以下标 i i i 字符结尾的最长有效括号的长度。将 dp \textit{dp} dp 数组全部初始化为 0 0 0 。显然有效的子串一定以 ‘)’ \text{‘)’} ‘)’ 结尾,因此可以知道以 ‘(’ \text{‘(’} ‘(’ 结尾的子串对应的 dp \textit{dp} dp 值必定为 0 0 0 ,只需要求解 ‘)’ \text{‘)’} ‘)’ dp \textit{dp} dp 数组中对应位置的值。

    从前往后遍历字符串求解 dp \textit{dp} dp 值,每两个字符检查一次

  1. s [ i ] = ‘)’ s[i] = \text{‘)’} s[i]=‘)’ s [ i − 1 ] = ‘(’ s[i - 1] = \text{‘(’} s[i1]=‘(’,也就是字符串形如 “ … … ( ) ” “……()” (),可以推出:

dp [ i ] = dp [ i − 2 ] + 2 \textit{dp}[i]=\textit{dp}[i-2]+2 dp[i]=dp[i2]+2
    可以进行这样的转移,是因为结束部分的 " ( ) " "()" "()" 是一个有效子字符串,并且将之前有效子字符串的长度增加了 2 2 2

  1. s [ i ] = ‘)’ s[i] = \text{‘)’} s[i]=‘)’ s [ i − 1 ] = ‘)’ s[i - 1] = \text{‘)’} s[i1]=‘)’,也就是字符串形如 “ … … ) ) ” “……))” )),可以推出:
    如果 s [ i − dp [ i − 1 ] − 1 ] = ‘(’ s[i - \textit{dp}[i - 1] - 1] = \text{‘(’} s[idp[i1]1]=‘(’,那么

dp [ i ] = dp [ i − 1 ] + dp [ i − dp [ i − 1 ] − 2 ] + 2 \textit{dp}[i]=\textit{dp}[i-1]+\textit{dp}[i-\textit{dp}[i-1]-2]+2 dp[i]=dp[i1]+dp[idp[i1]2]+2

    考虑如果倒数第二个 ‘)’ \text{‘)’} ‘)’ 是一个有效子字符串的一部分(记作 s u b s sub_s subs),对于最后一个 ‘)’ \text{‘)’} ‘)’ ,如果它是一个更长子字符串的一部分,那么它一定有一个对应的 ‘(’ \text{‘(’} ‘(’ ,且它的位置在倒数第二个 ‘)’ \text{‘)’} ‘)’ 所在的有效子字符串的前面(也就是 s u b s sub_s subs的前面)。因此,如果子字符串 s u b s sub_s subs的前面恰好是 ‘(’ \text{‘(’} ‘(’ ,那么就用 2 2 2 加上 s u b s sub_s subs的长度( dp [ i − 1 ] \textit{dp}[i-1] dp[i1])去更新 dp [ i ] \textit{dp}[i] dp[i]。同时,也会把有效子串 “ ( s u b s ) ” “(sub_s)” (subs)之前的有效子串的长度也加上,也就是再加上 dp [ i − dp [ i − 1 ] − 2 ] \textit{dp}[i-\textit{dp}[i-1]-2] dp[idp[i1]2]

    最后的答案即为 dp \textit{dp} dp 数组中的最大值。

状态转移方程的三步推导:

在这里插入图片描述

  1. 判断 i − dp [ i − 1 ] − 1 i-\textit{dp}[i-1]-1 idp[i1]1 是否为 " ( " "(" "(",则基础长度为 2 2 2

在这里插入图片描述
2. 内部连在一起的最长有效括号, dp [ i − 1 ] \textit{dp}[i-1] dp[i1];状态转移方程: dp [ i ] = 2 \textit{dp}[i] = 2 dp[i]=2
在这里插入图片描述
3. 外部连在一起的最长有效括号, dp [ i − d p [ i − 1 ] − 2 ] \textit{dp}[i-dp[i - 1] - 2] dp[idp[i1]2];状态转移方程: dp [ i ] = 2 + d p [ i − 1 ] \textit{dp}[i] = 2 + dp[i - 1] dp[i]=2+dp[i1]

在这里插入图片描述
状态转移方程: dp [ i ] = 2 + d p [ i − 1 ] + d p [ i − d p [ i − 1 ] − 2 ] \textit{dp}[i] = 2 + dp[i - 1] + dp[i - dp[i - 1] - 2] dp[i]=2+dp[i1]+dp[idp[i1]2]
分别将 1 , 4 , 5 1, 4, 5 1,4,5 带入到状态转移方程:
dp [ 1 ] = 2 + d p [ 0 ] + d p [ − 1 ] = 2 + 0 + 0 = 2 \textit{dp}[1] = 2 + dp[0] + dp[-1] = 2 + 0 + 0 = 2 dp[1]=2+dp[0]+dp[1]=2+0+0=2
dp [ 4 ] = 2 + d p [ 3 ] + d p [ 2 ] = 2 + 0 + 0 = 2 \textit{dp}[4] = 2 + dp[3] + dp[2] = 2 + 0 + 0 = 2 dp[4]=2+dp[3]+dp[2]=2+0+0=2
dp [ 5 ] = 2 + d p [ 4 ] + d p [ 1 ] = 2 + 2 + 2 = 6 \textit{dp}[5] = 2 + dp[4] + dp[1] = 2 + 2 + 2 = 6 dp[5]=2+dp[4]+dp[1]=2+2+2=6

思考:

在这里插入图片描述

思考1:

    当求解到 6 6 6 号位置的时候,因为 6 6 6 号前面有 3 , 4 , 5 3,4,5 345 三个连续匹配的括号,所以需要查看 3 3 3 号前面是否有连续匹配的括号,如果有就相加;

思考2:

    当求解到 2 2 2 号位置的时候,因为前面 0 , 1 0,1 01是连续匹配的,所以需要查看 − 1 -1 1 位置的值,这时候需要做边界处理;

public static int longestValidParentheses1(String s) {
    if (s == null || s.length() <= 0) {
        return 0;
    }
    int n = s.length();
    if (n < 2) {
        return 0;
    }

    int max = 0;
    int[] dp = new int[n];

    // 状态转移方程:dp[i] = 2 + dp[i - 1] + dp[i - dp[i - 1] - 2];
    for (int i = 1; i < n; ++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] - 1) >= 0) && (s.charAt(i - dp[i - 1] - 1) == '(')) {
                dp[i] = 2 + dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0 );
            }
            // 更新最大值
            max = Math.max(max, dp[i]);
        }
    }
    return max;
}
复杂度分析

时间复杂度: O ( n ) O(n) O(n),其中 n n n 为字符串的长度。我们只需遍历整个字符串一次,即可将 dp \textit{dp} dp 数组求出来。
空间复杂度: O ( n ) O(n) O(n)。我们需要一个大小为 n n n dp \textit{dp} dp 数组。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LeetCode-Editor是一种在线编码工具,它提供了一个用户友好的界面编写和运行代码。在使用LeetCode-Editor时,有时候会出现乱码的问题。 乱码的原因可能是由于编码格式不兼容或者编码错误导致的。在这种情况下,我们可以尝试以下几种解决方法: 1. 检查文件编码格式:首先,我们可以检查所编辑的文件的编码格式。通常来说,常用的编码格式有UTF-8和ASCII等。我们可以将编码格式更改为正确的格式。在LeetCode-Editor中,可以通过界面设置或编辑器设置来更改编码格式。 2. 使用正确的字符集:如果乱码是由于使用了不同的字符集导致的,我们可以尝试更改使用正确的字符集。常见的字符集如Unicode或者UTF-8等。在LeetCode-Editor中,可以在编辑器中选择正确的字符集。 3. 使用合适的编辑器:有时候,乱码问题可能与LeetCode-Editor自身相关。我们可以尝试使用其他编码工具,如Text Editor、Sublime Text或者IDE,看是否能够解决乱码问题。 4. 查找特殊字符:如果乱码问题只出现在某些特殊字符上,我们可以尝试找到并替换这些字符。通过仔细检查代码,我们可以找到导致乱码的特定字符,并进行修正或替换。 总之,解决LeetCode-Editor乱码问题的方法有很多。根据具体情况,我们可以尝试更改文件编码格式、使用正确的字符集、更换编辑器或者查找并替换特殊字符等方法来解决这个问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值