LeetCode算法题6:递归和回溯2

本文详细探讨了递归和回溯算法在解决组合问题上的应用,如全排列II、组合总和、子集等问题。通过构建n叉树和二叉树模型,解析了各种解法的思路和去重策略,例如利用标记数组、搜索起点调整和剪枝条件等方法。同时,展示了如何通过递归和循环实现这些算法,并提供了具体代码示例,帮助读者深入理解递归回溯法在组合问题中的核心原理。
摘要由CSDN通过智能技术生成


前言

      递归和回溯续:包括有全排列II 、组合总和、组合总和II、子集、子集II、第 k 个排列、复原IP地址。

一、全排列II

      题目链接:https://leetcode-cn.com/problems/permutations-ii/

      题目描述:给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

仿照全排列(n 叉树)

      和之前的全排列相比,这里的区别是数字序列包含了重复元素。一个很无脑的方法是将之间全排列解法中的 List 转换为 Set,去掉最后排列结果中的重复序列即可。参考算法如下:

	LinkedList<Integer> tmp=new LinkedList<>();
    Set<List<Integer>> ans=new HashSet<>();//改为 Set 存储
    boolean[] flag;
    public List<List<Integer>> permuteUnique(int[] nums) {
        int len=nums.length;
        flag=new boolean[len];//由于每次选择一个元素之后,该元素就不能再被选取了,所以需要标记状态
        
        solve(nums,len-1);
        return new ArrayList<>(ans);
    }
    public void solve(int[] nums,int end) {
        if(tmp.size()==end+1){//得到一次排列时,保存结果
            ans.add(new LinkedList<>(tmp));
            return;
        }
        for(int i=0;i<=end;i++){//依次选择每一个元素,由于有标记数组存在,所以此处循环每次都从下标 0 开始。
            if(flag[i]==false){
                tmp.addLast(nums[i]);
                flag[i]=true;
                solve(nums,end);
                flag[i]=false;
                tmp.removeLast();
            }
        }
    }

剪枝(去掉重复的结果)

      以数字序列 【1 1 3】为例,仿照之前的全排列构建回溯过程的选择树如下:

初始集合为空
第一轮选择一个元素113
第二轮选择第二个元素1 11 31 11 33 13 1
第三轮选择第三个元素1 1 31 3 11 1 31 3 13 1 13 1 1

      可以发现重复的结果出现在:第一轮中选择第二个 1 得到的最终结果和第一个 1 是一模一样的,均为 【1 1 3】和【1 3 1】;在第一轮选择 3 的情况下,第二轮的两次选择都是 1,最终结果也产生了重复。

      所以剪枝的条件,也即约束条件为:当数组采用升序排列时,如果出现当前元素和上一个元素相等时,就没有必要继续递归遍历了;但这还不够,这样在第二轮就永远不会出现【1 1】序列了。如果第一个 1 被选取了之后,第二个 1 也被选取,这是重复结果的第一次出现,这种情况应该被允许发生;对应的重复结果的第二次出现就不允许发生了,比如在第一轮选择第二个 1 时,第一个 1 未被选取(状态为 false),由于之后的递归遍历得到的都是重复结果,所以这种情况跳过(剪枝)。

      所以给出的剪枝条件(约束条件)为:

