题目一览图
零、搜索算法概述
【搜索搜索】本质是一个决策树的遍历问题,我们只需要考虑三个问题:
- 【路径】已经做出的选择。
- 【选择列表】当前可以做的选择。
- 【结束条件】达到决策树的底端。
一、暴力搜索
题目概述**
【暴力搜索】与后面【回溯算法】的区别在于,【暴力搜索】每一次选择相互独立,即选择列表相互不冲突。
var fun = function(nums) {
const res = [];
const backtrack = (idx,path)=>{
if( check(idx) ) res.push(path);
else backtrack(idx+1,changePath(path))
}
backtrack(0,'');
return res;
};
题目详解
1.1.1 电话号码的字母组合
【概述】 搜索
【题目描述**】
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
【题目分析**】本题的目的是将数字翻译成对应的字符,而这种映射关系不唯一,所以需要遍历。
- 【选择列表**】**每个选择列表相互独立,对应的字母互不相同。
- 【结束条件**】**序号
idx
等于长度时,说明数字和字符已经一一对应。 【路径更新**】**选一种映射方式,并将其添加至路径
path
的末尾。var letterCombinations = function(digits) {
if (digits.length == 0) return [];
const res = [];
const strs = ['','abc','def','ghi','jkl','mno','pqrs','tuv','wxyz'];
const backtrack = (idx,path)=>{
if(idx === digits.length) res.push(path);
else for(let c of strs[digits[idx]-'1']) backtrack(idx+1,path+c);
}
backtrack(0,'');
return res;
};
1.1.2 括号生成
【概述】 搜索 括号
【题目描述**】
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
【题目分析**】本题的目的是给出所有有效括号组合,所以我们要通过括号规则造长度为2n
的组合。【选择列表**】**每个选择列表相互独立,都是左右括号选其一。
- 【结束条件**】**右括号数
r
等于n
时,说明已经生成n
对括号。 【**路径更新**】
- 左括号数
l
小于n
,说明还可以加左括号path+(
。 - 右括号数
r
小于n
且小于l
,说明还可以加右括号path+)
。var generateParenthesis = function(n) {
const res = [];
const backtrack = (l,r,path)=>{
if( l < n ) backtrack(l+1,r,path+'(');
if( r < n && r < l ) backtrack(l,r+1,path+')');
if( r === n ) res.push(path);
}
backtrack(0,0,'');
return res;
};
1.1.3 组合总和
【概述】 搜索 有无
【题目描述**】
给定一个无重复元素的数组candidates
和一个目标数target
,找candidates
中所有可以使数字和为target
的组合。candidates
中的数字可以无限制重复被选取。
【题目分析**】本题的目的是给出数字和为target
的组合。
- 左括号数
【选择列表**】**
candidates
中的数字,但是可以无限次拿取,所以选择之间独立。- 【结束条件**】**
- 达到目标数
target
,满足要求输出结果。 candidates
数组遍历完,但未达到目标数target
,直接跳过。
- 达到目标数
【**路径更新**】
- 【选当前数】只要当前目标数还能被【当前数】减,就可以一直添加到路径。
【不选当前数】直接读取下一个数,不对路径操作。
var combinationSum = function(candidates, target) {
const res = [];
const backtrack = (index,target,path)=>{
if( target === 0 ) {
res.push(path);
return;
}
if( index === candidates.length) return;
backtrack(index+1,target,path);
// 选择当前数
if (target - candidates[index] >= 0) {
backtrack(index, target - candidates[index], [...path, candidates[index]]);
}
}
backtrack(0,target,[]);
return res;
};
二、回溯算法
题目概述**
【回溯算法】与后面【暴力搜索】的区别在于,【回溯算法】每一次选择相互不独立,每一次选择都会影响后续选择。
var fun = function(choices) {
const n = choices.length;
const res = [] , mono = new Array(n).fill(false);
const backtrack = (idx,path)=>{
if( check(idx) ){
res.push(path);
return;
}
for( let choice of choices){
if(!mono[i]) continue;
do();
backtrack(idx+1,path)
undo();
}
}
backtrack(0,[]);
return res;
};
题目详解
1.1.1 全排列
【概述】回溯算法 完整遍历
【题目描述**】
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
【题目分析**】
- 【选择列表】
nums
中【未被使用过】的数字,需要用【备忘录】记录。 - 【结束条件】
idx === nums.length
。 【**路径更新**】选择没有被【备忘录】记录过的数字。
const permute = (nums)=>{
const mono = new Array(m) , res = [];
const backtrack = (idx, path)=>{
if( idx === nums.length ) {
res.push([...path]);
return;
}
for(let i = 0 ; i < nums.length ; i++) {
if( mono [i] ) continue;
mono[i] = true;
backtrack(idx+1,[...path,nums[i]]);
mono[i] = false;
}
}
backtrack(0,[]);
return res;
}
1.1.2 全排列 II
【概述】回溯算法 完整遍历
【题目关系】全排列 - 增加了重复字符的条件。
【题目描述**】
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
【题目分析**】为了寻找相同元素,需要先排序让相同数字相邻。
- 【选择列表】
nums
中【未被使用过】的数字,需要用【备忘录】记录。 - 【结束条件】
idx === nums.length
。 【**路径更新**】选择没有被【备忘录】记录过的数字。
【重复字符处理】先排序让相同数字相邻,然后通过
nums[i] === nums[i-1]
判断是否重复。但是在全排序中,相同元素也是有顺序的。那如何去确保这个顺序?每次都确保前一个重复元素被选择过,即!mono[i-1]
。var permuteUnique = function(nums) {
const res = [] , mono = new Array(m).fill(false);
nums.sort((a,b)=>a-b);
const backtrack = (idx,path)=>{
if(idx===nums.length){
res.push([...path]);
return;
}
for(let i = 0 ; i < nums.length ; i++){
if( mono[i] || i && nums[i] === nums[i-1] && !mono[i-1] ) continue;
mono[i] = true;
backtrack(idx+1,[...path,nums[i]]);
mono[i] = false;
}
}
backtrack(0,[]);
return res;
};
1.1.3 排列序列
【概述】回溯算法 完整遍历
【题目关系】
全排列 - 寻找全排序中的第
k
个。
【题目描述**】
给出集合[1,2,3,...,n]
,其所有元素共有 n!
种排列。给定 n
和 k
,返回第 k
个排列。
【题目分析**】本题要求输出所有全排序中第 k
个元素。给全排序加个计数功能即可。
- 【选择列表】
nums
中【未被使用过】的数字,需要用【备忘录】记录。 - 【结束条件】
idx === nums.length
&& 第k
次找到符合的全排序。 【**路径更新**】选择没有被【备忘录】记录过的数字。
var getPermutation = function(n, k) {
const mono = new Array(n).fill(false);
let count = 0 , res = '';
const backtrack = (idx,path) =>{
if( count > k ) return;
if( idx === n ){
count++;
if(count===k) res = path.join('');
return;
}
for( let i = 1 ; i <=n ; i++){
if( mono[i] ) continue;
mono[i] = true;
backtrack(idx+1,[...path,i]);
mono[i] = false;
}
}
backtrack(0,[]);
return res;
};
1.1.4 复原IP地址
【概述】回溯算法 完整遍历 分割字符串
【题目关系】全排列 - 结束条件更为复杂。
【题目描述**】
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
【题目分析**】本题要求输出所有可能的 IP
地址格式。
- 【选择列表】索引
idx
不断更新,确保遍历整个选择列表。每次的选择列表是s[idx... idx+2]
。 - 【结束条件】IP地址被分割为
4
个部分 ,且被完全遍历idx === s.length
。 【**路径更新**】路径存在于
s[idx],s[idx,idx+1],s[idx,idx+1,idx+2]
之间,只要满足数字在0-255
之间,且开头不能为0
( 0 除外 ),就可以当做新的路径。var restoreIpAddresses = function(s) { const n = s.length , res = []; const backtrack = (idx,path)=>{ if( path.length === 4 ){ if( idx === n ) res.push(path.join('.')); return; } for( let i = 0 ; i < 3 ; i++ ){ if( idx + i > n || i && s[idx] === '0' ) return; const str = s.substr(idx,i+1); if( i === 2 && +str > 255 ) return; path.push(str); backtrack(idx+i+1,path); path.pop(); } } backtrack(0,[]); return res; };
1.1.5 分割回文串
【概述】回溯算法 完整遍历 分割字符串
【题目关系】复原IP地址 - 同样是对字符串进行分割。
【题目描述**】
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。
【题目分析**】本题要求输出所有可能的回文串分割方式。第一个难点在于如何标记回文串的范围,第二就是如何遍历出所有方案。
- 【回文问题】需要通过【动态规划】定位回文数组。
dp[idx][i]
表示从idx
到i
的子字符串是不是回文串。 - 【选择列表】索引
idx
不断更新,确保遍历整个选择列表。每次的选择列表是s[idx... n]
。 - 【结束条件】本题目的是分割
s
,所以最终回文子串的总长度要等于总长,idx === s.length
。 【**路径更新**】在
idx
的基础上进行遍历,索引是i
。如果s[idx... i]
是回文串,添加子回文串。var partition = function(s){ const n = s.length; if(!n) return []; const res = []; const dp = new Array(n).fill(false).map(()=>new Array(n).fill(false)); for( let i = n - 1 ; i >= 0 ; i--){ for( let j = i ; j < n ; j++){ dp[i][j] = s[i]===s[j] && ( j-i<2 || dp[i+1][j-1]) } } const backtrack = (idx,path) =>{ if( idx === s.length ) { res.push([...path]); return; } for( let i = idx ; i < n ; i++){ if(!dp[idx][i]) continue; path.push(s.substring(idx, i + 1)); backtrack(i+1,path); path.pop(); } } backtrack(0,[]); return res; };
1.1.6 子集
【概述】回溯算法 部分遍历 无顺序
【题目关系】
- 全排列 - 不需用完整遍历
【题目描述**】
给你一个整数数组 nums
,返回该数组所有可能的子集(幂集)。解集不能包含重复的子集。
【题目分析**】
- 【选择列表】索引
idx
不断更新,确保遍历整个选择列表。每次的选择列表是nums[idx... n]
。 - 【结束条件】直接输出。
【**路径更新**】
idx... n
每个字符都有【选】和【不选】两种可能。- 【选】
path.push(i)
,将选择后的path
放入下一轮递归。 【不选】撤销之前的选择
path.pop()
,进入下一轮 for 循环。**var subsets = function(nums) { const res = []; const backtrack = (idx,path)=>{ res.push([...path]); for( let i = idx ; i < nums.length ; i++ ){ path.push(nums[i]); backtrack(i+1,path); path.pop(); } } backtrack(0,[]); return res; };
1.1.7 子集 II
【概述】回溯算法 部分遍历 无顺序
【题目关系】
- 【选】
子集 - 增加有重复元素的条件。
- 全排列 II - 子集无顺序之分。
【题目描述**】
给定一个可能包含重复元素的整数数组 nums
,返回该数组所有可能的子集(幂集)。
【题目分析**】为了寻找相同元素,需要先排序让相同数字相邻。
- 【选择列表】索引
idx
不断更新,确保遍历整个选择列表。每次的选择列表是nums[idx... n]
。 - 【结束条件】直接输出。
【**路径更新**】
idx... n
每个字符都有【选】、【不选】和【重复】三种可能。- 【选】
path.push(i)
,将选择后的path
放入下一轮递归。 - 【不选】撤销之前的选择
path.pop()
,进入下一轮 for 循环。 - 【重复】如何
nums[i] === nums[i-1]
,说明出现重复,直接跳过。var subsetsWithDup = function(nums) { const res = []; nums.sort((a,b)=>a-b); const backtrack = (nums,idx,path)=>{ res.push([...path]); for( let i = idx ; i < nums.length ; i++ ){ if( i > idx && nums[i] === nums[i-1] ) continue; path.push(nums[i]); backtrack(nums,i+1,path); path.pop(); } } backtrack(nums,0,[]); return res; };
1.1.8 组合
【概述】回溯算法 部分遍历 无顺序
【题目关系】
- 【选】
子集 - 增加了长度限制。
【题目描述**】
给定两个整数 n
和 k
,返回 1 ... n
中所有可能的 k
个数的组合。
【题目分析**】
- 【选择列表】索引
idx
不断更新,确保遍历整个选择列表。每次的选择列表是idx... n
。 - 【结束条件】路径长度等于
k
。 - 【**路径更新**】
idx... n
每个字符都有【选】和【不选】两种可能。- 【选】
path.push(i)
,将选择后的path
放入下一轮递归。 - 【不选】撤销之前的选择
path.pop()
,进入下一轮 for 循环。var combine = function(n, k) { const res = []; if (k <= 0 || n <= 0) return res; const backtrack = (idx,path)=>{ if( path.length > k ) return; if( path.length === k ){ res.push([...path]); return; } for( let i = idx ; i <= n ; i++){ path.push(i); backtrack(i+1,path); path.pop(); } } backtrack(1,[]); return res; };
1.1.9 组合总和 II
【概述】回溯算法 部分遍历 无顺序
【题目描述**】**
给定一个数组candidates
和一个目标数target
,找出candidates
中所有可以使数字和为target
的组合。candidates
中的每个数字在每个组合中只能使用一次。
- 【选】
【题目分析**】**为了寻找相同元素,需要先排序让相同数字相邻。
- 【选择列表】索引
idx
不断更新,确保遍历整个选择列表。每次的选择列表是candidates[idx ... n]
。 - 【结束条件】数字和大于
0
时,说明不符合条件,直接跳出。 数字和等于于0
时,说明符合条件。 【**路径更新**】
candidates[idx ... n]
每个数字都有【选】和【不选】两种可能。- 【选】
path.push(i)
,将选择后的path
和 数字和 放入下一轮递归。 【不选】撤销之前的选择
path.pop()
,进入下一轮 for 循环。 ```javascript const combinationSum2 = (candidates, target) => { const res = []; candidates.sort((a,b) => a - b );const backtrack = (idx, path, target) => { if (target<0) return; if (!target) {
res.push([...path]); return;
} for (let i = idx; i < candidates.length; i++) {
if (i > idx && candidates[i - 1] == candidates[i]) continue; path.push(candidates[i]); backtrack(i + 1, path, target - candidates[i]); path.pop();
} };
backtrack(0, [], target); return res; }; ```
1.2.1 解数独
【概述】回溯算法 二维
【题目描述**】
编写一个程序,通过填充空格来解决数独问题。
【题目分析**】- 【选】
【遍历方式**】**索引改为
x
y
,y
每走到9
,相当于走完一行,跳转至下一行,即x++
。- 【选择列表】每次的选择列表是
0-9
,但【同行】、【同列】以及【同框】不能有重复元素。 - 【结束条件】遍历完所有格子,
x === 9
。 【**路径更新**】从
0-9
选【同行】、【同列】以及【同框】没有的数字。【选】和【不选】两种可能。- 【同行】
x
不一样。 - 【同列】
y
不一样。 【同框】
boxIndex
不一样,其中boxIndex = Math.floor(x/3)*3 + Math.floor(y/3)
。var solveSudoku = function(board) { const row = {} , col = {} , box = {}; for( let i = 0 ; i < 9 ; i++){ for( let j = 0 ; j < 9 ; j++){ if( board[i][j] !== '.'){ const num = board[i][j], boxIndex = Math.floor(i/3)*3 + Math.floor(j/3); row[`${i}-${num}`] = true; col[`${j}-${num}`] = true; box[`${boxIndex}-${num}`] = true; } } } const backtrack = (x,y)=>{ if( y === 9 ) x++, y = 0; if( x === 9 ) return true; if(board[x][y]!=='.') return dfs(x,y+1); for( let i = 1 ; i <= 9 ; i++){ const boxIndex = Math.floor(x/3)*3 + Math.floor(y/3) if(row[`${x}-${i}`]===undefined && col[`${y}-${i}`]===undefined && box[`${boxIndex}-${i}`]===undefined ){ board[x][y] = ''+i; row[`${x}-${i}`] = col[`${y}-${i}`] = box[`${boxIndex}-${i}`] = true; if(backtrack(x,y+1)) return true; board[x][y]='.'; row[`${x}-${i}`] = col[`${y}-${i}`] = box[`${boxIndex}-${i}`] = undefined; } } return false; } backtrack(0,0); };
1.2.2 N 皇后
【概述】回溯算法 二维
【题目描述**】
n 皇后问题 研究的是如何将n
个皇后放置在n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数n
,返回所有不同的 n 皇后问题 的解决方案。
【题目分析**】
- 【同行】
【遍历方式**】**由于 n 皇后问题是以行、对角线为单位,所以不用将索引改为
x
y
,采用idx
遍历n
行。- 【选择列表】每次的选择列表是是否放
Q
。由于idx
遍历的是n
行,可以确保Q
在不同行,所以只要考虑【同列】、【正对角】以及【反对角】不能有重复元素即可。 - 【结束条件】遍历完所有行,
idx === n
。 【**路径更新**】从
0-9
选【同列】、【正对角】以及【反对角】没有涉及的位置。- 【同列】
i
不一样。 - 【正对角】
idx-i+n
【反对角】
idx+i
var solveNQueens = function(n) { const col = new Array(n); const dg = new Array(2 * n) , udg = new Array(2 * n); const res = [] ; const backtrack = (idx , path)=>{ if( idx === n ) { const newPath = path.map((item)=>item.join('')); res.push(newPath); return; } for( let i = 0 ; i < n ; i++){ if(!col[i] && !dg[idx-i+n] && !udg[idx + i]){ col[i] = true , dg[idx-i+n] = true , udg[idx + i] = true; path[idx][i] = 'Q'; backtrack(idx+1,[...path]); col[i] = false , dg[idx-i+n] = false , udg[idx + i] = false; path[idx][i] = '.'; } } } backtrack(0,new Array(n).fill('.').map(()=>new Array(n).fill('.'))); return res; };
1.2.3 N皇后 II
【概述】回溯算法 二维
【题目描述**】
n 皇后问题 研究的是如何将n
个皇后放置在n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数n
,返回所有不同的 n 皇后问题 的解决方案。
【题目分析**】具体见【N 皇后】,只是从输出所有可能,变成了计数。var totalNQueens = function(n) { const col = new Array(n); const dg = new Array(2*n) , ndg = new Array(2*n); let count = 0; const backtrack = (idx)=>{ if(idx===n){ count++; return; } for(let i = 0 ; i < n ; i++){ if(col[i]||dg[i-idx+n]||ndg[i+idx])continue; col[i] = dg[i-idx+n] = ndg[i+idx] =true; backtrack(idx+1); col[i] = dg[i-idx+n] = ndg[i+idx] =false; } } backtrack(0); return count; };
1.2.4 单词搜索
【概述】回溯算法 二维
【题目描述**】
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
【题目分析**】
- 【同列】
【遍历方式**】**索引为
x
y
和idx
。x
y
表示二维矩阵的位置,idx
表示字符串的长度。- 【选择列表】每次的选择列表是 【上下左右】 四个方向移动。【同行】、【同列】以及【同框】不能有重复元素。
【结束条件】**【上下左右】** 四个方向有一方向走通。
【单路结束】
- 【成功】
idx === word.length
- 【失败】走到边界 || 回头路 || 字符不相同 ```javascript var exist = function(board, word) { const m = board.length , n = board[0].length; const mono = new Array(m).fill(false).map(()=>new Array(n));
const backtrack = (idx,x,y) =>{ if( idx === word.length ) return true; if( x < 0 || x >= m || y < 0 || y >= n || mono[x][y] || board[x][y] !== word[idx]) return false; mono[x][y] = true; const res = backtrack(idx+1,x+1,y)||backtrack(idx+1,x-1,y)||backtrack(idx+1,x,y+1)||backtrack(idx+1,x,y-1); if(res) return true; mono[x][y] = false; return false; }
for(let i = 0 ; i < m ; i++){ for( let j = 0 ; j < n ; j++){
if(board[i][j]===word[0] && backtrack(0,i,j)) { return true }
} } return false; }; ```
- 【成功】