算法分析与设计第十二次作业( leetcode 中 Longest Valid Parentheses 题解 )

题解正文

题目描述

在这里插入图片描述

问题分析

本题给出一串由 组成的字符串,要求出能够正确匹配的最长子串的长度,比如 (() 这个字符串能够匹配的最大长度是2.

解题思路

方法一:(朴素做法)

  • 一开始看到这个题目就想的很简单,我认为这就是个普通的括号匹配问题,直接用入栈出栈的思想来解决就好了(只是用栈的思想,实际实现的时候不需要用栈),就是在遇到左括号时就将 leftCount 计数器加一,遇到右括号时就将 leftCount 计数器减一,并且 Length 加二(表示获取两个合法字符);直到遇到右括号时 leftCount 为0,说明遇到非法右括号,当前串不再匹配,子串的 Length 停止增长,然后重新开始寻找下一个正确匹配的子串;当我们分析完整个字符串,就可以比较前面找到的各个正确匹配的子串,得出最长子串长度。
  • 这个思路看似正确,但是提交之后并不能通过,看一下例子就知道:对于字符串 ()(() 而言,它的最长匹配子串是2,但是我们的程序求出的结果是4,这是因为这个串直到最后也没有出现过匹配错误(右括号出现时 leftCount 为0),所以导致匹配子串长度不断增加,但是到最后左括号都没有被完全匹配完,所以实际上整个子串是不匹配的,而我们的程序没检测出来。
  • 我想到的解决方案是记录每一个未匹配的左括号的位置,然后将这些未匹配左括号作为分割点,将子串进行分割,然后求解分割后的各个子串长度最大值即可,使用这种方法编写程序能够通过检测。
    考虑到实际上使用未改进算法时,只在最后一个子串出现上述错误,我们可以之对最后一个子串进行分割,然后与前面的串进行长度比较。
    关于为什么只有最后一个子串会出现上面的错误:因为前面的子串 Length 停止增长是因为遇到右括号时 leftCount 为0,也就是说此时已经没有未匹配的左括号了,因此不存在未完全匹配完的左括号从而导致错误。

方法二:(动态规划方法)
这个方法是参考评论区大神做法做的,算法复杂度和上面的算法是一个量级的,但是减少了需多不必要的重复操作,所以时间复杂度更优,具体思路如下:

  • 这个方法将求解源字符串以第s[index]字符结尾的子串的最长匹配子串长度 longestSubset[index],于是可以得到如下的状态转移方程:
    l o n g e s t S u b s e t [ i n d e x ] = longestSubset[index] = longestSubset[index]=
    0 , 0, 0s[index](
    l o n g e s t S u b s e t [ i n d e x − 2 ] + 2 , longestSubset[index-2]+2, longestSubset[index2]+2s[index-1](s[index])
    l o n g e s t S u b s e t [ i n d e x − 1 ] + 2 + l o n g e s t S u b s e t [ i n d e x − 2 − l o n g e s t S u b s e t [ i n d e x − 1 ] ] , longestSubset[index-1]+2+longestSubset[index-2-longestSubset[index-1]], longestSubset[index1]+2+longestSubset[index2longestSubset[index1]]s[index-1](s[index-1-longestSubset[index-1]](
    (需要注意一下下标越界的判断,即在每次按下标访问时提前判断下标是否合法,不合法就用0代替该值或者跳过该步骤,具体判断见代码实现)
  • 当我们完成了整个 longestSubset[index] 数组的求解,遍历它取最大值就是本题答案。
算法步骤

方法一:

遍历字符串
	若当前字符为‘(’
		将当前字符位置作为元素入栈 leftParenthese;
	若当前字符为‘)’
		若 leftParenthese 长度为0
			若当前子串长度length 大于 整个当前最长匹配子串长度maxLength,用length更新maxLength;
		若 leftParenthese 长度大于0
			栈顶元素出栈 leftParenthese ;当前子串长度length 加二;
遍历栈 leftParenthese 
	使用栈中元素值(代表未匹配左括号在源字串的位置)对最后一个子串进行分割,将最后一个子串分割为n份,并求出其中长度最大值 ,记为 maxLength1;
比较 maxLength 和 maxLength1,取其中大的作为结果;

方法二:

longestSubset[length(s)] 数组初始化为{};
遍历字符串s,当前下标记为index
	若当前字符为‘(’
		longestSubset[index] 设为 0 ;
	若当前字符为‘)’
		若前一个字符为‘(’
			longestSubset[index] 设为 longestSubset[index-2]+2;
		若前 index-1-longestSubset[index-1] 个字符为 `(`
			longestSubset[index] 设为 longestSubset[index-1]+2+longestSubset[index-2-longestSubset[index-1]];
	若此步求出的 longestSubset[index] 大于当前的maxLength,替换maxlength的值;
复杂度分析

方法一:
使用两个循环遍历,每个循环遍历数组的长度最多是n,所以算法是O(2n)的,也就是O(n)的时间复杂度;
空间复杂度上,使用了若干个临时变量和一个栈,栈中元素不超过n个,所以空间复杂度是O(n)的;

方法二:
使用一个循环遍历,循环遍历数组的长度最多是n,所以算法是O(n)的,也就是O(n)的时间复杂度;
空间复杂度上,使用了若干个临时变量和一个长度为n的数组,所以空间复杂度是O(n)的;

虽然理论上这两个算法都是O(n)的,但是前一个方法用了两次遍历(两个O(n)),并且这两次遍历是不能合为一步的,因此方法一的复杂度要比方法二大一倍,并且由于栈操作比数组操作开销更大,因此方法一的复杂度实际上比方法二的复杂度超过2倍,从后面结果分析的 runTime 可以看出这一点:方法一RunTime = 12ms,方法二RunTime = 4ms。

代码实现&结果分析

方法一:

  • 代码实现
    class Solution {
    public:
        int longestValidParentheses(string s) {
            stack<int> leftParentheses;
            int maxCount = 0;
            int count = 0;
            int index = 0;
            int start = 0;
            int length = s.length();
            while (index != length) {
            	if (s[index] == '(') {
            		leftParentheses.push(index-start);
            	} else {
            		if (leftParentheses.size() == 0) {
            			maxCount = maxCount > count ? maxCount : count;
            			count = 0;
            			start = index+1;
            		} else {
            			leftParentheses.pop();
            			//cout << s.substr(index) << endl;
            			count+=2;
            		}
            	}
            	index++;
            }
            if (maxCount < count) {
            	if (leftParentheses.size() > 0) {
            		int end = 0;
            		int start = count+leftParentheses.size();
            		int tempCount = 0;
            		while (!leftParentheses.empty()) {
            			end = start;
            			start = leftParentheses.top();
            			int temp = end - start - 1;
            			tempCount = temp > tempCount ? temp : tempCount;
            			leftParentheses.pop();
            		}
            		tempCount = tempCount > start ? tempCount : start;
            		maxCount = maxCount > tempCount ? maxCount : tempCount;
            	} else {
            		maxCount = count;
            	}
            }
            return maxCount;
        }
    };
    
  • 提交结果:
    在这里插入图片描述

方法二:

  • 代码实现:
    class Solution {
    public:
        int longestValidParentheses(string s) {
        	if (s.length() == 0) return 0;
    
        	int maxCount = 0;
        	int length = s.length();
        	int longestSubset[length] = {};
        	
        	for (int i = 1; i < length; ++i) {
        		if (s[i] == ')') {
        			if (s[i-1] == '(') {
        				longestSubset[i] = 2 + (i >= 2 ? longestSubset[i-2] : 0);
        			} else if (i-1-longestSubset[i-1] >= 0 && s[i-1-longestSubset[i-1]] == '(') {
        				longestSubset[i] = 2 + longestSubset[i-1] + (
        					i-2-longestSubset[i-1] >= 0 ? 
        						longestSubset[i-2-longestSubset[i-1]] : 0
        					);
        			}
        			maxCount = maxCount > longestSubset[i] ? maxCount : longestSubset[i];
        		}
        	}
    
        	return maxCount;
        }
    };
    
  • 结果分析:
    虽然理论上这两个算法都是O(n)的,但是前一个方法用了两次遍历(两个O(n)),并且这两次遍历是不能合为一步的,因此方法一的复杂度要比方法二大一倍,并且由于栈操作比数组操作开销更大,因此方法一的复杂度实际上比方法二的复杂度超过2倍,从runTime可以看出这一点:方法一RunTime = 12ms,方法二RunTime = 4ms。
    在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值