if(i>0&&(nums[i]==nums[i-1])&&flag[i-1]==false)
	continue;

      它表示的含义为:仅允许重复的排列结果第一次出现,后面会重复出现的递归遍历被跳过(剪去)。

      参考算法如下:

	LinkedList<Integer> tmp=new LinkedList<>();
    List<List<Integer>> ans=new LinkedList<>();
    boolean[] flag;
    public List<List<Integer>> permuteUnique(int[] nums) {
        int len=nums.length;
        flag=new boolean[len];//由于每次选择一个元素之后,该元素就不能再被选取了,所以需要标记状态
        Arrays.sort(nums);
        solve(nums,len-1);
        return ans;
    }
    public void solve(int[] nums,int end) {
        if(tmp.size()==end+1){//得到一次排列时,保存结果
            ans.add(new LinkedList<>(tmp));
            return;
        }
        for(int i=0;i<=end;i++){//依次选择每一个元素,由于有标记数组存在,所以此处循环每次都从下标 0 开始。
            if(flag[i]==false){
                if(i>0&&(nums[i]==nums[i-1])&&flag[i-1]==false)
                    continue;
                tmp.addLast(nums[i]);
                flag[i]=true;
                solve(nums,end);
                flag[i]=false;
                tmp.removeLast();
            }
        }
    }

      虽说约束条件改为 if(i>0&&(nums[i]==nums[i-1])&&flag[i-1]) 也是可以的,它保存的是重复排列的最后一次出现结果,它和保存第一次出现结果正好相反,并且也不太好理解,不建议采用这种做法。

二、组合总和

      题目链接:https://leetcode-cn.com/problems/combination-sum/
      题目描述:给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

一、初始解法(n 叉树):

      直接干:

    List<List<Integer>> ans=new ArrayList<>();
    List<Integer> tmp=new LinkedList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        solve(candidates,target);//先排序
        return ans;
    }
    void solve(int[] candi,int target){
        if(target==0){
            ans.add(new LinkedList<>(tmp));
            return;
        }

        for(int i=0;i<candi.length;i++){
            if(candi[i]>target)
                break;//由于已经按照升序排序了,直接break即可。
            tmp.add(candi[i]);
            solve(candi,target-candi[i]);
            tmp.remove(tmp.size()-1);
        }
    }

      然而初始解法的答案存在重复情况,对于示例 candidates = [2,3,6,7], target = 7,此解法的答案为:[2, 2, 3] [2, 3, 2] [3, 2, 2] [7],这是由于在搜索过程产生了重复,具体的可以仿照上一题画出递归树来比对。关于如何去重有两种方法:

1,采用 Set 去重

      参考代码如下:(缺点是效率太慢,勉强能用)

	Set<List<Integer>> ans=new HashSet<>();
    List<Integer> tmp=new LinkedList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        solve(candidates,target);
        List<List<Integer>> re=new ArrayList<>();
        for(List l:ans)
            re.add(l);
        return re;
    }
    void solve(int[] candi,int target){
        if(target==0){
            List<Integer> t=new LinkedList<>(tmp);
            Collections.sort(t);//这里需要先排序,从而在添加进集合的时候去重。
            ans.add(t);
            return;
        }

        for(int i=0;i<candi.length;i++){
            if(candi[i]>target)
                break;
            tmp.add(candi[i]);
            solve(candi,target-candi[i]);
            tmp.remove(tmp.size()-1);
        }
    }

2,在递归搜索的时候去重(推荐解法)

      参考文章:https://leetcode-cn.com/problems/combination-sum/solution/hui-su-suan-fa-jian-zhi-python-dai-ma-java-dai-m-2/,本题的去重方式采用的是设置搜索起点。

      初始解法答案为[2, 2, 3] [2, 3, 2] [3, 2, 2] [7]。约定:第一轮表示选择一个元素的时候,第二轮选择第二个元素,第三轮选择第三个元素,第四轮选择第四个元素。

初始解法的遍历过程:

      当第一轮选择 2 时 ,接下来的遍历为:第二轮选2、3、6、7。… 第三轮选2、3、6、7。… 第四轮选2、3、6、7。 …

      当第一轮选择 3 时,要么还是按照之前的遍历思路(第二轮选2、3、6、7;… 第三轮选2、3、6、7;… 第四轮选2、3、6、7 …),要么就舍弃第一轮已经选过的 2,新的遍历方式(第二轮选3、6、7;… 第三轮选3、6、7;… 第四轮选3、6、7 …)。

