LeetCode - 最长有效括号

题目描述

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

示例

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

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

方法一:栈

思路
1、栈用来保存读取到的括号的下标,而不是括号字符。初始时为了统一现在栈底将-1入栈,以便第一个遇到右括号与之后遇到的情况保持统一的处理

2、保持栈底元素为最后一个没有被匹配的右括号下标(初始是-1,一但-1出栈,之后栈底就是保存最后的未被匹配的有括号下标),这将用来通过读取的匹配到的右括号的下标来减去此值以获取当前长度。

3、使用for循环,依次获取字符串(括号),如果是左括号’(’,将其下标入栈。

4、如果是右括号’)’,先弹出栈顶元素表示此右括号被匹配了(当然也不一定是匹配了,如果栈中一个左括号的下标都没有,那么其之前要么有个-1要么有上一个未被匹配的右括号的下标在栈中,此时将pop出-1或者上一个未被匹配的右括号下标并非表示匹配而是表示更新最后一个未被匹配的右括号下标),然后再接着根据情况判断
- 如果pop后栈为空,这是因为读取到右括号时,栈中一个左括号的下标都没有,那么此时栈只有两种情况,要么是初始时放入的-1,要么是上一个没有被匹配的右括号下标。所以pop出来后,栈就会进入为空的状态。而此时,也会将该右括号的下标入栈,更新为最后一个没有匹配的右括号。
- 如果pop后不为空,那么pop出来的必定一个左括号的下标,那其用于和此时读取的右括号进行匹配了。此时就可用该右括号的下标减去pop后的栈顶元素(那要么是-1,要么是上一个未被匹配的右括号下标),就可以得到当前有效括号长度。并再用其和之前保存的最大的有效长度值比较来更新新的最大有效长度

代码

// 最长有效括号(求其长度)
public class LongestValidParentheses {

	// 方法一:栈
    public int longestValidParentheses(String s) 
    {
    	int maxLen = 0;
    	
    	Stack<Integer> stack = new Stack<>();
    	// 要注意,如果一开始栈为空,如果遇到第一个字符为左括号的时候我们就会将之下标放入栈中
    	// 这样就不满足[最后一个没有被匹配的右括号下标],为了保持统一处理,一开始放入的值设为-1
    	stack.push(-1);
    	
    	for (int i = 0; i < s.length(); i++)
    	{
    		// 对于每个'('我们将它的下标放入栈中
    		if (s.charAt(i) == '(') stack.push(i);
    		
    		else
    		{
    			// 对于遇到的每个')',我们先弹出栈顶元素
    			stack.pop();
    			// pop后如果栈为空,说明当前右括号是最后的没有被匹配的右括号,将其下标入栈
    			if (stack.empty()) stack.push(i);
    			// 如果pop后不为空,那就是有匹配的左括号在栈中,此时,由当前字符串的下标减去pop后的栈顶元素就是当前有效长度
    			// 然后再利用当前有效长度更新之前的最大有效长度,如果更长则替换
    			else maxLen = Math.max(maxLen, i - stack.peek()); // peek获取栈尾元素但不移除(与pop有点区别)
    		}
    	}
    	return maxLen;
    }
}

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

方法二:动态规划

思路
动态规划求解需要明确两点,1.需要记录的状态是神什么。2.如何从这一个状态转移到下一个状态(状态转移方程)

1、初始化一个全零的dp数组,其中的每一位元素用于记录以该位元素结尾的最长有效子字符串的长度

2、很明显,有效的子字符串一定是成对出现的,所以结尾的那个字符一定是右括号。因此我们遇到左括号那dp数组上的值保持为0,遇到右括号才对dp的值进行重新计算

3、有效字符串必定从长度为2开始,因此我们可以在遍历的时候从i=1开始,碰到’)'右括号时,状态转移方程为dp[i−1]+dp[i−dp[i−1]−2]+2。

  • dp[i-1]代表以前一个字符结尾的子串的有效长度,dp[i−dp[i−1]−2]为再前面一个,+2是此是读取的右括号找到了匹配的左括号,长度+2。

4、判断dp[i−dp[i−1]−1]是不是左括号

  • 如果其前面一个字符dp[i-1]就是左括号,那么dp[i−dp[i−1]−1]就是dp[i-1],同时也是与此右括号匹配的左括号,又因为左括号的dp值为0,所以加上dp[i-1]等于没加,靠+2来增加长度。而dp[i−dp[i−1]−2]就是dp[i-2]也就是这个左括号再前面一个,所以再加上它就是整个连起来的有效长度了(如果dp[i−dp[i−1]−2]不与值连续,那它的值就是0,加上也不会影响)
  • 如果前面一个dp[i-1]不是左括号(那必然就是右括号了),那么dp[i-1]就是以它为结尾的子串的有效长度。就要判断dp[i−dp[i−1]−1]是不是一个左括号,是则可以匹配此时读取的偶括号,长度+2,并加上以这个左括号再往前一个括号结尾的有效字符长度也就是dp[i−dp[i−1]−2]+2

