算法思想一:栈
解题思路:
主要通过栈,可以在遍历给定字符串的过程中去判断到目前为止扫描的子串的有效性,同时能得到最长有效括号的长度。
具体做法是始终保持栈底元素为当前已经遍历过的元素中「最后一个没有被匹配的右括号的下标」,这样的做法主要是考虑了边界条件的处理,栈里其他元素维护左括号的下标:
1、对于遇到的每个‘(’ ,将它的下标放入栈中
2、对于遇到的每个 ‘)’ ,先弹出栈顶元素表示匹配了当前右括号:
1、如果栈为空,说明当前的右括号为没有被匹配的右括号,将其下标放入栈中来更新我们之前提到的「最后一个没有被匹配的右括号的下标」
2、如果栈不为空,当前右括号的下标减去栈顶元素即为「以该右括号为结尾的最长有效括号的长度」
3、从前往后遍历字符串并更新答案即可。
需要注意的是,如果一开始栈为空,第一个字符为左括号的时候我们会将其放入栈中,这样就不满足提及的「最后一个没有被匹配的右括号的下标」,为了保持统一,我们在一开始的时候往栈中放入一个值为 -1 的元素。
具体做法是始终保持栈底元素为当前已经遍历过的元素中「最后一个没有被匹配的右括号的下标」,这样的做法主要是考虑了边界条件的处理,栈里其他元素维护左括号的下标:
1、对于遇到的每个‘(’ ,将它的下标放入栈中
2、对于遇到的每个 ‘)’ ,先弹出栈顶元素表示匹配了当前右括号:
1、如果栈为空,说明当前的右括号为没有被匹配的右括号,将其下标放入栈中来更新我们之前提到的「最后一个没有被匹配的右括号的下标」
2、如果栈不为空,当前右括号的下标减去栈顶元素即为「以该右括号为结尾的最长有效括号的长度」
3、从前往后遍历字符串并更新答案即可。
需要注意的是,如果一开始栈为空,第一个字符为左括号的时候我们会将其放入栈中,这样就不满足提及的「最后一个没有被匹配的右括号的下标」,为了保持统一,我们在一开始的时候往栈中放入一个值为 -1 的元素。
图解:
代码展示:
Python版本
class Solution: def longestValidParentheses(self , s ): # write code here stack = [-1] ans = 0 for i, ch in enumerate(s): if ch == '(': # 左括号下标入栈 stack.append(i) else: if len(stack) > 1: # 匹配括号 stack.pop() # 最大括号长度 ans = max(ans, i-stack[-1]) else: # 将其下标放入栈中 stack[-1] = i return ans
复杂度分析
时间复杂度O(N):N是给定字符串长度,整个过程只需要遍历一次字符串即可
空间复杂度O(N):栈的大小在最坏情况下会达到 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
考虑如果倒数第二个 ‘)’ 是一个有效子字符串的一部分(记作 subs),对于最后一个‘)’ ,如果它是一个更长子字符串的一部分,那么它一定有一个对应的 ‘(’ ,且它的位置在倒数第二个 ‘)’ 所在的有效子字符串的前面(也就是 subs的前面)。因此,如果子字符串 subs 的前面恰好是 ‘(’ ,那么我们就用 2 加上 subs 的长度(dp[i−1])去更新 dp[i]。同时,也会把有效子串 “(subs)” 之前的有效子串的长度也加上,也就是再加上 dp[i−dp[i−1]−2]。
最后的答案即为 dp 数组中的最大值
图解:
代码展示:
JAVA版本
import java.util.*; public class Solution { /** * * @param s string字符串 * @return int整型 */ public int longestValidParentheses (String s) { // write code here int maxans = 0; int[] dp = new int[s.length()]; for (int i = 1; i < s.length(); 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] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') { dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2; } maxans = Math.max(maxans, dp[i]); } } return maxans; } }
复杂度分析
时间复杂度O(N):N是给定字符串长度,整个过程只需要遍历一次字符串即可
空间复杂度O(N):需要一个大小为 N 的 dp 数组
算法思想三:双指针
解题思路:
利用两个计数器 left 和 right 。首先,从左到右遍历字符串,对于遇到的每个‘(’,我们增加 left 计数器,对于遇到的每个 ‘)’ ,增加 right 计数器。每当 left 计数器与 right 计数器相等时,计算当前有效字符串的长度,并且记录目前为止找到的最长子字符串。当 right 计数器比 left 计数器大时,将 left 和 right 计数器同时变回 0 这样的做法贪心地考虑了以当前字符下标结尾的有效括号长度,每次当右括号数量多于左括号数量的时候之前的字符都扔掉不再考虑,重新从下一个字符开始计算,但这样会漏掉一种情况,就是遍历的时候左括号的数量始终大于右括号的数量,即 (() ,这种时候最长有效括号是求不出来的。
解决的方法也很简单,我们只需要从右往左遍历用类似的方法计算即可,只是这个时候判断条件反了过来
1、当 left 计数器比 right 计数器大时,将 left 和 right 计数器同时变回 0
2、当 left 计数器与 right 计数器相等时,计算当前有效字符串的长度,并且记录目前为止找到的最长子字符串
解决的方法也很简单,我们只需要从右往左遍历用类似的方法计算即可,只是这个时候判断条件反了过来
1、当 left 计数器比 right 计数器大时,将 left 和 right 计数器同时变回 0
2、当 left 计数器与 right 计数器相等时,计算当前有效字符串的长度,并且记录目前为止找到的最长子字符串
代码展示:
C++版本
class Solution { public: /** * * @param s string字符串 * @return int整型 */ int longestValidParentheses(string s) { // write code here int left = 0, right = 0, maxlength = 0; for (int i = 0; i < s.length(); i++) { if (s[i] == '(') { left++; } else { right++; } if (left == right) { maxlength = max(maxlength, 2 * right); } else if (right > left) { left = right = 0; } } left = right = 0; for (int i = (int)s.length() - 1; i >= 0; i--) { if (s[i] == '(') { left++; } else { right++; } if (left == right) { maxlength = max(maxlength, 2 * left); } else if (left > right) { left = right = 0; } } return maxlength; } };
复杂度分析
时间复杂度O(N):N是给定字符串长度,整个过程只需要遍历一次字符串即可
空间复杂度O(1):只需要常数空间存放若干变量