全组合(SubSets)全排列(Permutation)全组合和(Combination Sum)解题技巧

1.1 全组合(无重复)

  • 题目:给定一个数组(不存在重复),输出所有可能的组合,不限定顺序(LeetCode 78)。
  • 样例:[1,2,3]=>[[],[1],[2],[3],[1,2],[1,3],[2,3],[1,2,3]]
  • 技巧:因为是不存在重复的,典型的采用递归即可,即每次取定第一个开始的节点,然后从该节点后面开始放数据和出数据
  • 代码:
class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        // 思路:经典的排列组合问题
        if(nums==null||nums.length==0){
            return new ArrayList<>();
        }
        
        // 如果不需要去重,则不需要排序
        List<List<Integer>> list=new ArrayList<>();
        backtrack(nums,list,new ArrayList<>(),0);
        return list;
    }
    
    private void backtrack(int [] nums,List<List<Integer>> list,List<Integer> subList,int index){
        if(index>nums.length){
            return;
        }
        
        list.add(new ArrayList<>(subList));
        for(int i=index;i<nums.length;i++){
            subList.add(nums[i]);
            backtrack(nums,list,subList,i+1);
            subList.remove(subList.size()-1);
        }
    }
}

1.2 全组合(重复)

  • 题目:同上,但是存在重复(LeetCode 90)。
  • 样例:[1,2,2]=>[[],[1],[1,2],[1,2,2],[2],[2,2]]
  • 技巧:因为存在重复,所以我们必然得先排序,然后每次选定一个开始的节点,考虑到会存在重复,所以我们在访问完第一个重复的节点后,需要跳过重复的节点来选其为第一个节点。最后,就是递归重复1.1的放数据和出数据了。
  • 代码:
class Solution {
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        // 思路:需要对数字进行排序,第一个交换字符在后续递归过程中需要去重(第一次的话则不需要)。
        // 思路2:使用Set<List<Integer>> 可对数据进行去重。
        // 以上两个思路都需要对字符串进行排序后处理。
        if(nums==null||nums.length==0){
            return new ArrayList<>();
        }
        Arrays.sort(nums);
        List<List<Integer>> list=new ArrayList<>();
        backtrack(nums,list,new ArrayList<>(),0);
        return list;
    }
    
    private void backtrack(int []nums,List<List<Integer>> list,List<Integer> subList,int index){
        if(index>nums.length){
            return;
        }
        
        list.add(new ArrayList<>(subList));
        for(int i=index;i<nums.length;i++){
            if(i>index&&nums[i]==nums[i-1]){
                // 进行重复的第一个字符进行去重
                continue;
            }
            
            subList.add(nums[i]);
            backtrack(nums,list,subList,i+1);
            subList.remove(subList.size()-1);
        }
    }
}

1.3 全排列(无重复)

  • 题目:给定数组,其中不包含重复数据,需要输出所有可能的全排列,不限定顺序(LeetCode 46)。
  • 样例:[1,2,3]=>[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]
  • 技巧:
    • 解法1:考虑到无重复数据,我们只需每次选定一个数作为最开始的节点,然后for循环的选择第2个节点,但是我们需要用一个标识位数组来记录已经访问过的节点(即这些节点已经在前面放进去了)。然后递归的就是放数据和改标识位。
    • 解法2:考虑到无重复数据,最初,我们可以选定下标为0的为第一部分的第一个节点数据,然后选择后面的每一个节点跟其交换,从而形成一个排列。同理,这样后面部分同理采用递归的方式来选择当前部分第一个节点,依旧选择后面一个节点跟其交换。
    • 对比:
      • 解法1:简单,且容易理解和写出来。
      • 解法2:速度更快,因为通过交换的方式减少了数据的放入和放出。
  • 代码:
  • 解法1:
class Solution {
    public List<List<Integer>> permute(int[] nums) {
        // 思路:暴力解法,采用递归遍历的方式,采用一个数组记录已经访问的节点
        if(nums==null||nums.length==0){
            return new ArrayList<>();
        }
        
        List<List<Integer>> list=new ArrayList<>();
        backtrack(nums,list,new ArrayList<>(),new boolean [nums.length]);
        return list;
    }
    
    private void backtrack(int [] nums,List<List<Integer>> list,List<Integer> subList,boolean [] visited){
        if(subList.size()==nums.length){
            list.add(new ArrayList<>(subList));
        }else{
            for(int i=0;i<nums.length;i++){
                if(visited[i]){
                    continue;
                }
                
                visited[i]=true;
                subList.add(nums[i]);
                backtrack(nums,list,subList,visited);
                subList.remove(subList.size()-1);
                visited[i]=false;
            }
        }
    }
}
  • 解法2:
class Solution {
    public List<List<Integer>> permute(int[] nums) {
        // 思路:使用剑指offer的方式,每次交换第一个位置,然后递归后续的数据
        if(nums==null||nums.length==0){
            return new ArrayList<>();
        }
        
        List<List<Integer>> list=new ArrayList<>();
        backtrack(nums,list,0);
        return list;
    }
    
    private void backtrack(int [] nums,List<List<Integer>> list,int index){
        if(index==nums.length){
            // 已经到末尾了
            list.add(getList(nums));
        }else{
            for(int i=index;i<nums.length;i++){
                // 当前index位置和后面的每个都进行一次交换
                swap(nums,index,i);
                // 对index后面部分递归调用backtrack,即保证交换第一个位置后面的部分也按照第一个交换的规律进行交换
                backtrack(nums,list,index+1);
                swap(nums,i,index);
            }
        }
    }
    
    private List<Integer> getList(int [] nums){
        List<Integer> list=new ArrayList<>();
        for(int num:nums){
            list.add(num);
        }
        return list;
    }
    
    private void swap(int [] nums,int i,int j){
        int tmp=nums[i];
        nums[i]=nums[j];
        nums[j]=tmp;
    }
}

1.4 全排列(重复)

  • 题目:同上,但是存在重复数据(LeetCode 47)。
  • 样例:[1,1,2]=>[[1,1,2],[1,2,1],[2,1,1]]
  • 技巧:
    • 解法1:同1.3的解法1,因为存在重复数据,我们需要对数据进行排序,然后每次选定第一个没有访问的过重复作为第一个节点。最后,就是按照上面解法1的做法了(如果同前面一样,并且前面没有访问过的话,说明该节点为重复的,则不能选择)。
    • 解法2:同1.3的解法2,我们需要每次都跳过同第一个节点一样的来进行交换(因为后面节点跟第一个节点一样,两者交换的排列一样的)。
  • 代码:
  • 解法1:
class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {
        // 思路:采用最简单的暴力解法试下,由于存在重复,所以必须先排序,且我们需要采用一个数组来记录已经访问的节点
        if(nums==null||nums.length==0){
            return new ArrayList<>();
        }
        
        Arrays.sort(nums);
        List<List<Integer>> list=new ArrayList<>();
        backtrack(nums,list,new ArrayList<>(),new boolean[nums.length]);
        return list;
    }
    
    private void backtrack(int [] nums,List<List<Integer>> list,List<Integer> subList,boolean [] visited){
        if(subList.size()==nums.length){
            list.add(new ArrayList<>(subList));
        }else{
            for(int i=0;i<nums.length;i++){
                if(visited[i]||(i>0&&nums[i]==nums[i-1]&&!visited[i-1])){
                    // 若已经访问过了或者同前面一样并且前面的还没有被访问过,说明前面一直是重复节点,不能选该节点放下去
                    continue;
                }
                
                subList.add(nums[i]);
                visited[i]=true;
                backtrack(nums,list,subList,visited);
                visited[i]=false;
                subList.remove(subList.size()-1);
            }
        }
    }
}
  • 解法2:
class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {
       // 思路:采用第一个位置作为交换位置,因为可能存在重复,所以需要对数组进行排序,然后去掉第一个交换位置和当前相同的情况,即第一个位置没必要使用相同的
        if(nums==null||nums.length==0){
            return new ArrayList<>();
        }
        
        List<List<Integer>> list=new ArrayList<>();
        backtrack(nums,list,0);
        return list;
    }
    
    private void backtrack(int []nums,List<List<Integer>> list,int index){
        if(index==nums.length){
            list.add(new ArrayList<>(asList(nums)));
        }else{
            for(int i=index;i<nums.length;i++){
                if(!needSwap(nums,index,i)){
                    continue;
                }
                swap(nums,index,i);
                backtrack(nums,list,index+1);
                swap(nums,i,index);
            }
        }
    }
    
    private boolean needSwap(int []nums,int index,int i){
        for(int start=index;start<i;start++){
            // 说明第一个节点到i这段中间存在和i一样的,所以不需要更换,因为重复的节点交换没有意义
            if(nums[start]==nums[i]){
                return false;
            }
        }
        return true;
    }
    
    private List<Integer> asList(int [] nums){
        List<Integer> list=new ArrayList<>();
        for(int num:nums){
            list.add(num);
        }
        return list;
    }
    
    private void swap(int [] nums,int i,int j){
        int tmp=nums[i];
        nums[i]=nums[j];
        nums[j]=tmp;
    }
}

1.5 全组合和(无重复)

  • 题目:给定数组和target,我们需要找出满足子数组和等于target的所有满足条件的子数组,且不限定顺序,其中每个数字可以重复使用多次(LeetCode 39)。
  • 样例:[2,3,6,7],target=7 => [[2,2,3],[7]]
  • 说明:每个数字>=0,数组长度<=30,无重复数字。
  • 技巧:同上面解题思路类似,只是这里我们可以对每个数组使用多次,我们每次递归需要把当前剩余的差值(target-nums[i])作为target当作参数传入。
  • 代码:
class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        // 思路:典型的递归求组合和
        if(candidates==null||candidates.length==0||target==0){
            return new ArrayList<>();
        }
        
        List<List<Integer>> list=new ArrayList<>();
        backtrack(candidates,list,new ArrayList<>(),target,0);
        return list;
    }
    
    public void backtrack(int [] candidates,List<List<Integer>> list,List<Integer> subList,int remain,int index){
        if(remain<0){
            // 因为都是正数,所以不可能得到remain为负数的情况
            return;
        }
        
        if(remain==0){
            list.add(new ArrayList<>(subList));
        }else{
            for(int i=index;i<candidates.length;i++){
                subList.add(candidates[i]);
                // 因为每个数字可以使用多次,即下一次还是可以以该下标作为开始节点
                backtrack(candidates,list,subList,remain-candidates[i],i);
                subList.remove(subList.size()-1);
            }
        }
    }
}

1.6 全组合(重复)

  • 题目:同上,但是数组中的每个数字我们只能使用一次。
  • 样例:[10,1,2,7,6,1,5],target=8 => [[1,1,6],[1,2,5],[1,7],[2,6]]
  • 技巧:因为存在重复,而且每个数我们只能用一次,所以我们需要
    排序,并且跳过重复数作为第一个选定的节点,然后就是递归进行同样的步骤即可。
  • 代码:
class Solution {
    public List<List<Integer>> combinationSum2(int[] nums, int target) {
        // 思路:因为不能用重复元素,所以需要排序,然后对不是第一次出现重复的数进行跳过
        if(nums==null||nums.length==0){
            return new ArrayList<>();
        }
        
        Arrays.sort(nums);
        List<List<Integer>> list=new ArrayList<>();
        backtrack(nums,list,new ArrayList<>(),target,0);
        return list;
    }
    
    private void backtrack(int [] nums,List<List<Integer>> list,List<Integer> subList,int remain,int index){
        if(remain<0){
            return ;
        }
        
        if(remain==0){
            list.add(new ArrayList<>(subList));
        }else{
            for(int i=index;i<nums.length;i++){
                // 去掉已经第一个元素为已经用过的组合
                if(i>index&&nums[i]==nums[i-1]){
                    continue;
                }
                
                subList.add(nums[i]);
                // 不能使用重复的元素,必须往后走
                backtrack(nums,list,subList,remain-nums[i],i+1);
                subList.remove(subList.size()-1);
            }
        }
    }
}

1.7 所有回文组合

  • 题目:给定字符串,我们需要找出所有满足回文(正反对称的)的子字符串。
  • 样例:“aab”=>[[“a”,“a”,“b”],[“aa”,“b”]]
  • 技巧:同1.1的全组合类型,我们需要选定开始的第一个节点,然后分别追加判断是否满足回文。同理,接着递归进行数据放入和放出。
  • 代码:
class Solution {
    public List<List<String>> partition(String s) {
        // 思路:典型的全组合问题,但是我们需要判断组合后的字符是否满足回文,如果不满足,则直接中断本次递归。
        if(s==null||s.length()==0){
            return new ArrayList<>();
        }
        
        List<List<String>> list=new ArrayList<>();
        backtrack(s.toCharArray(),list,new ArrayList<>(),0);
        return list;
    }
    
    private void backtrack(char [] chs,List<List<String>> list,List<String> subList,int index){
        if(index==chs.length){
            list.add(new ArrayList<>(subList));
        }else{
            for(int i=index;i<chs.length;i++){
                if(isPalindrome(chs,index,i)){
                    subList.add(getString(chs,index,i));
                    // 因为是组合,所以需要选当前下标i的后面一个作为开始节点
                    backtrack(chs,list,subList,i+1);
                    subList.remove(subList.size()-1);
                }
            }
        }
    }
    
    private boolean isPalindrome(char[] chs,int start,int end){
        while(start<end){
            if(chs[start++]!=chs[end--]){
                return false;
            }
        }
        return true;
    }
    
    private String getString(char[] chs,int start,int end){
        StringBuilder str=new StringBuilder();
        for(int i=start;i<=end;i++){
            str.append(chs[i]);
        }
        return str.toString();
    }
}

2.小结

  • 针对全排列:如果第一时间想不到交换的方式,可以采用从0->length的全排列探测法来放入数据,同时采用visited标识位数组来记录已经访问过的节点。
  • 针对全组合:每次递归的下标即当前访问节点的i的下一个i+1(因为是从当前节点的后面开始选节点),所以不能使用第一个index(index仅表示当前递归部分的第一个节点,这种是在全排列交换数据的时候使用的)。

3.参考地址

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值