代码

	public int longestValidParentheses(String s) 
	{
		// 长度在2以下不可能称为有效子串,直接排除,这样也可确保通过的字符串长度都在2以上(i在1以上),方便后面的循环判断
		if (s.length() <= 1) return 0;
		
		int maxLen = 0;
		// 初始话一个全零的dp数组,其中的每一位元素用于记录以该位元素结尾的最长有效子字符串的长度
		int[] dp = new int[s.length()];
		
		// 可以直接从第二个字符也就是i=1开始,因为有效长度至少为2。这样还可以确保dp[i - 1]中的i - 1没有越界
		// 而且上面的if已经过滤掉了长度低于2的字符串,所以到了这里i至少是能达到1的。
		for (int i = 1; i < s.length(); i++)
		{
			// i - dp[i - 1] - 1是与当前')'右括号对应现需要出现左括号的位置
			if (s.charAt(i) == ')' && i - dp[i - 1] - 1 >= 0 && 
				s.charAt(i - dp[i - 1] - 1) == '(')
				dp[i] = dp[i - 1] + ((i - dp[i - 1] - 2) >= 1 ? dp[i - dp[i - 1] - 2] : 0) + 2;
			
			maxLen = Math.max(maxLen, dp[i]);
		}
		return maxLen;
	}

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

方法三:左右分别遍历

思路
1、分别从左和右遍历两次字符串

2、每次遍历用两个变量left,right记录遇到的左括号’(‘和右括号’)'数量,用len记录当前有效长度。

3、每当left和right相等时我们记录一次此时的有效长度,并由此更新当前最长的有效长度。

4、分别从左和从右遍历是为了搜寻到所有的情况,假如只从左往右,那么如果一旦出现左括号一直大于右括号则一直无法进入到计算当前有效字符串长度,哪怕有有效字符串出现,例:……(()()()。所以再来一次反方向就可以考虑到所有的情况了。

5、以从左遍历为例,当右括号大于左括号数量时,之前的字符便不在考虑了,需要从下一个字符重新开始统计left和right。若是从右往左就是反过来当left>right时开始重新计算
代码

	public int longestValidParentheses(String s)
	{
		int left = 0, right = 0, maxLen = 0;
		// 从左到右遍历
		for (int i = 0; i < s.length(); i++)
		{
			// 每当遇到左括号或右括号,就让left或right加一
			if (s.charAt(i) == '(') left++;
			else right++;
			// 当左右括号相等时,可以更新现在的最大有效长度
			if (left == right) maxLen = Math.max(maxLen, 2 * right);
			// 一旦右括号数量多余左括号,那么就要重新置零开始算
			else if (right > left) left = right = 0;
		}
		
		// 如果只从一边遍历,那么会丢失一种情况
		// 如上如果只从左遍历,那么一旦出现左括号一直大于右括号就无法更新当前的最大有效长度
		// 此时maxLen要么保持0,要么一直保持上一次的最大有效长度
		
		// 所以再来一次从右往左遍历就可以保证所有情况都考虑到了
		left = right = 0;
		for (int i = s.length() - 1; i >= 0; i--)
		{
			if (s.charAt(i) == ')') right++;
			else left++;
			
			if (right == left) maxLen = Math.max(maxLen, 2 * left);
			// 此时判断置零重新计算的条件就要反过来
			else if (left > right) left = right = 0;
		}
		
		return maxLen;
	}

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

方法四:暴力解法

思路
1、因为要计算的是有效括号长度,而有效括号是成对出现的,最长有效括号一定是偶数
2、加入输入的一个字符串为长度为12,那么我们就从12,10,8……每次减2的长度来遍历。如果奇数就从最大的偶数长度开始
3、每次遍历依次寻找该遍历下的所有此长度的子串,只要满足,那必是最大的,更小的就不再遍历了
4、有效括号如何判断之前有一题就是求这个

复杂度分析
时间复杂度:O(N^3) 从长度为N的字符串产生所有可能的子字符串需要时间复杂度O(N^2),然后验证一个长度为N的子字符串需要时间复杂度O(N),两个是嵌套关系所以复杂度相乘
空间复杂度:O(N),子字符串的长度最多会需要一个深度为N的栈

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值