出处:代码随想录 代码随想录 代码随想录 代码随想录
要会设置回溯函数终止条件,进入回溯函数,首先判断,是否符合终止条件。
形成回溯搜索树,集合大小构成树的宽度,递归深度构成树的深度,遍历过程伪代码:
if (终止条件) {
记录结果;
return;
}
for (选择 : 本层集合中元素) { // for循环的执行次数和当前节点的孩子数一致
处理节点;
backtracking(路径,选择列表); // 递归过程
撤销结果,达到回溯目的
}
for:横向遍历
backtracking:深度遍历
一、1 组合问题:
/**
* 一、组合问题
* 返回1-n个数中所有可能的k个数的组合
*
* 此处对照之前的回溯算法,n为树的宽度,k为树的深度
* startIndex标记本层递归中,集合从哪里开始遍历
* 两个要素:
* 【终止条件的判断】:path(存放单一结果的集合)的长度==k时,找到了一个解,说明path是根节点到叶子节点的路径
* 【单层搜索过程】:for循环横向遍历,每次从startIndex开始
*/
public static List<List<Integer>> combine(int n, int k) {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
combineHelper(result, path, 1, n, k);
return result;
}
private static void combineHelper(List<List<Integer>> result, LinkedList<Integer> path, int startIndex, int n, int k) {
//【终止条件的判断】
if (path.size() == k) {
result.add(new ArrayList<>(path));
return;
}
//【单层搜索过程】
/**
* 二、组合问题的剪枝优化
* 返回1-n个数中所有可能的k个数的组合 问题的优化
* 如果n = 4,k = 4时,从2开始遍历就没有意义了,则每层for循环从第二个数开始遍历就没有意义
* 【剪枝的地方】:在于递归中每一层的for循环所选择的起始位置
* 如果for循环的起始位置之后的元素数不足我们需要的k个,就无须搜素
*
* 【i <= n - (k - path.size() + 1)】:为剪枝优化
* path.size() 已经选择的元素个数
* k - path.size() 还需要的元素个数
* 最多从 n - (k - path.size() + 1)开始遍历,再往后就不够了,+1 因为左边需要的是闭
* n = 4,k = 3,path.size() == 0, n - (k - path.size() + 1) = 2, 所以从2开始搜索都是可以的
*/
for (int i = startIndex; i <= n - (k - path.size() + 1); i++) {
path.add(i);
combineHelper(result, path, startIndex + 1, n, k);
path.removeLast();
}
}
组合问题什么时候需要startIndex呢?
如果是一个集合求解,就需要startIndex,如果是多个集合取组合就各个集合互不影响就不需要index
/**
* 三、组合总和问题
* 找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
* 输入: k = 3, n = 7 输出: [[1,2,4]]
* 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]
*
* 【解集中元素不能重复】
* 其实就是在{1,2,3,4,5,6,7,8,9}这个集合中找到和为n的k个数的组合
* k是树的深度,9是树的宽度
*
* 【终止条件的判断】:如果path.size()==k就停止,如果sum == targetSum,就将path放到result里
* 【单层搜索过程】:因为集合固定从9个数中搜索,所以 for的次数为9次
* 【剪枝处理】:如果元素总和>n,就没有必要往后遍历了
*/
public static List<List<Integer>> combinationSum(int n, int k) {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
combineSumHelper(result, path, 1, n, 0, k);
return result;
}
private static void combineSumHelper(List<List<Integer>> result, LinkedList<Integer> path,
int startIndex, int targetSum, int sum, int k) {
//【剪枝处理】
if (sum > targetSum) {
return;
}
//【终止条件的判断】
if (path.size() == k) {
if (sum == targetSum) {
// 找到了
result.add(path);
}
return;
}
//【单层搜索结果】
for (int i = startIndex; i <= 9 - (k - path.size() + 1); i++) {
path.add(i);
sum += i;
//【递归操作】
combineSumHelper(result, path, startIndex + 1, targetSum, sum, k);
//【回溯操作】
path.removeLast();
sum -= i;
}
}
/**
* 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
*
* candidates中的数字可以无限制重复选取
* 输入:candidates = [2,3,6,7], target = 7, 所求解集为: [ [7], [2,2,3] ]
* 输入:candidates = [2,3,5], target = 8, 所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ]
*
* 一般都没有0,提示:1 <= candidates[i] <= 200
* 没有明确的个数限制,但是总和也会限制个数
* 【递归函数参数】:result存放结果,path存放一条深度路径的结果
* candidates用来选择成员,target用来对比结果
* startIndex控制for循环的起始位置
*
* 【组合问题中,startIndex什么时候需要???】
* 【递归终止条件】:sum > target的时候需要终止搜索,sum == target的时候需要记录结果并终止搜索
* 【单层搜索的逻辑】:从startIndex开始搜索candidates
*
* ##########################
* 【剪枝优化】:如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了
* 对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历
*/
public static List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
combinationSum2Helper(result, path, candidates, target, 0, 0);
return result;
}
private static void combinationSum2Helper(List<List<Integer>> result, LinkedList<Integer> path, int[] candidates,
int target, int sum, int startIndex) {
if (sum == target) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < candidates.length; i++) {
// 【剪枝操作】
if (sum + candidates[i] > target) {
break;
}
path.add(candidates[i]);
combinationSum2Helper(result, path, candidates, target, sum + candidates[i], i);
//【回溯操作】
path.removeLast();
}
}
/**
* 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
* 每个candidate只能用一次
*
* 输入: candidates = [10,1,2,7,6,1,5], target = 8, 所求解集为: [ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ]
* 输入: candidates = [2,5,2,1,2], target = 5, 所求解集为: [ [1,2,2], [5] ]
* 【本题中candidate有重复,并且一个只能用一次】:要在搜索过程中就去掉重复组合
* 【使用过描述的难度】:同一个树枝上使用过,或者同一个树层上使用过
* 因为元素在同一个组合可以重复,但是两个组合不能相同,所以同树枝上可重复,同树层上不可重复
* 【树层去重需要对数组排序】
* 【递归函数参数】:result, path,used去重,candidates,target,sum, targetIndex
* 【终止递归条件】:sum > target || sum == target
* 【单层搜索逻辑】:判断同一树层上是否使用过:candidates[i] == candidates[i - 1] 并且 used[i - 1] == false
* used[i - 1] = true说明同一树枝使用过
* used[i - 1] = false说明同一树层使用过,要对同一树层使用过的元素跳过
* 或者直接用startIndex去重
* 【剪枝操作】:sum + candidates[i] <= target为剪枝操作
*/
public static List<List<Integer>> combinationSum3(int[] candidates, int target) {
Arrays.sort(candidates);
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
combinationSum3Helper(result, path, candidates, target, 0, 0);
return result;
}
private static void combinationSum3Helper(List<List<Integer>> result, LinkedList<Integer> path,
int[] candidates, int target, int sum, int startIndex) {
if (sum == target) {
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i < candidates.length && sum + candidates[i] <= target; i++) {
//【剔除重复解】
if (i > startIndex && candidates[i] == candidates[i - 1]) {
continue;
}
path.add(candidates[i]);
combinationSum3Helper(result, path, candidates, target, sum + candidates[i], startIndex + 1);
path.removeLast();
}
}
一、2 深度上不同集合的遍历类型的组合问题
/**
* 四、组合问题-电话号码的字母组合
* 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
* 1不对应任何字母 2->abc 3->def
* 4->ghi 5->jkl 6->mno
* 7->pqrs 8->tuv 9->wxyz
*
* 输入:"23" 输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
* 【数字和字母的映射】:使用map
* 【两个字母就2个for循环,3个字母就三个for循环,n个字母就n个for循环】:回溯解决
* 遍历的深度就是"23"的长度,叶子节点就是要收集的结果
* 【确定回溯参数】:字符串s收集叶子节点的结果,字符串数组result保存结果
* index记录在遍历第几个数字,同时也表示树的深度
* 【终止条件】:index = digits.length就收集结果
* 【单层遍历逻辑】:取到index对应的数字,然后找到对应字母集
* !!!和前面组合题不同,这里的每个数字代表的是不同集合的组合,前面组合是同一个集合的组合
* 【输入1等异常】:没有异常输入则不考虑,有或者跳过或者做其他处理,看题目要求
*/
public static List<String> letterCombinations(String digits) {
List<String> result = new ArrayList<>();
if (digits == null || digits.length() == 0) {
return result;
}
String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
letterCombinationsHelper(result, new StringBuilder(), digits, 0, numString);
return result;
}
private static void letterCombinationsHelper(List<String> result, StringBuilder stringBuilder,
String digits, int index, String[] numString){
if (index == digits.length()) {
result.add(stringBuilder.toString());
return;
}
// 找到当前数字对应的字符串
String str = numString[digits.charAt(index) - '0'];
for (int i = 0; i < str.length(); i++) {
stringBuilder.append(str.charAt(i));
//【递归操作】
letterCombinationsHelper(result, stringBuilder, digits, i + 1, numString);
//【回溯操作】
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
}
}
二、切割问题
切割问题和组合问题很像,切割问题要判断切割结果是否合法再放入result中。
/**
* 分割回文串
* 给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。
* 输入: "aab" 输出: [ ["aa","b"], ["a","a","b"] ]
* 【切割问题】:切割问题和组合问题类似
* 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中在选组第三个...
* 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中在切割第三段...
* 分别在横向和纵向两个方向切割
* 【判断回文】
* 【递归函数参数】:result path startIndex标记切割的位置,因为切割以后不能重复切割
* 【递归终止条件】:切割线切到字符串最后面
* 【单层搜索的逻辑】:for (int i = startIndex; i < s.size(); i++),如果是回文就放到path里
*/
public static List<List<String>> partition(String s) {
List<List<String>> result = new ArrayList<>();
LinkedList<String> path = new LinkedList<>();
partitionHelper(result, path, s, 0);
return result;
}
private static void partitionHelper(List<List<String>> result, LinkedList<String> path,
String s, int startIndex) {
//【递归终止条件】
if (startIndex >= s.length()) {
result.add(path);
return;
}
for (int i = startIndex; i < s.length(); i++) {
//如果是回文,就记录在path中
if (isPalindrome(s, startIndex, i)) {
String str = s.substring(startIndex, i + 1);
path.add(str);
} else {
// 继续往下割
continue;
}
partitionHelper(result, path, s, startIndex + 1);
path.removeLast();
}
}
private static boolean isPalindrome(String s, int startIndex, int end) {
for (int i = startIndex, j = end; i < j; i++, j--) {
if (s.charAt(i) != s.charAt(j)) {
return false;
}
}
return true;
}
/**
* 给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式
* 有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔
*
* 输入:s = "25525511135"
* 输出:["255.255.11.135","255.255.111.35"]
*
* 这还是属于【切割问题】也可以抽象为树形结构
* 【递归参数】:startIndex记录切割的位置,pointSum记录点符号的位置
* 【递归终止条件】:pointSum的个数是否为3,为3证明已经切割成四段了,这时候再验证一下最后一段符不符合ip地址要求,
* 符合就加入到result中
* 【单层搜索的逻辑】:for (int i = startIndex; i < s.size(); i++)循环中
* [startIndex, i] 这个区间就是截取的子串,如果截取的不合法就剪枝
* 下一层递归的startIndex要从i+2开始(因为需要在字符串中加入了分隔符.)
* 同时记录分割符的数量pointNum 要 +1
* 【判断子串是否合法】:段位以0为开头的数字不合法
* 段位里有非正整数字符不合法
* 段位如果大于255了不合法
*/
public static List<String> restoreIpAddresses(String s) {
List<String> result = new ArrayList<>();
if (s.length() > 12) {
return result;
}
restoreIpAddressesHelper(result, s, 0, 0);
return result;
}
private static void restoreIpAddressesHelper(List<String> result, String s,
int startIndex, int pointNum) {
if (pointNum == 3) {
if (isValid(s, startIndex, s.length() - 1)) {
result.add(s);
}
return;
}
for (int i = startIndex; i < s.length(); i++) {
if (isValid(s,startIndex, i)) {
s = s.substring(0, i + 1) + "." + s.substring(i + 1);
pointNum++;
restoreIpAddressesHelper(result, s, i + 2, pointNum);
//【回溯】
pointNum--;
//【回溯】去掉.
s = s.substring(0, i + 1) + s.substring(i + 2);
} else {
//【剪枝】
break;
}
}
}
private static boolean isValid(String s, int start, int end) {
if (start > end) {
return false;
}
if (s.charAt(start) == '0' && start != end) {
return false;
}
int num = 0;
for (int i = start; i <= end; i++) {
if (s.charAt(i) > '9' || s.charAt(i) < '0') { // 非数字不合法
return false;
}
num = num * 10 + (s.charAt(i) - '0');
if (num > 255) {
// 大于255不合法
return false;
}
}
return true;
}
/**
* 有一种校验码机制,用于数据传输中的数据完整性检查,规则如下:
* 在字符串中插入一些数字作为校验码,每个数字之后跟随对应个数的字符;
* 要求有校验码(校验码大于零并且无前导零),并且正确匹配、无歧义:
* 如,"sorryhappy" 在插入校验码之后可以为 "5sorry5happy",即 5 + "sorry" + 5 + "happy";
* 但是,有些字符串在进行校验时会产生歧义,
* 比如 "109beautiful" 可以校验为 10 + "9beautiful" 或者 1 + "0" + 9 + "beautiful",故这类编码方式是有歧义的。
*
* 现给出一个字符串 encodedString,请判断这个字符串是否符合上述规则:
* 如果是,则返回去掉校验码后的字符串长度;
* 如果不是,则返回 -1。
*
* 输入:encodedString = "9gorgeous012"
* 输出:10
* 解释:只可以解析为 9 + "gorgeous0" + 1 + "2",可以正确匹配(校验码与随后字符个数相同)且无歧义。
* 返回去掉校验码后的字符串 “gorgeous02” 的长度 10。
*
* 输入:encodedString = "118gorgeous1y"
* 输出:-1
* 解释:可以解析为 11 + "8gorgeous1y" 或 1 + "1" + 8 + "gorgeous" + 1 + "y",有两种解析方式,所以有歧义。
*
* 输入:encodedString = "1u02tt"
* 输出:-1
* 解释:0 不能作为校验码,02 也不能作为校验码,因此返回 -1。
*/
public int checkEncodingString(String encodedString) {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
helper(result, path, encodedString, 0, encodedString.length());
if(result.size() != 1) {
return -1;
}
System.out.println(" ==== result.toString():" + result.get(0).toString());
// 只要把编码前缀转化成数字后相加就行
return result.get(0).stream().mapToInt(value -> value).sum();
}
private void helper(List<List<Integer>> result, List<Integer> path,
String str, int start, int end) {
if (start > end) {
return;
}
if (result.size() >= 2) {
// 如果result已经有大于1个了,肯定最终返回-1,不用再计算
return;
}
if (start == end) {
// 只有这种情况是会加入到结果中的
result.add(new ArrayList<>(path));
return;
}
if (str.charAt(start) == '0') {
// 第一个字符是0
return;
}
for (int i = 1; i <= 4 && start + i < end; i++) {
if (str.charAt(start + i - 1) < '0' || str.charAt(start + i - 1) > '9') {
break;
}
String tempStr = str.substring(start, start + i);
int preNum = Integer.valueOf(tempStr);
path.add(preNum);
helper(result, path, str, start + i + preNum, end);
path.remove(path.size() - 1);
}
}
三、子集问题
组合问题和分割问题都是收集树的叶子节点,而子集问题是收集所有节点。
子集问题是无序的,取过的元素不能重复,所以for层的时候也要从startIndex开始。
什么时候for可以从0开始?----排列问题的时候
/**
* 四、子集问题
* 给定一组【不含重复元素】的整数数组 nums,返回该数组所有可能的子集(幂集)
* 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
*
* 【递归函数参数】:result,path,startIndex,原始数组nums
* 【递归终止条件】:剩余集合为空的时候就是叶子节点,当startIndex >= nums.length的时候
* 【单层逻辑】:子集问题不需要剪枝
*/
public static List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
if (nums.length == 0) {
return result;
}
subsetsHelper(result, path, nums, 0);
return result;
}
private static void subsetsHelper(List<List<Integer>> result, LinkedList<Integer> path, int[] nums, int startIndex) {
result.add(new ArrayList<>(path));
if (startIndex >= nums.length) {
return;
}
for (int i = startIndex; i < nums.length; i++) {
path.add(nums[i]);
subsetsHelper(result, path, nums, startIndex + 1);
path.removeLast();
}
}
/**
* 给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)
* 输入: [1,2,2]
* 输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]
* 给定的数组包含重复元素,但子集不能重复
* 【同一树层上不能取重复元素】:前面组合问题也有同一树层元素不能重复的情况
* 因为树层的宽度是在整数数组nums中选择,这里又是同一个组合中元素可以重复,但是不同组合中不能重复排列
* 【递归函数参数】:result, path, nums, startIndex
* 【递归终止条件】:和上面的一样 startIndex >= nums.length就return
* 【单层遍历逻辑】:去重操作,先将nums排序,for遍历时遇到相同的跳过
* 【本题可以不使用used数组】:如果是全排列,需要从0开始,要used数组记录哪个没有遍历过
* used[i - 1] = true说明同一树枝使用过
* used[i - 1] = false说明同一树层使用过,要对同一树层使用过的元素跳过
*/
public static List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
Arrays.sort(nums);
subsetsWithDupHelper(result, path, nums, 0);
return result;
}
private static void subsetsWithDupHelper(List<List<Integer>> result, LinkedList<Integer> path,
int[] nums, int startIndex) {
result.add(new ArrayList<>(path));
if (startIndex >= nums.length) {
return;
}
// 因集合中取过的元素不能再次选取,因此要从startIndex开始
for (int i = startIndex; i < nums.length; i++) {
if (i > startIndex && nums[i - 1] == nums[i]) {
continue;
}
path.add(nums[i]);
subsetsWithDupHelper(result, path, nums, startIndex + 1);
path.removeLast();
}
}
/**
* !!!递增子序列
* 给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
* 输入: [4, 6, 7, 7]
* 输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
* 给定数组的长度不会超过15。
* 数组中的整数范围是 [-100,100]。
* 给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况
*
* 【画出树形结构,被删除的路径有两种】:在树层上,如果同一父节点下重复使用同value的后面的元素,去掉
* 在树枝上,如果当前元素小于(题目中大于等于为递增)path最后一个元素,去掉
* 【递归函数参数】:result,path,num,重复元素不能使用,所以有个startIndex
* 【递归终止条件】:本题要求size起码要是2,所以当path.size()再加入到结果中
* startIndex >= nums.length
* 去重不能sort了,会打乱原有数组的顺序
* 【单层搜索逻辑】:同一父节点下的同层上使用过的元素就不能在使用了
*/
public static List<List<Integer>> findSubsequences(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
findSubsequencesHelper(result, path, nums, 0);
return result;
}
private static void findSubsequencesHelper(List<List<Integer>> result, LinkedList<Integer> path,
int[] nums, int startIndex) {
if (path.size() > 1) {
result.add(new ArrayList<>(path));
}
if (startIndex >= nums.length) {
return;
}
// 数组中数字的范围,最多有201个数可选来组成nums
int[] used = new int[201];
for (int i = startIndex; i < nums.length; i++) {
if ((!path.isEmpty() && nums[i] < path.peekLast()) || (used[nums[i] + 100] == 1)) {
// 同树层去重操作
continue;
}
// used是同层使用的,针对for循环的下个元素,不针对树枝
used[nums[i] + 100] = 1;
path.add(nums[i]);
// startIndex为树枝上的用过的元素不能再使用
findSubsequencesHelper(result, path, nums, startIndex + 1);
path.removeLast();
}
}
四、排列问题
排列是有序的,这点和子集问题不一样,[1, 2]和[2, 1]是两个不同的排列。
所以排列问题不用使用startIndex。但排列问题需要一个 used数组标记已经使用过的元素。
树层级的used数组去重:在递归函数中创建,每次进入新的树枝都会刷新。
树枝级的used数组去重:在递归函数外部创建,对应nums。
/**
* 原始数组中没有重复数据的全排列
* 给定一个 没有重复 数字的序列,返回其所有可能的全排列
* 输入: [1,2,3]
* 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
*
* 【递归函数参数】:result,path,nums,used数组标记已经选择的元素
* 因为是全排列,每次都要从头搜索,for层的时候就不从startIndex开始了,从0开始
* 【递归终止条件】:当path.size()和nums.length一样大的时候就是找到了一个全排列的结果
* 【单层搜索逻辑】:used元素在for循环中记录哪些元素使用过了,在树枝遍历的时候就不能再使用了,
* used用来做树枝的去重,所以used数组要一起传下去,之前used用来做树层的去重
*/
public static List<List<Integer>> permute(int[] nums) {
if (nums.length == 0) {
return new ArrayList<>();
}
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
boolean[] used = new boolean[nums.length];
permuteHelper(result, path, nums, used);
return result;
}
private static void permuteHelper(List<List<Integer>> result, LinkedList<Integer> path,
int[] nums, boolean[] used) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) {
continue;
}
path.add(nums[i]);
used[i] = true;
permuteHelper(result, path, nums, used);
//【回溯操作】
used[i] = false;
path.removeLast();
}
}
/**
* 原始数组中有重复数据的全排列
* 给定一个【可包含重复数字】的序列 nums ,按任意顺序 返回所有【不重复的全排列】。
* 输入:nums = [1,1,2]
* 输出: [[1,1,2], [1,2,1], [2,1,1]]
*
* 【涉及去重】:一定要对元素进行排序,方便通过相邻节点来判断是否重复使用
* 同一个树层上不能有两个相同的元素
* 【树层去重】:i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false 效率更高
* 【树枝去重】:i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true 会做很多没有什么用的计算
*/
public static List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
boolean[] used = new boolean[nums.length];
// 通过相邻元素来判断是否重复使用的基础
Arrays.sort(nums);
permuteUniqueHelper(result, path, nums, used);
return result;
}
private static void permuteUniqueHelper(List<List<Integer>> result, LinkedList<Integer> path,
int[] nums, boolean[] used) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
if (used[i] == false) {
// 如果同一树枝nums[i]没用过
used[i] = true;
path.add(nums[i]);
permuteUniqueHelper(result, path, nums, used);
path.removeLast();
used[i] = false;
}
}
}
应用题
/**
* 给定一个机票的字符串二维数组 [from, to],
* 子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。
* 所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。
*
* 输入:[["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]]
* 输出:["JFK", "MUC", "LHR", "SFO", "SJC"]
*
* 输入:[["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
* 输出:["JFK","ATL","JFK","SFO","ATL","SFO"]
* 解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"]。但是它自然排序更大更靠后。
*
* 【递归函数参数】:target记录航班映射关系的map,ticketNum表示航班数
* 【终止条件】:我们回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1),
* 那么我们就找到了一个行程,把所有航班串在一起了
* 【单层搜索逻辑】:
*/
public static List<String> findItinerary(List<List<String>> tickets) {
// 使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了。
Map<String, Map<String, Integer>> map = new HashMap<>();
LinkedList<String> result = new LinkedList<>();
for (List<String> t : tickets) {
// 计算每个起飞机场有几个目的机场,且分别能去几次
Map<String, Integer> temp;
if (map.containsKey(t.get(0))) {
temp = map.get(t.get(0));
temp.put(t.get(1), temp.getOrDefault(t.get(1), 0) + 1);
} else {
temp = new TreeMap<>();
temp.put(t.get(1), 1);
}
map.put(t.get(0), temp);
}
result.add("JFK");
backTracking(map, result, tickets.size());
return new ArrayList<>(result);
}
private static boolean backTracking(Map<String, Map<String, Integer>> map,
LinkedList<String> result, int ticketNum) {
if (result.size() == ticketNum + 1) {
return true;
}
String last = result.getLast();
if (map.containsKey(last)) {
for(Map.Entry<String, Integer> target : map.get(last).entrySet()){
int count = target.getValue();
if (count > 0) {
// 如果“航班次数”大于零,说明目的地还可以飞,
// 如果如果“航班次数”等于零说明目的地不能飞了,
// 而不用对集合做删除元素或者增加元素的操作
result.add(target.getKey());
target.setValue(count - 1);
if (backTracking(map, result, ticketNum)) {
return true;
}
result.removeLast();
target.setValue(count);
}
}
}
return false;
}
n皇后问题
/**
* n皇后问题
* n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
* 各个皇后
* 不能同行
* 不能同列
* 不能同斜线
*
* 搜索过程也可以抽象为一棵树,不符合要求的就舍去
* 【递归函数参数】:定义二维数组记录结果result,n是棋盘大小,row是遍历到第几行
* 【递归终止条件】:遍历到叶子节点的时候就可以终止了
* 【单层搜索的逻辑】:递归深度是row的行,col是for中循环的
* 因为每次都是从新一行的起始位置开始搜,所以每个for循环都是从0开始
* 【检验棋盘是否合法】:同行不用检查,因为for循环里都是在一行里面,所以不用检查
*/
public static List<List<String>> solveNQueues(int n) {
List<List<String>> res = new ArrayList<>();
char[][] chessboard = new char[n][n];
for (char[] c : chessboard) {
Arrays.fill(c, '.');
}
solveHelper(res, n, 0, chessboard);
return res;
}
private static void solveHelper(List<List<String>> res, int n,
int row, char[][] chessboard) {
if (row == n) {
res.add(Array2List(chessboard));
return;
}
for (int col = 0; col < n; col++) {
if (isValid(row, col, n, chessboard)) {
chessboard[row][col] = 'Q';
solveHelper(res, n, row + 1, chessboard);
chessboard[row][col] = '.';
}
}
}
private static List Array2List(char[][] chessboard) {
List<String> list = new ArrayList<>();
for (char[] c : chessboard) {
list.add(String.copyValueOf(c));
}
return list;
}
private static boolean isValid(int row, int col, int n, char[][] chessboard) {
// 检查列
for (int i = 0; i < row; i++) { // 相当于剪枝
if (chessboard[i][col] == 'Q') {
return false;
}
}
// 检查45度对角线
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
// 检查135度对角线
for (int i = row - 1, j = col + 1; i >= 0 && j <= n-1; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
数独问题
/**
* 数独问题
*
* 【二维递归】
* n皇后每行只放一个皇后,所以树形结构的规模比数独的小很多
* 【递归函数参数和返回值】:返回值类型是boolean,因为只需要找到一个结果就返回即可
* 【递归终止条件】:不用终止条件,因为要遍历整个树形结构,直到返回true
* 【for遍历】:一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,
* 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!
*/
public static void solveSudoku(char[][] board) {
solveSudokuHelper(board);
}
private static boolean solveSudokuHelper(char[][] board){
//「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,
// 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」
for (int i = 0; i < 9; i++){ // 遍历行
for (int j = 0; j < 9; j++){ // 遍历列
if (board[i][j] != '.'){ // 跳过原始数字
continue;
}
for (char k = '1'; k <= '9'; k++){ // (i, j) 这个位置放k是否合适
if (isValidSudoku(i, j, k, board)){
board[i][j] = k;
if (solveSudokuHelper(board)){ // 如果找到合适一组立刻返回
return true;
}
board[i][j] = '.';
}
}
// 9个数都试完了,都不行,那么就返回false
return false;
// 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!
// 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」
}
}
// 遍历完没有返回false,说明找到了合适棋盘位置了
return true;
}
private static boolean isValidSudoku(int row, int col, char val, char[][] board){
// 同行是否重复
for (int i = 0; i < 9; i++){
if (board[row][i] == val){
return false;
}
}
// 同列是否重复
for (int j = 0; j < 9; j++){
if (board[j][col] == val){
return false;
}
}
// 9宫格里是否重复
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++){
for (int j = startCol; j < startCol + 3; j++){
if (board[i][j] == val){
return false;
}
}
}
return true;
}
Leetcode 剑指offer-46把数字翻译成字符串
/**
* 剑指offer 把数字翻译成字符串
* 给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。
* 一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
*
* 输入: 12258
* 输出: 5
* 解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"
* 感觉和电话号码的组合问题很像
* 递归的深度就是"12258"的长度,组合的结果就是叶子节点
* 【确定回溯参数】:字符串s收集叶子节点的结果,字符串数组result保存结果
* index记录在遍历第几个数字,同时也表示树的深度
* 【终止条件】:index = digits.length就收集结果
* 【单层遍历逻辑】:取到index对应的数字,然后找到对应的字母集合
*/
public int translateNum(int num) {
// num转为string
String digits = String.valueOf(num);
List<String> result = new ArrayList<>();
String[] numDict = {"a", "b", "c", "d", "e", "f", "g", "h",
"i", "j", "k", "l", "m", "n", "o", "p",
"q", "r", "s", "t", "u", "v", "w", "x", "y", "z"};
translateNumHelper(result, new StringBuilder(), digits, 0, numDict);
return result.size();
}
private void translateNumHelper(List<String> result, StringBuilder stringBuilder, String digits,
int index, String[] numDict) {
if (index == digits.length()) {
result.add(stringBuilder.toString());
return;
}
// 这里有个重要不同就是,当前index必然可以一次翻译一位
String str = numDict[digits.charAt(index) - '0'];
stringBuilder.append(str);
translateNumHelper(result, stringBuilder, digits, index + 1, numDict);
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
// 也可以通过条件判断是否可以一次翻译两位
if (index + 1 < digits.length() &&
(digits.charAt(index) == '1' || (digits.charAt(index) == '2' && digits.charAt(index + 1) <= '5'))) {
String str2 = numDict[(digits.charAt(index) - '0') * 10 + (digits.charAt(index + 1) - '0')];
stringBuilder.append(str);
translateNumHelper(result, stringBuilder, digits, index + 2, numDict);
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
}
}
/**
* 其实上面这道题还可以简化,因为题目中并没有要求写出所有的情况,只是要求一个所有可能情况的个数和,所以result不是必须的
*/
private int method = 0;
public int translateNumOptimized(int num) {
// num转为string
String digits = String.valueOf(num);
String[] numDict = {"a", "b", "c", "d", "e", "f", "g", "h",
"i", "j", "k", "l", "m", "n", "o", "p",
"q", "r", "s", "t", "u", "v", "w", "x", "y", "z"};
translateNumHelperOptimized(digits, 0);
return method;
}
private void translateNumHelperOptimized(String digits, int index) {
if (index == digits.length()) {
method++;
return;
}
// 这里有个重要不同就是,当前index必然可以一次翻译一位
translateNumHelperOptimized(digits, index + 1);
// 也可以通过条件判断是否可以一次翻译两位
if (index + 1 < digits.length() &&
(digits.charAt(index) == '1' || (digits.charAt(index) == '2' && digits.charAt(index + 1) <= '5'))) {
translateNumHelperOptimized(digits, index + 2);
}
}