合法括号对之三种解法


  这道题目是leetcode上的一个题目,算是一道小题,但是解法很多,特此总结。

题目描述

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

示例 1 示例 2
输入 “(()” “)()())”
输出 2 4
解释 最长有效括号子串为 “()” 最长有效括号子串为 “()()”

题目分析

  回想以前的操作,可能都会用栈来判断一个字符串是否是合法的括号序列,这个复杂度是的 O ( n ) O(n) ,而一个长度为n的字符串一共可以有 n 2 n^2 个子串,对每一个都判断,时间复杂度也就是 O ( n 3 ) O(n^3) 的,这个就是暴力方法。
  暴力法中相当于对很多子串都重复判断了是否是合法的括号。所以我们思考其他方法。

动态规划解法

  我们模仿最大子串和的方法,我们定义 d p i dp_i 为到第i个字符为止,最长的合法的括号子串长度。我们通过这个设置来简化问题。首先,如果第i个字符是’(’,则此时显然不存在合法的字符串,因为不会有一个合法的括号序列时候以左括号结尾的, d p i = 0 dp_i=0 。如果第i个字符是’)’,这个时候有可能会增加最长合法括号子串的长度。如果前一个字符是’(’,那么一定会有合法的子串,且长度是 d p i 2 + 2 dp_{i-2}+2 ,如果i>1的话。如果i=1,显然不需要继续向 d p i 2 dp_{i-2} 追溯。
  但是如果前一个字符也是右括号,那么这个时候情况就复杂了。因为有可能前面的字符已经是非法的括号子串了,这个时候,再多一个’)’,还是非法的, d p i = 0 dp_i=0 ,但是如果前面的字符已经和之前的匹配了,这个时候‘)’如果要和左括号匹配,只能从前一个字符之前未匹配的左括号中找。举个例子’(()))’,第二个右括号前面的右括号已经匹配了第二个左括号,所以第二个右括号要匹配的左括号只能是前面的右括号所匹配的左括号(第二个左括号)之前的字符,也就是第一个左括号。
  此时,如果这个右括号和前面的左括号匹配了,这个时候就有可能把再前面的字符串连接起来,此时的长度就是 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)} ,这里还是需要判断一下下标是否越界。

  总结一下所有的情况,看到动态规划的情况就是下面的样子。这里默认所有的 d p i = 0 i < 1 dp_i=0 \quad i<1 ,下标为负数,值为0。
在这里插入图片描述
  这个动态规划最复杂的问题就是各种条件判断,基本的条件判断都是要同时看两个字符,而且甚至有些情况开始套娃了。但是总之查看过去的状态数是常数,不超过两个。理清了思路可以写代码了。

python代码如下
def longestValidParentheses_dp(s: str) -> int:
    if not s: return 0
    dp = [0] * len(s)
    maxLength = 0
    for i in range(1, len(s)):
        if s[i] == ')':
            if s[i - 1] == '(':
                dp[i] = dp[i - 2] + 2 if i > 1 else 2
            elif i - dp[i - 1] > 0 and s[i - dp[i - 1] - 1] == '(':
                dp[i] = dp[i - 1] + 2
                if i - dp[i - 1] - 1 > 0:
                    dp[i] += dp[i - dp[i - 1] - 2]
            maxLength = max(maxLength, dp[i])
    return maxLength

  显然动态规划只需要对字符串扫描一遍,时间复杂度是 O ( n ) O(n) 的,因为要保存以前的值,所以空间复杂度也是 O ( n ) O(n) 的。

栈的方法

  如果我们对栈匹配括号的算法进行改进一下,我们就能解决这个问题,但是这个方法同样是判断条件比较多一些。这个解法就比较巧妙一些,我们直接使用栈来处理这个问题,左括号入栈,右括号出栈匹配,一个最大的难点就是后面匹配的时候出栈了,和前面匹配的序列不好连接起来,每次匹配都是到右括号结束,但是匹配之后入栈的左括号也全都匹配了,这个时候就需要把这部分连起来,直观的思路比较难以处理这个地方。
  我们可以通过在栈中保存左括号的索引来解决这个问题,我们不知道这个序列一直可以向前连到什么地方,但是我们可以看栈顶元素的索引,显然栈顶元素右边的序列肯定是已经完成了匹配,这个时候我们通过做差来求解匹配的长度。
  如果先前进去的括号都匹配了,这个时候我们无法查看栈顶元素,为了把操作统一起来,我们初始化栈的时候在里面加上-1,如果它在栈顶,表示0-i的括号完成了匹配,长度依旧是i-(-1)。
  但是如果是右括号来了,结果把-1给弹栈了,这个时候是匹配失败啊,我们就采取一种方法,把i入栈,来把弹出去的-1替代下来,这样也方便后面求取长度。后面再遇到无法匹配的右括号也是同理,只要判断栈是否为空即可。

python代码
def longestValidParentheses(s: str) -> int:
    maxLength = 0
    stack = [-1]
    for i, c in enumerate(s):
        if c == '(':
            stack.append(i)
        else:
            stack.pop()
            if len(stack)==0:
                stack.append(i)
            else:
                maxLength = max(maxLength, i - stack[-1])
    return maxLength

两遍扫描,空间 O ( 1 ) O(1) 的解法

  这种方法我承认自己想不到,看到之后感觉真的耳目一新。但是这种方法可能过于对问题有针对性,本文所述的三种算法普适性是逐渐减弱的,这种方法大家就当扩充眼界吧。
  利用两个计数器 left和 right。首先,我们从左到右遍历字符串,对于遇到的每个 ‘(’,我们增加 left计算器,对于遇到的每个‘)’,我们增加 right 计数器。如果 right 计数器比 left 计数器大时,我们将 left 和 right 计数器同时变回 0 。此外每当 left 计数器与 right计数器相等时,我们计算当前有效字符串的长度,在这种情况下,左括号数目始终不小于右括号,两者数目相等时一定匹配。
  但是这种情况下会不会漏掉什么情况呢,比如’(()’,显然有一对合法的括号,但是我们是无法对其计数的。所以我们要排除这种特殊情况。
  我们从需要右到左做一遍类似的工作,但是这一次是如果左括号数目大于右括号,我们就从新计数。原理和从左到右是类似的。这样总共需要两趟扫描,时间复杂度是 O ( n ) O(n) ,仅保存常数个变量,空间复杂度 O ( 1 ) O(1)

python代码
def longestValidParentheses_twoscan(s: str) -> int:
    left = right = maxLength = 0
    for c in s:
        if c == '(':
            left += 1
        else:
            right += 1
        if left == right:
            maxLength = max(right << 1, maxLength)
        elif right > left:
            left = right = 0
    left = right = 0
    for c in s[::-1]:
        if c == '(':
            left += 1
        else:
            right += 1
        if left == right:
            maxLength = max(right << 1, maxLength)
        elif right < left:
            left = right = 0
    return maxLength
发布了36 篇原创文章 · 获赞 4 · 访问量 47万+

猜你喜欢

转载自blog.csdn.net/m0_38065572/article/details/104860451
今日推荐