新方式的遍历过程:

      okk,感觉这样好像有点合理哈,因为我们的目的是求一些元素的组合,而新的遍历方式并不会漏掉任何组合方式。也刚好解决了初始解法中重复问题(能够轻易的发现[3, 2, 2]被排除掉了)。

      当第一轮选择 2 时,第二轮选择2、3、6、7,第三轮选择 2、3、6、7,第四轮 2、3、6、7时:目标集合[2, 2, 3]表示第一轮选择 2 、第二轮选择 2 、第三轮选择 3 ;继续执行直到状态为:第一轮选择 2 、第二轮选择 3 时。

      如果按照旧的遍历方式,第三轮仍然选 2,得到了重复集合[2, 3, 2]。但是按照新的遍历方式,在第二轮选择 3 时,第三轮也会从 3 开始选择,所以在此也跳过了重复的集合选取,[2, 3, 2]就会被排除掉了。

      由此可得到的算法参考如下:(采用 begin 变量标记遍历的起点下标)

	List<List<Integer>> ans=new ArrayList<>();
    List<Integer> tmp=new LinkedList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        solve(candidates,target,0);
        return ans;
    }
    void solve(int[] candi,int target,int begin){
        if(target==0){
            ans.add(new LinkedList<>(tmp));
            return;
        }

        for(int i=begin;i<candi.length;i++){
            if(candi[i]>target)
                break;
            tmp.add(candi[i]);
            solve(candi,target-candi[i],i);//一旦到了下标i处,打死也不会选择 下标小于i处 的元素了
            tmp.remove(tmp.size()-1);
        }
    }

引申(组合问题)

组合问题的解法-n 叉树

       参考上面的新遍历方式的代码可以得到的一种求组合(在candidates中求 k 个数的组合)的解法如下:

	List<List<Integer>> ans=new ArrayList<>();
    List<Integer> tmp=new LinkedList<>();
    public List<List<Integer>> combine(int[] candidates, int k) {
        solve(candidates,k,0);
        return ans;
    }
    void solve(int[] candi,int k,int begin){
        if(tmp.size()==k){
            ans.add(new LinkedList<>(tmp));
            return;
        }

        for(int i=begin;i<candi.length;i++){
        	if(tmp.size()+candi.length-i)<k)	
        		break; //这里添加了剪枝条件
            tmp.add(candi[i]);
            solve(candi,k,i+1);//此处同上,选择了 i 处元素之后之后就不会选择下标小于等于i处的元素了
            tmp.remove(tmp.size()-1);
        }
    }
组合问题的解法-二叉树

      参考博客:https://blog.csdn.net/Little_ant_/article/details/123676777,代码如下:

    List<List<Integer>> ans=new LinkedList<>();
    List<Integer> tmp=new LinkedList<>();
    public List<List<Integer>> combine(int n, int k) {
        solve(1,n,k);
        return ans;
    }
    public void solve(int cur,int end,int k){
        if(tmp.size()+(end-cur+1)<k)
            return;

        if(tmp.size()==k){ //碰到每一种成功的情况时应该保存                      
            ans.add(new LinkedList<>(tmp));
            return;
        }

        tmp.add(cur); //选取当前 cur 的情况								  
        solve(cur+1,end,k);
        tmp.remove(tmp.size()-1); //不选取的情况
        solve(cur+1,end,k);
    }
组合问题总结

      解法1 如下面的二叉树来示意 (n=4,k=2):(不想画图,凑合一看)
                                                                        【 】
                                          【1】                                                                【】
                                 【1,2】    【1】                                           【2】                                         【】
                                         【1,3】    【1】                           【2,3】    【2】                  【3】                  【】
                                                  【1,4】   【1】                              【2,4]    【2】    【3,4】 【3】    【4】   【】
      解法2 如下面的 n 叉树所示意:
                                                                                                    【 】
                                          【1】                                          【2】                          【3】            【4】
                         【1,2】    【1,3】     【1,4】           【2,3】      【2,4】                【3,4】
      以上两种示意图对应两种不同的解法思路。务必领会!

