回溯算法复习(三) leetcode 22. Generate Parentheses 括号匹配拼接问题分析

继上一篇博客 手机号码字符集枚举组合的递归实现分析 后,我们来继续学习回溯算法。同样采用递归结构去分析解决问题,在此先据题对回溯算法进行进一步分析,而后结合java相关类实现特性对传参过程中关于String与StringBuilder形参进行讨论分析,最后和大家一起思考思考算法优化问题。

Leetcode原题如下:leetcode 22. Generate Parentheses

Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.

For example, given n = 3, a solution set is:

[
  "((()))",
  "(()())",
  "(())()",
  "()(())",
  "()()()"
]

分析:

还是典型的组合问题,在此首先想到是利用String装配结果传参的递归枚举方法。而后在论坛上发现发现另一种基于StringBuilder的传参方法(关于String与StringBuilder、StringBuffer的区别就不说了)。从回溯算法的分析学习上来讲,个人觉得SringBuilder的结果回切拼接方式会更加有助于学习理解(虽然在本题中会更难理解一点点)。下面对两种签名的递归方式分别进行分析:

方式一 String:generateParenthesis(List<String> results, String result, int left, int right, int max)

在此用result来保存每一个有效递归分支的结果,results当然就是保存最终结果集的啦!Left在此代表左括号个数(right同理),max作为限定组合括号对数用于递归终止条件判断中(在满足left==right==max时对结果进行装填记录并终止递归,此时得到一个符合期望的结果元素)。Ok!首先我们知道,按照从左往右的书写格式习惯,正确的括号书写需要先写左括号而后写右括号,也就是要满足括号匹配基本规则(嗯?你说这是废话?别急,请听我慢慢道来)。据上述,我们可以用左括号的个数限定右括号的个数,用调用入口给定括号对数max限定左括号对数,如此一来就完成了递归执行体的代码书写啦。

结合上述,在基于String类型的result参数传递下,每次都需要进行一个字符串拼接。根据java中String的特性及参数传递相关知识我们知道,String是对char[]的一个简单封装,并没有提供动态内存扩展;在每一次递归传参时都会在创建一个临时String变量用于保存新的result,频繁的空间申请损耗了不必要的资源。

代码如下:

public static List<String> generateParenthesis(int n) {
		List<String> results = new ArrayList();
		generateParenthesis(results, "", 0, 0, n);
		return results;
	}

	public static void generateParenthesis(List<String> results, String result, int left, int right, int max) {
		if (left == max && right == max) {
			results.add(result);
			return;
		}
		if (left < max) {//append "(", which numbers must be limited by max
			generateParenthesis(results, result + "(", left + 1, right, max);
		}
		if (right < left) {//append ")", , which numbers must be limited by left
			generateParenthesis(results, result + ")", left, right + 1, max);
		}
	}

 

方式二 StringBilder:generateParenthesis(List<String> results, StringBuilder sb, int left, int right, int max)

在此为单线程代码设计,故而自然而然的想到了StringBuilder(若不了解这些基本操作不妨去网上查查String、StringBuilder与StringBuffer的对比!),代码实现上与上一种方式相对比,除了StringBuilder型的result传参时字符串拼接方式在代码层面上的不同外,其他都相同。

在此对sb.setLength(sb.length()-1)代码进行一个分析:或许有的小伙伴会有疑问,“为什么要多了这行代码呢?”。

首先,这行代码的功能就是通过重设sb字符串长度的方式对其完成末尾字符的剪切,也就是丢弃最后面append的那个字符。为什么要在递归调用完成后对它进行剪切呢?先举个例子:就比如说在某次调用中,假设在进行append之后result值为“()”,也就是在本次递归运算中将“)”加入了result中,而对于append之前的“(”来说,其下一次递归运算时可能成立的结果有“((”及“()”两种,为了得到这样的结果我们需要在完成一次递归调用后进行result末位剪切(类似于走迷宫算法的绝路出栈再试探,也就是回溯),个人理解其就是一个回溯思想的体现。

代码如下:

public static List<String> generateParenthesis(int n) {
	List<String> results = new ArrayList();
	generateParenthesis(results, new StringBuilder(), 0, 0, n);
	return results;
}

public static void generateParenthesis(List<String> results, StringBuilder sb, int left, int right, int max) {
		if (left == max && right == max) {
			results.add(sb.toString());
		}
		if (left < max) {
			sb.append("(");
			generateParenthesis(results, sb, left + 1, right, max);
			// trim a new char for next selection to contain,which is branch of other recursion
			sb.setLength(sb.length() - 1);
		}
		if (right < left) {
			sb.append(")");
			generateParenthesis(results, sb, left, right + 1, max);
			sb.setLength(sb.length() - 1);
		}
	}

测试结果:

 

以下是两种result类型传参方式在leetcode上的ac资源损耗情况:

pattern

Status

Runtime

Memory

Language

StringAccepted1 ms36.1 MBjava
StringBuilderAccepted1 ms38.6 MBjava


发现两种方式的运行时间趋于一致,而相对来说StringBuilder方式反而在内存损耗上略高于String方式。运行时间上相近也不难理解,主要是String方式损耗在内存申请上的时间与StringBuilder进行末位剪切运算时间相抵消了(严格来说,StringBuilder在toString()及其他额外的运算也需要考虑进去)。

总结:

综合前面两篇博客来看,在采用递归结构解决问题时其实有暴力枚举的嫌疑(确信无疑)。在回溯算法中是可以对算法边界进行剪切以期达到更高处理效率的;留些思考:在此如何对算法进行优化呢?用空间换时间的方式怎么样,如何平衡?如何设计出时间和空间都让人满意的算法呢?思考会增加人的幸福感哦!

2.5h written for this essay

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值