一、组合问题
组合问题里集合是无序的,取过的元素不会重复取,所以for循环遍历的时候要从startIndex开始。排列问题可以重复,从0开始。
77. 组合
集合中没有重复的元素,元素只能使用一次。解集不能包含重复的组合。
var combine = function(n, k) {
// result存放符合条件的所有结果,path存放单一结果
let result = [], path = [];
// 递归函数,遍历决策二叉树。从集合n里取k个数,n决定了树的宽度,k决定了树的深度
function backtrack(n,k,startIndex){
// 终止条件,即到达所谓的叶子节点(path里的长度为k),收集结果。
if(path.length == k){
result.push([...path])
return
}
// 单层搜索逻辑,for循环用于横向遍历,递归用于纵向遍历
for(let i=startIndex;i<=n;i++){
path.push(i) // 做出选择
backtrack(n,k,i+1) // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
path.pop() // 回溯,撤销选择
}
}
backtrack(n,k,1)
return result
};
39. 组合总和
集合中没有重复的元素,但是元素可以使用多次。解集不能包含重复的组合。
var combinationSum = function(candidates, target) {
let result = [], path = [];
// 1、确定递归函数的参数:题目中的参数+遍历的起始位置+path中的和
function backtrace(candidates,target,startIndex,sum){
// 2、终止条件
if(sum>target){
return
}
if(sum == target){
result.push([...path])
return
}
// 3、单层搜索
for(let i=startIndex; i<candidates.length; i++){
sum += candidates[i] // 计算做出选择后的总和
path.push(candidates[i]) // 做出选择
backtrace(candidates,target,i,sum) // 纵向可以重复选择,不需要i+1
sum -= candidates[i] // 回溯
path.pop()
}
}
backtrace(candidates,target,0,0)
return result
};
40. 组合总和 II (去重操作)
集合中有重复的元素,每个元素只能使用一次,但是解集不能包含重复的组合。所以要去重的是“同一树层上的使用过”。
var combinationSum2 = function(candidates, target) {
let result = [], path = [];
// used布尔型数组,记录元素使用情况,从而判断是在同一树枝还是树层
const used = new Array(candidates.length).fill(false)
// 1、确定递归函数的参数:题目中的参数+遍历的起始位置+path中的和+
function backtrace(candidates,target,startIndex,sum,used){
// 2、终止条件
if(sum>target){
return
}
if(sum == target){
result.push([...path])
return
}
// 3、单层搜索
for(let i=startIndex; i<candidates.length; i++){
// 要对同一树层使用过的元素进行跳过
if(i>0 && candidates[i] == candidates[i-1] && used[i-1] == false){
continue;
}
sum += candidates[i] // 计算做出选择后的总和
path.push(candidates[i]) // 做出选择
used[i] = true
backtrace(candidates,target,i+1,sum,used) // 纵向可以重复选择,不需要i+1
used[i] = false
sum -= candidates[i] // 回溯
path.pop()
}
}
// 首先把给candidates排序,让其相同的元素都挨在一起。
candidates.sort((a,b) => a-b)
backtrace(candidates,target,0,0,used)
return result
};
17. 电话号码的字母组合
和上面的组合问题不同,这里对应了不同数量的组合。横向是遍历其中一个组合,所以i从0开始。纵向的回溯是用于进入下一个组合里,所以此时需要的是控制不同组合的index。
var letterCombinations = function(digits) {
// 定义字符数组,每个下标代表从0~9号码对应的字符
const letterMap = ["","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"]
let result = [], path = [];
if(!digits.length) return [] // 空字符情况
// 1、参数:digits + 记录遍历到digits里第几个数字 index
function backtrack(digits,index){
// 2、终止条件:比如23就是遍历两层
if(index == digits.length){
result.push(path.join("")) // 在这里把数组转换成字符串
return
}
// 3、单层遍历逻辑
let num = digits[index] - '0' // 将index指向的数字字符'2'转为int
let letters = letterMap[num] // 拿到数字对应的字符集'abc'
for(let i=0;i<letters.length;i++){ // 遍历拿到的字符集
path.push(letters[i])
backtrack(digits,index+1) // 处理下一次数字'3'
path.pop()
}
}
backtrack(digits,0)
return result
};
二、子集问题
78. 子集
组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!所以result.push([…path])无需判断每次递归都要收集。
子集也是一种组合问题,无序,for就要从startIndex开始,而不是从0开始!
var subsets = function(nums) {
let result = [], path = [];
function backtrack(nums,startIndex){
// 单层递归结束条件,剩余集合为空,就是for循环结束条件
if(startIndex > nums.length) return
// 子集问题每一层都要收集
result.push([...path])
// 单层搜索逻辑
for(let i=startIndex;i<nums.length;i++){
path.push(nums[i])
backtrack(nums,i+1)
path.pop()
}
}
backtrack(nums,0)
return result
};
三、排列问题
46. 全排列
// 由于是排列,所以[1,2]和[2,1]是两个集合。不需要startIndex
var permute = function(nums) {
let result = [], path = []
const used = new Array(nums.length).fill(false)
// 1、确定参数:nums + used
function backtrack(nums,used){
// 2、终止条件
if(path.length == nums.length){
result.push([...path])
}
// 3、单层搜索
for(let i=0; i<nums.length; i++){
// 由于没有startIndex控制横向for循环的次序,所以需要使用used数组。
// used为true,代表已经遍历,跳过
if(used[i] == true) continue
used[i] = true
path.push(nums[i])
backtrack(nums,used)
used[i] = false
path.pop()
}
}
backtrack(nums,used)
return result
};
47. 全排列 II
题目 : 给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
解析:和40. 组合总和 II 一样,由于序列有重复数字,所以需要去重。去重是树层的重复:if(i > 0 && nums[i] == nums[i-1] && used[i-1] == false) continue,并且还需要排序
var permuteUnique = function(nums) {
let result = [], path = []
const used = new Array(nums.length).fill(false)
// 1、确定参数:nums + used
function backtrack(nums,used){
// 2、终止条件
if(path.length == nums.length){
result.push([...path])
}
// 3、单层搜索
for(let i=0; i<nums.length; i++){
// 由于没有startIndex控制横向for循环的次序,所以需要使用used数组。
// 去重操作:同一树层的需要跳过
if(i > 0 && nums[i] == nums[i-1] && used[i-1] == false) continue
// 没有遍历过的才需要,对应used[i]为true时跳过
if(used[i] == false){
used[i] = true
path.push(nums[i])
backtrack(nums,used)
path.pop()
used[i] = false
}
}
}
// 需要排序
nums.sort((a,b) => a-b)
backtrack(nums,used)
return result
};
四、分割问题
131、分割回文串
切割问题的回溯搜索的过程和组合问题的回溯搜索的过程是差不多的
// 判断回文子串
var isPalindrome = function(s,start,end){
for(let i=start,j=end; i<j; i++,j--){
if(s[i] != s[j]) return false
}
return true
}
var partition = function(s) {
// path存放分割后的子串
let path = [],result = [];
// 递归函数
function backtrack(s,startIndex){
// 单层递归(每个竖向分支)结束条件:切割线切到字符串最后
if(startIndex >= s.length){
result.push(Array.from(path));
}
// 单层搜索逻辑
for(let i=startIndex;i<s.length;i++){
if(isPalindrome(s,startIndex,i)){ // 是回文子串
// 获取[startIndex,i]在s中的子串
let str = s.slice(startIndex,i+1)
path.push(str)
}else{
continue // 跳过
}
backtrack(s,i+1)
path.pop()
}
}
backtrack(s,0)
return result
};
93、复原IP地址
var restoreIpAddresses = function(s) {
// path为分割后的字符,res存放最后所有结果
let res = [], path = []
function backtrack(s,startIndex){
// 单层递归结束条件:必须分割为4份且已经遍历完字符串
const len = path.length
if(len > 4) return
if(len == 4 && startIndex == s.length){
res.push(path.join("."))
return
}
// 单层搜索逻辑
for(let i=startIndex;i<s.length;i++){
// 拿到截取的子串
const str = s.slice(startIndex,i+1)
// 字符长度不能超过3,不能超过255
if(str.length > 3 || str > 255) break
// 0x/0xx这种异常,开头不能为0
if(str.length > 1 && str[0] == '0') break
path.push(str)
backtrack(s,i+1)
path.pop()
}
}
backtrack(s,0)
return res
};