二、另一种解法(二叉树):

      组合问题解法1的思路参考上图,本题也可以采用类似的方式,直接上代码:

	List<List<Integer>> ans=new ArrayList<>();
    List<Integer> tmp=new LinkedList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        solve(candidates,target,0);
        return ans;
    }
    void solve(int[] candi,int target,int cur){//cur 为当前元素的下标
        if(target==0){
            ans.add(new LinkedList<>(tmp));
            return;
        }
        if(cur==candi.length)//退出条件1
            return;
        if(candi[cur]>target)//退出条件2
            return;
        //选择当前元素,target减小,cur由题意可知,应不变。    
        tmp.add(candi[cur]);
        solve(candi,target-candi[cur],cur);
        //不选择当前元素,target不变,cur应加一。
        tmp.remove(tmp.size()-1);
        solve(candi,target,cur+1);
    }

      这种解法不会造成有重复结果。因为,一个 solve 中的下标为 start,另一个 solve 中的下标为 start+1,这也表明了,一旦访问到下标 start 处,就再也不会遇到下标小于 start 的元素了。这和初始解法中的推荐解法含义是一样的(用 begin 变量标记下一轮起始元素)
little_ant_
      对应的代码为(稍加修改后):

    LinkedList<Integer> tmp=new LinkedList<>();
    List<List<Integer>> ans=new LinkedList<>();
    boolean[] flag;
    public List<List<Integer>> permute(int[] nums) {
        int len=nums.length;
        flag=new boolean[len];//由于每次选择一个元素之后,该元素就不能再被选取了,所以需要标记状态
        
        solve(nums,len-1);
        return ans;
    }
    public void solve(int[] nums,int end) {
        if(tmp.size()==end+1){//得到一次排列时,保存结果
            ans.add(new LinkedList<>(tmp));
            return;
        }
        for(int i=0;i<=end;i++){//依次选择每一个元素,由于有标记数组存在,所以此处循环每次都从下标 0 开始。
            if(flag[i]==false){
                tmp.addLast(nums[i]);
                flag[i]=true;
                solve(nums,end);
                flag[i]=false;
                tmp.removeLast();
            }
        }
    }

三、组合总和II

      题目链接:https://leetcode-cn.com/problems/combination-sum-ii/

      题目描述:给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次 。

注意:解集不能包含重复的组合。

解法1(n 叉树)

      初始代码为:

	List<List<Integer>> ans=new ArrayList<>();
    List<Integer> tmp=new LinkedList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        solve(candidates,target,candidates.length,0);
        return ans;
    }
    public void solve(int[] c,int target,int len,int begin){
        if(target==0){
            ans.add(new LinkedList<>(tmp));
            return;
        }
        for(int i=begin;i<len;i++){
            if(c[i]>target)
                break;  //这里和前面的 sort 对应,如果不排序的话应该为 continue
            tmp.add(c[i]);
            solve(c,target-c[i],len,i+1);//i+1表示不对当前元素重复选取。
            tmp.remove(tmp.size()-1);
        }
    }

      但仍会造成重复,重复的原因在于 candidates 数组中本身存在重复元素,比如:[10,1,2,7,6,1,5] ,这里有两个 1 存在。那么如何剪枝去重呢?这里可以先画一画递归搜索树,对 candidates 排完序之后,两个 1 接连排列,在初始代码中:前面那个 1 的搜索空间包含了后面那个 1 的搜索空间,这就是重复发生的原因。解决方法:如果前面那个 1 搜索结束之后,那么后面那个 1 直接跳过,在同一层中的重复数字永远只取第一个。这一点和 全排列II 剪枝非常类似。得到了最终代码如下:

	List<List<Integer>> ans=new ArrayList<>();
    List<Integer> tmp=new LinkedList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        boolean[] visited=new boolean[candidates.length];//新加入 visited 数组
        solve(candidates,target,candidates.length,0,visited);
        return ans;
    }
    public void solve(int[] c,int target,int len,int begin,boolean[] visited){
        if(target==0){
            ans.add(new LinkedList<>(tmp));
            return;
        }
        for(int i=begin;i<len;i++){//如果一个 1 被遍历完之后,其他 1 也不能被访问了。
            if(c[i]>target)
                break;
            if(i>0&&c[i]==c[i-1]&&visited[i-1]==false)//false 表示重复元素的第一个已遍历结束,后面的重复元素都需要跳过
                continue;
            visited[i]=true;
            tmp.add(c[i]);
            solve(c,target-c[i],len,i+1,visited);
            tmp.remove(tmp.size()-1);
            visited[i]=false;
        }
    }

      补充:上述代码将 if(i>0&&c[i]==c[i-1]&&visited[i-1]==false) continue; 修改成为 if (i > begin && candidates[i] == candidates[i - 1]) continue; ,也会具有相同的作用。

