LeetCode_22.括号生成
难度:中等
![]()
目录
作者原始思路
分治算法
class Solution {
public List<String> generateParenthesis(int n) {
var list = new ArrayList<String>();
var sChars = new char[n*4];
combine(list,1,n,sChars,0,sChars.length-1,0,sChars.length-1);
return list;
}
/**
* @param list 集合
* @param i 计数器
* @param n 一共的对数
* @param sChars 数组
* @param leftRotation 最左边的下标(
* @param rightRotation 最右边的下标)
* @param addIndex1 待插入的第一个下标
* @param addIndex2 待插入的第二个下标
*/
public void combine(ArrayList<String> list,int i,int n,char[] sChars,int leftRotation,int rightRotation,int addIndex1,int addIndex2) {
var left = leftRotation;
var right = rightRotation;
if (i == 1) {
left = (leftRotation + rightRotation)/2;
right = (leftRotation + rightRotation)/2 + 1;
sChars[left] = '(';
sChars[right] = ')';
if (i == n) {
list.add(new String(sChars,left,2*i));
}
combine(list,i+1,n,sChars,left-1,right+1,left-1,right+1);
combine(list,i+1,n,sChars,left,right+2,right+1,right+2);
}else {
if (i <= n) {
sChars[addIndex1] = '(';
sChars[addIndex2] = ')';
var newItem = new String(sChars,left,2*i);
if (i == n) {
list.remove(newItem);
list.add(newItem);
}
combine(list,i+1,n,sChars,left-1,right+1,left-1,right+1);
combine(list,i+1,n,sChars,left,right+2,right+1,right+2);
combine(list,i+1,n,sChars,left-2,right,left-2,left-1);
}
}
}
}
存在问题
反省
- 在这道题中, 相信大家可以轻而易举的发现应该用递归的思想去解决该问题,递归的思想中,又存在着分治,快排,深度优先等算法思想
- 在我的视角中,我第一反应是用分治算法,即将问题通过递归的方式进行简单化,然后再从简单问题找出解决所有问题的通用方法
- 通过分治对于两个括号的解法,有以下三种放置方法,即在在"()"左边或右边放置"()",或者在"()"外面包裹一层类似于"(())"
- 但是,这种思想到了第四个括号就会发生错误,原因是解决方法中.我们只针对一个括号进行左中右放置,没有考虑到第二个括号也有这种情况,即"(())(())"是无法推出来的,这样的话,反而使我们的算法更加的复杂,所以不推荐用分治算法
最优解法-回溯算法(法一)
题目分析
- 题目中要求的是有效的括号组合,即我们要找到"()"的形式,这暗示了我们,一定要先放左括号再放右括号
- 如果我们先放右括号,那么他就没有能与他匹配的左括号,这个组合一定是错的
- 不论算法走到哪一步都要遵循这个规则,否则就是错误的,
- 这个重要的结论影响着我们算法的策略!
思路分析
- 建立一个方法,通过递归回溯的方式去放置括号
- 策略如下
- 当剩余的括号相同时,我们必须先放左括号,深入递归
- 当剩余括号中左边的少于右边的时候,我们优先使用左边的,再使用右边的
- 如果发现左右剩余的括号都为0了,说明放完了,放完了一定就是合法的,就可以加入集合了
代码实现
class Solution {
public List<String> generateParenthesis(int n) {
var list = new ArrayList<String>();
combine(list,n,n,new StringBuilder());
return list;
}
public static void combine(ArrayList<String> list,int numOfLeft,int numOfRight,StringBuilder strBul) {
if (numOfLeft == numOfRight && numOfRight == 0) {
list.add(strBul.toString());
return;
}
if (numOfLeft == numOfRight) {
strBul.append("(");
combine(list,numOfLeft-1,numOfRight,strBul);
strBul.deleteCharAt(strBul.length() - 1);
}else if (numOfLeft < numOfRight) {
if (numOfLeft > 0) {
strBul.append("(");
combine(list,numOfLeft-1,numOfRight,strBul);
strBul.deleteCharAt(strBul.length() - 1);
}
strBul.append(")");
combine(list,numOfLeft,numOfRight-1,strBul);
strBul.deleteCharAt(strBul.length() - 1);
}
}
}
代码分析
- 1.参数列表
- list(集合,专门用于放结果的)
- numOfLeft(左括号剩余的个数)
- numOfRight(右括号剩余的个数)
- strBul(字符串拼接的媒介)
- 2.如果左边和右边剩余的括号数都为0,说明用完了,所得到的结果放入集合中
- 3.根据策略,当左括号与右括号剩余个数一样的时候,我们优先放左括号,即字符串拼接
- 4.因为用了左括号,接下来就可以递归去添加,下一个括号了
- 注意:因为我们放置了一个左括号,所以左括号个数要减1,即numOfLeft-1
- 为什么要执行strBul.deleteCharAt(strBul.length()-1)这个代码
- 原因:举一个最简单的例子,如果我们最终得到了该结果"((()))",没有这个操作,回溯后这个结果不会有任何变化,下一次拼接只能在后面添加,达不到想要的结果
- 5.当左括号剩余个数少于右括号的时候,根据策略,如果左括号没有放完,即numOfLeft>0,我们就继续放左括号,否则放右括号
官方解法-回溯算法(法二)
代码实现
class Solution {
public List<String> generateParenthesis(int n) {
var list = new ArrayList<String >();
combine(list,0,0,new StringBuilder(),n);
return list;
}
public void combine(ArrayList<String> list,int numOfLeft,int numOfRight,StringBuilder strBul,int max) {
if (strBul.length() == max * 2) {
list.add(strBul.toString());
return;
}
if (numOfLeft < max) {
strBul.append("(");
combine(list,numOfLeft+1,numOfRight,strBul,max);
strBul.deleteCharAt(strBul.length() - 1);
}
if (numOfLeft > numOfRight) {
strBul.append(")");
combine(list,numOfLeft,numOfRight+1,strBul,max);
strBul.deleteCharAt(strBul.length() - 1);
}
}
}
代码分析
- 1.参数列表
- numOfLeft(已放置的左括号个数)
- numOfRight(已放置的右括号个数)
- max(括号的总对数) 其他都一致
- 2.如果字符串拼接后,长度等于max的两倍,说明拼接完了,所有的括号都用完了,此刻可以加入集合中,结束方法
- 3.对于该算法有第二种策略
- 3.1如果发现左括号个数没有放满,即numOfLeft < max,我们就放左括号,并递归
- 注意:因为用了一个左括号,所以递归的时候要让numOfLeft+1,千万别++,不然回溯的效果就消失了(相信大家都懂,不懂再咨询)
- 3.2如果发现左括号用的比右括号多,我们就放右括号,并递归放置
- 为什么要有这个if判断,没有行不行?>=行不行
- 不行!原因是如果没有这个判断,就会出现"(())))"的右括号比左括号多的情况,而且等于时,再放一个右括号,就永远都找不到与之匹配的左括号,肯定是错误的结果,所以都不行
- 3.1如果发现左括号个数没有放满,即numOfLeft < max,我们就放左括号,并递归
结论
通过比较上述的两个方法,我们得出了以上的结果,所以,法一是优于法二的,而且在理解上,个人认为法一更容易理解,对此,我总结以下必须掌握的几个点
1.回溯算法代码的理解
2.放置括号的策略
3.分治算法的适用场景
如果有不懂的地方可以参考博主的迷宫回溯问题,里面有对回溯算法的深度剖析