for循环横向遍历,递归纵向遍历,回溯不断调整结果集
画树,回溯要画树
77. 组合
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
result.add(new ArrayList(path)):开辟一个独立地址,地址中存放的内容为path链表,后续path的变化不会影响到res
res.add(path):将res尾部指向了path地址,后续path内容的变化会导致res的变化。
class Solution { LinkedList path = new LinkedList(); List<List> result = new ArrayList(); public List<List> combine(int n, int k) { backTracking(n,k,1); return result; } void backTracking(int n, int k, int startIndex){ if(path.size()==k){ result.add(new ArrayList(path)); return; } for (int i = startIndex; i <= n; i++) { path.add(i); backTracking(n,k,i+1); path.removeLast();//回溯 } }}
剪枝操作优化代码:
for (int i = startIndex; i <= n; i++) {
剪枝:
for (int i = startIndex; i <=n-(k-path.size())+1; i++) {
path.size() 是当前路径里元素的个数
k-path.size() 是还需要的元素个数
n-(k-path.size())+1 至多走到哪里
216. 组合总和 III
找出所有相加之和为n 的k个数的组合,且满足下列条件:
只使用数字1到9
每个数字最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
- 这题和上面一题十分相似,就浅浅改了一下上面的代码
class Solution { LinkedList path = new LinkedList(); List<List> result = new ArrayList(); public List<List> combinationSum3(int k, int n) { if(k > n){//如果n小于k的话直接返回 return result; } backTracking(k,n,1); return result; } void backTracking(int k, int n, int startIndex){ if(path.size()==k){//数组的长度达到标准之后 int sum = 0; for (Integer p : path) { sum +=p; } if(sum == n){//看数组中数字之和是否达到标准 result.add(new ArrayList(path)); } return; } for (int i = startIndex; i <= n; i++) { path.add(i); backTracking(k,n,i+1); path.removeLast(); } }}
- 结果超出时间限制,因为没有剪枝
for (int i = startIndex; i <= Math.min(n-(k*(k-1)/2),9) ; i++) {
- k*(k-1)/2 是对前k-1个数的求和
- n-(k*(k-1)/2) 和 9 取最小是怕n太大,k太小导致path里的数 > 9
17. 电话号码的字母组合
给定一个仅包含数字2-9的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
- 把按键上对应的数字转换成字符串letters,遍历的时候要遍历的是字符串
class Solution { List result = new ArrayList(); static String[] letters = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};//下标就是按键上的数字 public List letterCombinations(String digits) { if(digits.equals("")) return result;//对特殊情况特殊处理 backTracking(digits,0);//从零开始 return result; } static StringBuilder sb = new StringBuilder(); void backTracking(String digits, int index){ //index指的是当前对哪个数字进行处理 if(sb.length()==digits.length()){ result.add(sb.toString()); return; } String letter = letters[(digits.charAt(index))-'0'];//取当前下标对应的字符串 for (int i = 0; i < letter.length(); i++) { sb.append(letter.charAt(i)); backTracking(digits,index+1); sb.deleteCharAt(sb.length()-1); } }}
- 感觉慢慢理解回溯了?
39. 组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
- 对组合问题的变形:允许元素重复使用,不固定路径数组的长度
class Solution { List<List> res = new ArrayList(); List path = new ArrayList(); public List<List> combinationSum(int[] candidates, int target) { combine(candidates, target, 0); return res; } void combine(int[] candidates, int target, int startIndex){ int sum = 0; //算出当前路径的结点之和 for (Integer node : path) { sum += node; } if(sum == target){ res.add(new ArrayList(path));//得到符合条件的结果,把路径加到结果集中 return; } if(sum > target){ return;//当前路径上的结点之和已经大于所求,不用再往下加了 } for (int i = startIndex; i < candidates.length; i++) { path.add(candidates[i]);//把当前的元素加入path中 combine(candidates,target,i); path.remove(path.size()-1); } }}
40. 组合总和 II
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
- 本题也是对组合问题的变形:candidates中的元素 无序 且 重复
- 元素无序 给剪枝操作增加了难度,我在操作前对数组candidates进行了排序
- 元素重复 我的做法是对结果集res 新建了一个result进行去重,但是有的测试用例超时
class Solution { List<List> res = new ArrayList(); List path = new ArrayList(); public List<List> combinationSum2(int[] candidates, int target) { Arrays.sort(candidates);//先给数组排序 int length = candidates.length; for (int candidate : candidates) { if(candidate > target){ length--; } } Arrays.copyOf(candidates,length); combine(candidates, target, 0); //此时已经拿到结果集res,但是需要对其进行去重操作 List<List> result = new ArrayList(); for (List re : res) { if (!result.contains(re)){ result.add(re);//如果真正的结果集result中不包括re,就可以把re加入道result } } return result; } void combine(int[] candidates, int target, int startIndex){ int sum = 0; //算出当前路径的结点之和 for (Integer node : path) { sum += node; } if(sum == target){ res.add(new ArrayList(path));//得到符合条件的结果,把路径加到结果集中 return; } if(sum > target){ return;//当前路径上的结点之和已经大于所求,不用再往下加了 } for (int i = startIndex; i < candidates.length; i++) { path.add(candidates[i]);//把当前的元素加入path中 combine(candidates,target,i+1); path.remove(path.size()-1); } }}
- 就是这个用例超时,仔细瞅瞅这个用例,它说要对重复的数据进行处理,而不能只改结果集。确实,那样效率太低了?
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]30
- 我在for循环中加入了如下语句,想使重复的元素跳出当前循环
if(i>0&&candidates[i]==candidates[i-1]){ continue;}
- 但是他把满足条件的含有重复元素的结果也给剪掉了,是剪枝的条件出了问题
- 把 i>0 换成 i>startIndex 后运行通过
if(i>startIndex&&candidates[i]==candidates[i-1]){
131. 分割回文串
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
- 这题是分割问题,但是本质和组合问题思路一样
- 剪枝的思路是:如果当前切出来的子串已经不是回文串了,就continue进入下一次循环,这样可以保证叶子结点都是满足要求的path
class Solution { List<List> res = new ArrayList(); LinkedList path = new LinkedList(); public List<List> partition(String s) { combine(s,0); return res; } void combine(String s, int startIndex){ if(startIndex >= s.length()){ res.add(new LinkedList(path));//收获叶子结点 } for (int i = startIndex; i endIndex){ return true; } if(s.charAt(startIndex)==s.charAt(endIndex)){ startIndex++; endIndex--; } else { return false; } } }}
93. 复原 IP 地址
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 ‘.’ 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
❗ ❗ ❗ 不是自己写出来的
- 按照自己本来的思路写,是new了一个StringBuilder,想着这样操作方便些,但是很难回溯啊,就把自己绕进去了,下面的题解是在String上用subString()的方法操作
- 还有递归出口,要专门创建一个int去记录逗点的数量
class Solution { List result = new ArrayList(); public List restoreIpAddresses(String s) { if (s.length() > 12) return result; // 算是剪枝了 backTrack(s, 0, 0); return result; } // startIndex: 搜索的起始位置, pointNum:添加逗点的数量 private void backTrack(String s, int startIndex, int pointNum) { if (pointNum == 3) {// 逗点数量为3时,分隔结束 // 判断第四段⼦字符串是否合法,如果合法就放进result中 if (isValid(s,startIndex,s.length()-1)) { result.add(s); } return; } for (int i = startIndex; i end) { return false; } if (s.charAt(start) == '0' && start != end) { // 0开头的数字不合法 return false; } int num = 0; for (int i = start; i '9' || s.charAt(i) 255) { // 如果⼤于255了不合法 return false; } } return true; }}
- 回溯是把当前for循环里的操作给回溯掉,干干净净的进入下一个循环
- 感觉这一题是挺难的,现在还有点没迷过来?
78. 子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
class Solution { LinkedList path = new LinkedList(); List<List> res = new LinkedList(); public List<List> subsets(int[] nums) { res.add(new LinkedList(path));//把空的path进去 combine(nums,0); return res; } void combine(int[] nums, int startIndex){ if(!path.isEmpty()&&path.getLast()==nums[nums.length-1]){ return;//递归出口 } for (int i = startIndex; i < nums.length; i++) { path.add(nums[i]); res.add(new LinkedList(path));//每一个结点都要收集 combine(nums,i+1); path.removeLast();//回溯 } }}
- 这题比较简单,子集的特点就是每一个结点都要收集,那我们在添加路径时收集就可以了
- 递归的出口是当path最后一个数是数组中的最后一个数时,不再向下递归
90. 子集 II
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
- 这题和之前相比就多了一个去重操作,先排序后去重
class Solution { LinkedList path = new LinkedList(); List<List> res = new LinkedList(); public List<List> subsetsWithDup(int[] nums) { res.add(new LinkedList(path));//把空的path进去 Arrays.sort(nums);//先排序 combine(nums,0); return res; } void combine(int[] nums, int startIndex){ for (int i = startIndex; i startIndex && nums[i] == nums[i - 1]) { continue;//去重 } path.add(nums[i]); res.add(new LinkedList(path));//每一个结点都要收集 combine(nums,i+1); path.removeLast();//回溯 } }}
491. 递增子序列
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
class Solution { List<List> res = new ArrayList(); ArrayList path = new ArrayList(); public List<List> findSubsequences(int[] nums) { combine(nums, 0); return res; } void combine(int[] nums, int startIndex) { if (path.size() > 1) { res.add(new ArrayList(path)); } Map map = new HashMap();//每层一个map for (int i = startIndex; i < nums.length; i++) { if (!path.isEmpty() && nums[i] 0){//如果该元素的value不是0,说明之前添加过,直接进下一次循环 continue;//去重 } map.put(nums[i],map.getOrDefault(nums[i],0)+1);//第一次遍历到该元素时,以数组元素为key,value赋1 path.add(nums[i]); combine(nums, i + 1); path.remove(path.size() - 1);//回溯 } }}
- 这里去重是用map实现的
default V getOrDefault ( Object key , V defaultValue ) 返回指定key映射到的value,如果当前map不包含当前key的映射,则返回 defaultValue 。
46. 全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
全排列问题 还是要画树
循环遍历当前path中没有的数
List<List> res = new ArrayList(); List path = new ArrayList(); public List<List> permute(int[] nums) { combine(nums); return res; } void combine(int[] nums){ if (path.size()== nums.length){ res.add(new ArrayList(path));//收集结果 } for (int num : nums) { if (path.contains(num)) { continue;//如果路径里有num,跳出此次循环 } path.add(num); combine(nums); path.remove(path.size()-1); } }
47. 全排列 II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
只做树层的去重,不做树枝的去重
nums[i]=nums[i-1]并且used[i-1]=true说明nums[i-1]已经被加进了path中,这是数枝的重复
nums[i]=nums[i-1]并且used[i-1]=false说明nums[i-1]已经被回溯成了false,这是树层的重复,要剪掉✂
新建了一个used数组来表示nums数组的使用情况
class Solution { List<List> res = new ArrayList(); List path = new ArrayList(); boolean[] used; public List<List> permuteUnique(int[] nums) { used = new boolean[nums.length]; for (boolean b : used) { b=false; } Arrays.sort(nums); comine(nums,used); return res; } void comine(int[] nums, boolean[] used) { if (path.size() == nums.length) { res.add(new ArrayList(path));//收集结果 } for (int i = 0; i 0&&nums[i]==nums[i-1]&&used[i-1]==false) { continue;//当前数和前面的数相等并且前面的数还未使用时 } if(used[i]==false){//当前元素还未使用时 path.add(nums[i]); used[i]=true;//在path中添加元素,该位置标记为true comine(nums,used); path.remove(path.size() - 1);//回溯 used[i]=false;//回溯 } } }}