解法2(二叉树)

      初始代码为:

	List<List<Integer>> ans=new ArrayList<>();
    List<Integer> tmp=new LinkedList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        solve(candidates,target,candidates.length,0);
        return ans;
    }
    public void solve(int[] c,int target,int len,int cur){
        if(target==0){
            ans.add(new LinkedList<>(tmp));
            return;
        }
        if(cur==len)
            return;
        if(c[cur]>target)
            return;
        tmp.add(c[cur]);
        solve(c,target-c[cur],len,cur+1);
        tmp.remove(tmp.size()-1);
        solve(c,target,len,cur+1);
        
    }

      需要去重,保证在同一层中的相同元素永远只取第一个。最终代码如下:

	List<List<Integer>> ans=new ArrayList<>();
    List<Integer> tmp=new LinkedList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        solve(candidates,target,candidates.length,0);
        return ans;
    }
    public void solve(int[] c,int target,int len,int cur){
        if(target==0){
            ans.add(new LinkedList<>(tmp));
            return;
        }
        if(cur==len)// 退出条件1
            return;
        if(c[cur]>target)// 退出条件2
            return;

        tmp.add(c[cur]);
        solve(c,target-c[cur],len,cur+1);

        int a=tmp.get(tmp.size()-1),i;//若当前层有重复元素,那 a 为第一个,并且此处 a 已经被使用过了
        tmp.remove(tmp.size()-1);
        for(i=cur+1;i<len;i++)//同一层的下一个元素值不能为 a 了,
            if(c[i]!=a)
                break;

        solve(c,target,len,i);
    }

四、子集

      题目链接:https://leetcode-cn.com/problems/subsets/

      题目描述:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

解法1(二叉树)

      求集合的子集,本题需要熟悉二叉递归搜索树,因为该树的所有叶子结点就是本题的答案,将所有叶子结点加入到集合并返回。当 nums = { 1, 2, 3 };时,该树如下图所示(画图有点累…):

little_ant_

      代码如下:

	List<List<Integer>> ans=new LinkedList<>();
    Deque<Integer> tmp=new LinkedList<>();
    public List<List<Integer>> subsets(int[] nums) {
        solve(nums,0);
        return ans;
    }
    public void solve(int[] nums,int cur){
        if(cur==nums.length){
            ans.add(new LinkedList<>(tmp));
            return;
        }
        tmp.add(nums[cur]);
        solve(nums,cur+1);
        tmp.removeLast();
        solve(nums,cur+1);
    }

解法2(n 叉树)

      同理,n 叉递归搜索树的所有结点是本题的答案,所有结点加入集合直接返回即可。当 nums = { 1, 2, 3 };时,该树如下图所示:

little_ant_

      代码如下:

	List<List<Integer>> ans=new LinkedList<>();
    Deque<Integer> tmp=new LinkedList<>();
    public List<List<Integer>> subsets(int[] nums) {
        solve(nums,0);
        return ans;
    }
    public void solve(int[] nums,int begin){
        ans.add(new LinkedList<>(tmp));

        for(int i=begin;i<nums.length;i++){
            tmp.add(nums[i]);
            solve(nums,i+1);
            tmp.removeLast();
        }
    }

五、子集II

      题目链接:https://leetcode-cn.com/problems/subsets-ii/

      题目描述:给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

