一、题目描述

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

示例 1:

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

示例 2:

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

二、解题思路 & 代码

2.1 动态规划

d p [ i ] dp[i] dp[i] 表示以 i i i 结尾的最长有效括号

  1. s [ i ] s[i] s[i]( d p [ i ] dp[i] dp[i] 必然等于 0 0 0,因为不可能组成有效的括号;

  2. s [ i ] s[i] s[i])
    1)当 s [ i − 1 ] s[i-1] s[i1](,那么 d p [ i ] = d p [ i − 2 ] + 2 dp[i] = dp[i-2] + 2 dp[i]=dp[i2]+2
    2)当 s [ i − 1 ] s[i-1] s[i1]) 并且 s [ i − d p [ i − 1 ] − 1 ] s[i-dp[i-1] - 1] s[idp[i1]1](,那么

d p [ i ] = d p [ i − 1 ] + 2 + d p [ i − d p [ i − 1 ] − 2 ] dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2] dp[i]=dp[i1]+2+dp[idp[i1]2]

代码可读性强版本

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        n = len(s)
        if n == 0: 
            return 0
        dp = [0] * n
        for i in range(n):
            if i>0 and s[i] == ")":
                if  s[i - 1] == "(":
                    dp[i] = dp[i - 2] + 2
                elif s[i - 1] == ")" and i - dp[i - 1] - 1 >= 0 and s[i - dp[i - 1] - 1] == "(":
                    dp[i] = dp[i - 1] + 2 + dp[i - dp[i - 1] - 2]

        return max(dp)

代码简化版本:

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        n = len(s)
        if n == 0:
            return 0
        dp = [0]*n
        for i in range(len(s)):
            # i-dp[i-1]-1是与当前)对称的位置
            if s[i]==')' and i-dp[i-1]-1>=0 and s[i-dp[i-1]-1]=='(':
               dp[i]=dp[i-1]+dp[i-dp[i-1]-2]+2
        return max(dp)   

复杂度分析

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

2.2 栈

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

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

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

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

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        stack = [-1]
        length = 0
        max_length = 0
        for i in range(len(s)):
            if s[i] == '(':
                stack.append(i)
            else:
                stack.pop()
                if stack == []:
                    stack.append(i)
                else:
                    length = i-stack[-1]
                    max_length = max(max_length,length)
        return max_length

复杂度分析

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

2.3 正向逆向结合(空间 O ( 1 ) O(1) O(1)

利用两个计数器 left 和 right 。

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

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

解决的方法,我们只需要从右往左遍历用类似的方法计算即可,只是这个时候判断条件反了过来:

  • 当 left 计数器比 right 计数器大时,我们将 left 和 right 计数器同时变回 0
  • 当 left 计数器与right 计数器相等时,我们计算当前有效字符串的长度,并且记录目前为止找到的最长子字符串
class Solution:
    def longestValidParentheses(self, s: str) -> int:
        n, left, right, maxlength = len(s), 0, 0, 0
        for i in range(n):
            if s[i] =='(':
                left+=1
            else:
                right+=1
            if left == right:
                maxlength = max(maxlength, 2 * right)
            elif right > left:
                left = right = 0
        left = right = 0
        for i in range(n-1,-1,-1):
            if s[i] =='(':
                left+=1
            else:
                right+=1
            if left == right:
                maxlength = max(maxlength, 2 * left)
            elif right < left:
                left = right = 0

        return maxlength

复杂度分析

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

参考:

  1. LeetCode官方题解 & 评论区