回溯
是什么
穷举(列举所有情况),时间和空间复杂度很高,但是已经是最好的办法了
- 一种算法思想
- 一种渐进式寻找并构建问题解决方式的策略
会先从一个可能的动作开始path.push()解决问题,如果不行,就回溯并选择另一个动作path.pop(),直到将问题解决
模板
path不能重复选择nums里的元素
function fn(nums) { const res = [] const dfs = (start, path) => { if (path.length === nums.length) { //终结条件 res.push([...path]) return } //如果不能重复选择nums里的元素,则+1 for (let i = start; i < nums.length; i++) { path.push(nums[i]) dfs(i + 1, path) path.pop() } } dfs(0, []) return res; }
path能重复选择nums里的元素
function fn(nums) { const res = [] const dfs = (start, path) => { if (path.length === nums.length) { //终结条件 res.push([...path]) return } //如果不能重复选择nums里的元素,则直接传i for (let i = start; i < nums.length; i++) { path.push(nums[i]) dfs(i, path) path.pop() } } dfs(0, []) return res; }
案例: 鬼吹灯寻路
leetcode
46 全排列
思路
所有情况(出路死路)
过滤后的所有情况
代码
写法一
function premute(nums) {
// 1. 设置结果集
const res = []
// 2. 回溯
const backtrack = (path) => {
// 2.1 设置回溯终止条件
if(path.length === nums.length){
// 2.1.1 推入结果集
res.push(path)
// 2.1.2 终止递归
return;
}
// 2.2 遍历数组
nums.forEach(n => {
let pathIncludesN = path.includes(n)
// 2.2.1 必须是不存在 数组 中的元素
if(pathIncludesN){
return
}
// 2.2.2 本地递归条件
let pathConcatN = path.concat(n)
// 2.2.3 进一步递归
backtrack(pathConcatN)
})
}
backtrack([])
// 3. 返回结果
return res;
}
var nums = [1, 2]
// var nums = [1, 2,3]
var result = premute(nums)
console.log('result', result)
写法二(推荐)
function permute(nums){
const result = []
const dfs = function(path){
if(path.length === nums.length){
result.push([...path])
return;
}
for(let i =0;i<nums.length;i++){
if(!path.includes(nums[i])){
path.push(nums[i])
dfs(path)
path.pop(nums[i])
}
}
}
dfs([])
return result;
}
复杂度分析
- 时间复杂度:O(n∗n!),其中 n 为序列的长度
- 空间复杂度:O(n),其中 n 为序列的长度。除答案数组以外,递归函数在递归过程中需要为每一层递归函数分配栈空间,所以这里需要额外的空间且该空间取决于递归的深度,这里可知递归调用深度为O(n)。
参考
39 组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
输入:candidates = [2,3,6,7], target = 7,
所求解集为: [ [7], [2,2,3] ]
function combinationSum(candidates, target) {
const res = []
const dfs = (start, path, sum) => {
if (sum >= target) {
if (sum == target) {
res.push([...path])
}
return
}
for (let i = start; i < candidates.length; i++) {
path.push(candidates[i])
dfs(i, path, sum + candidates[i])
path.pop()
}
}
dfs(0, [], 0)
return res;
}
40组合总和2
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
注意:解集不能包含重复的组合。
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出: [ [1,1,6], [1,2,5], [1,7], [2,6] ]
function combinationSum2(candidates, target) {
candidates.sort((a, b) => a - b)
const res = []
const dfs = (start, path, sum) => {
if (sum >= target) {
if (sum == target) {
res.push([...path])
}
}
for (let i = start; i < candidates.length; i++) {
if (i > start && candidates[i - 1] == candidates[i]) {
continue
} //有candidates[i - 1] == candidates[i],证明是有2个数,假设start = 0; 则i >0,就只能是从下标1开始,就能凑够2个数
path.push(candidates[i])
dfs(i + 1, path, sum + candidates[i])
path.pop()
}
}
dfs(0, [], 0)
return res;
}
216组合总和3
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
function combinationSum3(k, n) {
const res = []
const dfs = (start, path, sum) => {
if (path.length == k && sum === n) {
res.push([...path])
}
for (let i = start; i <= 9; i++) {
path.push(i)
dfs(i + 1, path, i + sum)
path.pop()
}
}
dfs(1, [], 0)
return res;
}
46全排列
给定一个不重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
function subsets(nums) {
const res = []
const dfs = (path) => {
if (path.length === nums.length) {
res.push([...path])
}
for (let i = 0; i < nums.length; i++) {
if (path.includes(nums[i])) continue
path.push(nums[i])
dfs(path)
path.pop()
}
}
dfs([])
return res;
}
47 全排列2
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
输入:nums = [1,1,2]
输出: [[1,1,2], [1,2,1], [2,1,1]]
function permuteUnique(nums) {
const res = []
nums.sort((a, b) => a - b)
const used = new Array(nums.length)
const dfs = (path) => {
if (path.length === nums.length) {
res.push([...path])
}
for (let i = 0; i < nums.length; i++) {
if (used[i]) {
continue
}
if (i > 0 && nums[i - 1] == nums[i] && !used[i - 1]) {
continue
}
path.push(nums[i])
used[i] = true;
dfs(path)
path.pop()
used[i] = false
}
}
dfs([])
return res;
}
78 子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
function subsets(nums) {
const res = []
const dfs = (start, path) => {
if (path.length <= nums.length) {
res.push([...path])
}
for (let i = start; i < nums.length; i++) {
path.push(nums[i])
dfs(i + 1, path)
path.pop()
}
}
dfs(0, [])
return res;
}
90 子集2
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
var subsetsWithDup = function (nums) {
nums = nums.sort((a, b) => { return a - b });
const res = [];
const used = []
const dfs = (start, path) => {
if (path.length <= nums.length) {
res.push([...path])
}
for (let i = start; i < nums.length; i++) {
if (i > 0 && nums[i - 1] == nums[i] && !used[i - 1]) continue;
// if(used[i] || (i > 0 && nums[i-1] == nums[i] && !used[i-1])) continue;
used[i] = true;
path.push(nums[i]);
dfs(i + 1, path);
used[i] = false;
path.pop();
}
}
dfs(0, []);
return res;
};
77组合
给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
输入: n = 4, k = 2
输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
function combine(n, k) {
const res = []
const dfs = (start, path) => {
if (path.length == k) {
res.push([...path])
}
for (let i = start; i <= n; i++) {
path.push(i)
dfs(i + 1, path)
path.pop()
}
}
dfs(1, [])
return res;
}
93 有效ip
给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。
例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]
const restoreIpAddresses = (s) => {
const res = [];
// 复原从start开始的子串
const dfs = (subRes, start) => {
if (subRes.length == 4 && start == s.length) { // 片段满4段,且耗尽所有字符
res.push(subRes.join('.')); // 拼成字符串,加入解集
return; // 返不返回都行,指针已经到头了,严谨的说还是返回
}
if (subRes.length == 4 && start < s.length) { // 满4段,字符未耗尽,不用往下选了
return;
}
for (let len = 1; len <= 3; len++) { // 枚举出选择,三种切割长度
if (start + len - 1 >= s.length) return; // 加上要切的长度就越界,不能切这个长度
if (len != 1 && s[start] == '0') return; // 不能切出'0x'、'0xx'
const str = s.substring(start, start + len); // 当前选择切出的片段
if (len == 3 && +str > 255) return; // 不能超过255
subRes.push(str); // 作出选择,将片段加入subRes
dfs(subRes, start + len); // 基于当前选择,继续选择,注意更新指针
subRes.pop(); // 上面一句的递归分支结束,撤销最后的选择,进入下一轮迭代,考察下一个切割长度
}
};
dfs([], 0); // dfs入口
return res;
};
491 递增子序列
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是 2 。
输入:[4, 2, 7, 7]
输出:[[4,7],[4,7,7],[2,7],[2,7,7],[7,7]]
const findSubsequences = (nums) => {
const res = [];
const len = nums.length;
const set = new Set();
const dfs = (start, path) => {
if (path.length >= 2) {
const str = path.toString(); // path数组 转成字符串
if (!set.has(str)) { // set中没有存有当前path
res.push(path.slice()); // 推入一份path的拷贝
set.add(str); // 存入set,记录一下
}
}
for (let i = start; i < len; i++) { // 枚举出当前所有的选项,从start到末尾
const prev = path[path.length - 1]; // 上一个选择,即path数组的末尾元素
const cur = nums[i]; // 当前选择
if (path.length == 0 || prev <= cur) { //
path.push(cur); // 选择当前的数如果path为空,或满足递增关系,则可选择字
dfs(i + 1, path); // 继续往下递归,注意传的是i+1
path.pop(); // 撤销选择当前数字,选择别的数字
}
}
};
dfs(0, []); //递归的入口,从下标0到末尾的数组中选择合适的数加入path,组成解集。初始path是空数组
return res;
};
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。