解法1(二叉树)

      在上题的基础上去重即可,代码如下:

	List<List<Integer>> ans=new LinkedList<>();
    Deque<Integer> tmp=new LinkedList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);//含有重复元素要记得排序哦。
        solve(nums,0);
        return ans;
    }
    public void solve(int[] nums,int cur){
        if(cur==nums.length){
            ans.add(new LinkedList<>(tmp));
            return;
        }
        tmp.add(nums[cur]);
        solve(nums,cur+1);

        int a=tmp.peekLast(),i;
        tmp.removeLast();
        for(i=cur+1;i<nums.length;i++)
            if(nums[i]!=a)
                break;

        solve(nums,i);
    }

解法2(n 叉树)

      同理,代码如下:

	List<List<Integer>> ans=new LinkedList<>();
    Deque<Integer> tmp=new LinkedList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        solve(nums,0);
        return ans;
    }
    public void solve(int[] nums,int begin){
        ans.add(new LinkedList<>(tmp));

        for(int i=begin;i<nums.length;i++){
            if(i>begin&&nums[i]==nums[i-1])//如果有重复元素,只会在其第一次出现时选取
                continue;
            tmp.add(nums[i]);
            solve(nums,i+1);
            tmp.removeLast();
        }
    }

      采用标记数组的代码如下:

List<List<Integer>> ans=new LinkedList<>();
    Deque<Integer> tmp=new LinkedList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        boolean[] visited=new boolean[nums.length];
        solve(nums,0,visited);
        return ans;
    }
    public void solve(int[] nums,int begin,boolean[] visited){
        ans.add(new LinkedList<>(tmp));

        for(int i=begin;i<nums.length;i++){
            if(i>0&&nums[i]==nums[i-1]&&visited[i-1]==false)
                continue;
            visited[i]=true;
            tmp.add(nums[i]);
            solve(nums,i+1,visited);
            tmp.removeLast();
            visited[i]=false;
        }
    }

六、排列序列

      题目链接:https://leetcode-cn.com/problems/permutation-sequence/

      题目描述:给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列。

按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:

“123”
“132”
“213”
“231”
“312”
“321”
给定 n 和 k,返回第 k 个排列。

解法1(回溯)

      在全排列的基础上要进行剪枝,假设第一轮为选取第一个元素时,第二轮为选取第二个元素时… 那么基本思想是,在一次第一轮遍历可以得到 n-1! 种结果,在一次第二轮遍历可以得到 n-2! 种结果… 既然这样,那么在每一轮遍历开始之前先判断本轮能得到几种结果假设为 tmp,如果它小于 k 的话,本轮直接跳过,令 k=k-tmp,继续下一轮判断;如果 tmp 大于 k 的话,则证明最终的第 k 种结果在本轮中出现,这样就可以避免很多次没必要的递归遍历了,事实上它只会找到我们想要的第 k 次排列,其余的排列根本不会被遍历到。

      初始代码如下(有点粗糙):

	boolean[] used;// 用来保障全排列的 used 数组
    boolean flag=false;// 递归退出标志
    public String getPermutation(int n, int k) {
        StringBuilder re=new StringBuilder();
        used=new boolean[n+1];
        solve(re,(char)('0'+n),k);
        return new String(re);
    }
    public void solve(StringBuilder re,char n,int k){
        if(re.length()==n-'0'){
            flag=true;
            return;
        }
        for(char i='1';i<=n;i++){
            if(used[i-'0']==false){
                //通过 used 中的 false 变量的个数来判断本轮可以产生多少种结果数,由 tmp 来表示。
                //比如 false 的个数 count 为 0,表明这是第一轮,本轮产生的结果数 tmp = n-1!。依次类推。
                int tmp=1,count=0; // tmp 初始化为 1,因为当 count 为 1 时,0!等于 1,
                for(int j='1'-'0';j<n-'0'+1;j++){
                    if(used[j]==false)
                        count++;   
                }
                for(int j=1;j<count;j++)
                    tmp=tmp*j;
                
                //当 tmp < k 时,修改 k 的值,跳过本轮进入下一轮。
                if(tmp<k){
                    k-=tmp;
                    continue;
                }

                re.append(i);
                used[i-'0']=true;
                solve(re,n,k);
                if(flag) //当 flag 为 true,禁止程序对 re 变量进行改变,此时程序直接一层层退出。
                    break;
                used[i-'0']=false;
                re.deleteCharAt(re.length()-1);
            }

        }
    }

      修改整理之后的代码如下:

	boolean[] used;// 全排列需要的 used 数组
    boolean flag=false;// 退出标志
    int[] f;// 阶乘值,即 f[i] 表示 i 的阶乘
    public String getPermutation(int n, int k) {
        StringBuilder re=new StringBuilder();
        used=new boolean[n+1];
        f=new int[n];
        f[0]=1;
        for(int i=1;i<n;i++)
            f[i]=i*f[i-1];

        solve(re,(char)('0'+n),k,n-1);//新加的参数 cc 表示本轮可产生 cc! 种结果,即 f[cc]
        return new String(re);
    }
    public void solve(StringBuilder re,char n,int k,int cc){
        if(re.length()==n-'0'){
            flag=true;
            return;
        }

        for(char i='1';i<=n;i++){
            if(used[i-'0']==false){
                if(f[cc]<k){
                    k-=f[cc];
                    continue;
                }

                re.append(i);
                used[i-'0']=true;
                solve(re,n,k,cc-1);
                if(flag)
                    break;
                used[i-'0']=false;
                re.deleteCharAt(re.length()-1);
            }

        }
    }

