一、备注
最近做leetcode的过程经常遇到有关数组排列组合的问题,leetcode的讨论中也涉及到这些,于是将其中的内容整理出来,供自己日后复习之用,这些问题都可以划分为有重复元素和无重复元素两种,基本方法都是深度优先遍历。
二、子数组
给定数组求子集的组合。
1.给定数组不包含重复元素的情况
这种情况就是求给定数组的幂集(power set),代码如下。
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, 0);
return list;
}
private void backtrack(List<List<Integer>> list , List<Integer> tempList, int [] nums, int start){
list.add(new ArrayList<>(tempList));
for(int i = start; i < nums.length; i++){
tempList.add(nums[i]);
backtrack(list, tempList, nums, i + 1);
tempList.remove(tempList.size() - 1);
}
}
通过排序可以更好地避免子集重复地情况发生。backtrack函数中在向下遍历的过程中没有直接new出一个新的list,而是在加入到结果集中的时候在利用当前子数组构造新数组,这样节省了不少空间,后面的操作皆是如此。
当然,也可以用二进制编码的方法来做,假设给定数组(无重复)有n个元素,则可能的集合数量有种,可以用一个二进制数来表示当前元素的组合,0表示对应位置的元素不出现,1表示对应位置的元素的元素出现。
例如,对于集合{0,1,2,3},0001表示子集{3},0110表示{1,2},对应代码如下:
class Solution{
public static List<List<Integer>> subSetWithouDuplicate(int[] nums){
List<List<Integer>> res=new ArrayList<>();
for(int i=0;i<1<<nums.length;i++){
List<Integer> subSet=new ArrayList<>();
for(int j=0;j<nums.length;j++){
if(((i>>j)&1)!=0){
subSet.add(nums[j]);
//System.out.print(nums[j]);
}
}
//System.out.print("\n");
res.add(subSet);
}
return res;
}
}
2.给定数组包含重复元素的情况
这种情况就不好用之前的二进制数表示法来做了,因为会有重复的情况,比如对于数组{0,1,1},110和101表示的子数组是相同的,代码如下:
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, 0);
return list;
}
private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, int start){
list.add(new ArrayList<>(tempList));
for(int i = start; i < nums.length; i++){
if(i > start && nums[i] == nums[i-1]) continue; // skip duplicates
tempList.add(nums[i]);
backtrack(list, tempList, nums, i + 1);
tempList.remove(tempList.size() - 1);
}
}
操作同样是先对给定数组排序,然后再用dfs进行遍历。基本思想就是遇到和前一个元素相同的元素就跳过当前位置,避免重复,同时要标注当前元素的位置。
对于i > start && nums[i] == nums[i-1],i>start说明nums[i-1]还未被加入到当前集合中,nums[i-1]==nums[i]说明当前元素与前一元素相同,这种情况下需要跳过当前元素。
三、元素的排列组合
给定数组的情况求数组中所有元素的排列组合的情况。例如{1,2}的组合有{1,2}和{2,1}
1.给定数组不包含重复元素的情况
和之前的方法类似,同样利用深度优先遍历,只不过需要判断当前list的大小是否为所给定数组的长度。
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
// Arrays.sort(nums); // not necessary
backtrack(list, new ArrayList<>(), nums);
return list;
}
private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums){
if(tempList.size() == nums.length){
list.add(new ArrayList<>(tempList));
} else{
for(int i = 0; i < nums.length; i++){
if(tempList.contains(nums[i])) continue; // element already exists, skip
tempList.add(nums[i]);
backtrack(list, tempList, nums);
tempList.remove(tempList.size() - 1);
}
}
}
此时对给定数组进行预排序就没有必要了。
2.给定数组包含重复元素的情况
代码如下:
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, new boolean[nums.length]);
return list;
}
private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, boolean [] used){
if(tempList.size() == nums.length){
list.add(new ArrayList<>(tempList));
} else{
for(int i = 0; i < nums.length; i++){
if(used[i] || i > 0 && nums[i] == nums[i-1] && !used[i - 1]) continue;
used[i] = true;
tempList.add(nums[i]);
backtrack(list, tempList, nums, used);
used[i] = false;
tempList.remove(tempList.size() - 1);
}
}
}
和前面一样,需要跳过重复的元素。
四、集合元素的和
给定数组,求元素和为给定值的子数组
1.元素可以重复
比如{0,1,2,3},target=6,有几种可能,{1,1,1,1,1,1}、{2,2,2}等(未列举完全)。
代码如下:
public List<List<Integer>> combinationSum(int[] nums, int target) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, target, 0);
return list;
}
private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, int remain, int start){
if(remain < 0) return;
else if(remain == 0) list.add(new ArrayList<>(tempList));
else{
for(int i = start; i < nums.length; i++){
tempList.add(nums[i]);
backtrack(list, tempList, nums, remain - nums[i], i); // not i + 1 because we can reuse same elements
tempList.remove(tempList.size() - 1);
}
}
}
最开始我的做法是列举出所有可能的子数组,去掉重复的子数组,虽然可以通过oj,但是时间花费比较大,后来看了leetcode中的讨论,做法如上,先将给定数组排序,然后判断子数组的和是否与给定值相同,如果相同,则加入到结果集中。
2.元素没有重复
比如{1,2,3},target=3,则结果为{1,2}和{3}.
代码如下:
public List<List<Integer>> combinationSum2(int[] nums, int target) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, target, 0);
return list;
}
private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, int remain, int start){
if(remain < 0) return;
else if(remain == 0) list.add(new ArrayList<>(tempList));
else{
for(int i = start; i < nums.length; i++){
if(i > start && nums[i] == nums[i-1]) continue; // skip duplicates
tempList.add(nums[i]);
backtrack(list, tempList, nums, remain - nums[i], i + 1);
tempList.remove(tempList.size() - 1);
}
}
}
和前面的做法类似,先对给定数组进行排序,然后跳过重复的数组。