LeetCode琅琊榜第四层-走出循环的逻辑怪圈(括号生成)

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判断,没有行不行?>=行不行
      • 不行!原因是如果没有这个判断,就会出现"(())))"的右括号比左括号多的情况,而且等于时,再放一个右括号,就永远都找不到与之匹配的左括号,肯定是错误的结果,所以都不行

结论

        通过比较上述的两个方法,我们得出了以上的结果,所以,法一是优于法二的,而且在理解上,个人认为法一更容易理解,对此,我总结以下必须掌握的几个点

        1.回溯算法代码的理解

        2.放置括号的策略 

        3.分治算法的适用场景

        如果有不懂的地方可以参考博主的迷宫回溯问题,里面有对回溯算法的深度剖析

  • 20
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 35
    评论
评论 35
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爪哇土著、JOElib

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值