解法2(循环)

      如果想通过 k 值直接得到最终的字符串,那就在循环里一轮轮判断,内在规律和上面的回溯一样。循环部分采用减法来做的代码如下:

	public String getPermutation(int n, int k) {
        int[] f=new int[n];
        f[0]=1;
        for(int i=1;i<n;i++)
            f[i]=i*f[i-1];
        StringBuilder re=new StringBuilder();

        List<Character> c=new LinkedList<>();
        for(char i='1';i<=n+'0';i++)
            c.add(i);
        int index=n-1,indexOfC=0;//标记变量。

        while(re.length()!=n){ 
            if(k<=f[index]){
                re.append(c.get(indexOfC));
                c.remove(indexOfC);
                indexOfC=0;
                index--;
            }
            else{
                indexOfC++;
                k-=f[index];
            }
        }

        return new String(re);
    }

七、复原 IP 地址

      题目链接:https://leetcode-cn.com/problems/restore-ip-addresses/

      题目描述:有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。

例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 ‘.’ 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

解法(三叉树)

      一个有效的整数可能有一位、两位或者三位,对每一个整数分别取一、二和三位递归回溯即可。在回溯的过程中剪枝,对于不满足条件的情况直接跳过。自己列出了三种情况,分别为:1,每次选取一个整数之后,剩下数的个数不能大于未选取整数的三倍。2,每次选择一个整数时,该数不能大于 255 。3,每次选择一个整数时,如果该整数大于一位,那么最高位不能为 0 。参考代码如下:

List<String> ans=new LinkedList<>();
    StringBuilder tmp=new StringBuilder();
    public List<String> restoreIpAddresses(String s) {
        solve(s,0,s.length(),0);
        return ans;
    }
    public void solve(String s,int start,int end,int count){
        if(count==4){
            ans.add(tmp.substring(0,tmp.length()-1));//选取4个整数之后,去掉最后一个字符,保存结果
            return;
        }
        for(int remain=start+1;remain<=end&&remain<=start+3;remain++){//[0,remain):已选取字符  [remain,end):未选取字符
            if(end-remain>3*(4-count-1))  // 1
                continue;
            String ss=s.substring(start,remain);
            if(ss.length()>1&&ss.charAt(0)=='0') // 3
                continue;
            int v=Integer.parseInt(ss);
            if(v>255)// 2    这里不能采用String的compareTo方法比较,因为"35"大于"255"
                continue;

            tmp.append(ss).append('.');
            solve(s,remain,end,count+1);
            int begin=ss.length()+1;
            tmp.delete(tmp.length()-begin,tmp.length());
        }
    }

总结

      完

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值