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) {
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;
}
}
}
}
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,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++){
swap(nums,index,i);
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);
}
}
}
}
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++){
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){
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));
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.参考地址