当前位置: 代码迷 >> 综合 >> 回溯算法复习(三) leetcode 22. Generate Parentheses 括号匹配拼接问题分析
  详细解决方案

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

热度:72   发布时间:2023-10-19 11:42:21.0

继上一篇博客 手机号码字符集枚举组合的递归实现分析 后,我们来继续学习回溯算法。同样采用递归结构去分析解决问题,在此先据题对回溯算法进行进一步分析,而后结合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 maxgenerateParenthesis(results, result + "(", left + 1, right, max);}if (right < left) {//append ")", , which numbers must be limited by leftgenerateParenthesis(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 recursionsb.setLength(sb.length() - 1);}if (right < left) {sb.append(")");generateParenthesis(results, sb, left, right + 1, max);sb.setLength(sb.length() - 1);}}

测试结果:

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

 

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

pattern

Status

Runtime

Memory

Language

String Accepted 1 ms 36.1 MB java
StringBuilder Accepted 1 ms 38.6 MB java


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

总结:

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

2.5h written for this essay