JS刷题之路-递归回溯(上篇)
递归与回溯的题就不像栈的题那么好刷了,差点就鸽了,看到有催更,这不我啪的一下赶出了递归与回溯的JS上篇;递归与回溯是基础,有了基础才好在下一篇刷二叉树~
思维导图
获取高清PDF,请在微信公众号【小狮子前端】回复【LeetCode】,一起刷题或者交流学习可以加Q群【666151691】
思维导图相较于上篇我在递归与回溯上加了分类,获得原作者的许可了;在这一篇呢讲的相对于简单一些,后续持续加更下一篇,递归与回溯(下)进阶~~
先搞几道开胃小菜,热热身
一、热身题
16.11.跳水板
题目描述
- [ ] 题目链接:16.11.跳水板
你正在使用一堆木板建造跳水板。有两种类型的木板,其中长度较短的木板长度为shorter,长度较长的木板长度为longer。你必须正好使用k块木板。编写一个方法,生成跳水板所有可能的长度。
返回的长度需要从小到大排列。
示例 1
输入:
shorter = 1
longer = 2
k = 3
输出: [3,4,5,6]
解释:
可以使用 3 次 shorter,得到结果 3;使用 2 次 shorter 和 1 次 longer,得到结果 4 。以此类推,得到最终结果。
提示:
0 0 <= k <= 100000
解题思路
总有k+1个结果,结果就是短板长度*k-i+长版长度i 以题目示例shorter
= 1 longer
= 2 k
= 3
k=3 res[i] = 结果计算
1 1 1 0 1*3 + 2*0 3
1 1 2 1 1*2 + 2*1 4
1 2 2 2 1*1 + 2*2 5
2 2 2 4 1*0 + 2*3 6
挺简单的:只需要注意这个特殊情况:1 1 100000
代码如下(示例):
/**
* @param {number} shorter
* @param {number} longer
* @param {number} k
* @return {number[]}
*/
var divingBoard = function (shorter, longer, k) {
/**
* k=3 res
* 1 1 1 0 1*3 + 2*0 3
* 1 1 2 1 1*2 + 2*1 4
* 1 2 2 2 1*1 + 2*2 5
* 2 2 2 4 1*0 + 2*3 6
*/
let res = [];
if (shorter === longer && k) {
res[0] = k * shorter;
} else if (k) {
for (let i = 0; i <= k; i++) {
res[i] = shorter * (k - i) + longer * i;
}
}
return res;
};
1291.顺次数
题目描述
- [ ] 题目链接:1291.顺次数
我们定义「顺次数」为:每一位上的数字都比前一位上的数字大 1 的整数。
请你返回由 [low, high] 范围内所有顺次数组成的 有序 列表(从小到大排序)。
示例 1:
输出:low = 100, high = 300
输出:[123,234]
示例 2:
输出:low = 1000, high = 13000
输出:[1234,2345,3456,4567,5678,6789,12345]
提示:
10 <= low <= high <= 10^9
解题思路
方法一:穷举 注意:出来的结果是要排序的代码如下(示例):
/**
* @param {number} low
* @param {number} high
* @return {number[]}
*/
var sequentialDigits = function (low, high) {
// return low.toString().split('');
/**
* 提示:10 <= low <= high <= 10^9
* 「顺次数」不是很多,穷举吧
*/
let res = [],
index = 0;
for (let i = 1; i <= 9; i++) {
let n = i;
for (let j = i + 1; j <= 9; j++) {
n = n * 10 + j;
if (n >= low && n <= high) {
res[index++] = n;
}
}
}
return res.sort(function(a,b){return a-b;});
};
方法二:来自Chocolate:上面的方法虽然说也没有遍历太多数但是还是太耗时了,并且有很多没有必要遍历,可以剪枝
先求出最小值和最大值对应字符串的长度,即求出我们能枚举的数字的长度范围。
然后我们的起点的最小值从 1 开始,起点的最大值从 10-len 开始。
为什么是 10-len?举例说明,示例1给的是 [100,300]范围的值,那么可枚举的长度 len 为 3,起点的最大值就位 10 - 3 = 7。那么此时顺次数为 789 但是不在我们区间范围内,舍弃。然后8、9开头的数字就不需要枚举了。这样,我们就能剪掉一部门数据了。
代码如下(示例):
/**
* @param {number} low
* @param {number} high
* @return {number[]}
*/
var sequentialDigits = function(low, high) {
let res = []
let lowLen = low.toString().length
let highLen = high.toString().length
for(let i=lowLen;i<=highLen;i++){
for(let j=1;j<=10-i;j++){
let str = ''
let num = j
str += num
let k = i-1
while(k--){
num++
str += num
}
let ans = parseInt(str)
if(ans>=low && ans<=high){
res.push(ans)
}
}
}
return res
};
二、矩阵
59. 螺旋矩阵 II
题目描述
- [ ] 题目链接:59. 螺旋矩阵 II
给定一个正整数 n,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
示例:
输入: 3
输出:
[
[ 1, 2, 3 ],
[ 8, 9, 4 ],
[ 7, 6, 5 ]
]
解题思路
解题关键:边界思想 首先定义上下左右四个边界
从左边界往右边界走(123
)上边界下移(up++
)
然后从上往下走(45
)(此时没有3是因为up
加后变成1了)右边界左移(right- -
)
然后从右往左走(67
)(没5因为right- -了)下边界上移(down- -
)
然后从下往上走(8
)(没7没1因为上下边界)左边界右移(left++
)
最后跳出循环条件:值小于等于n*n
代码如下(示例):
/**
* @param {number} n
* @return {number[][]}
*/
var generateMatrix = function (n) {
/**
*
* 0 1(0,0) 2(0,1) 3(0,2)
* 1 8(1,0) 9(1,1) 4(1,2)
* 2 7(2,0) 6(2,1) 5(2,2)
*
*/
let res = [];
let up = 0,
down = n - 1,
left = 0,
right = n - 1;
for (let i = 0; i res[i] = [];
}
let cur = 1;
while (cur <= n * n) {
for (let i = left; i <= right; i++) { //左到右 最上添加一行数据-> 上边界改变
res[up][i] = cur++;
}
up++;
for (let i = up; i <= down; i++) { //从上到下 -> 改变右边界
res[i][right] = cur++;
}
right--;
for (let i = right; i >= left; i--) { //从右到左 -> 改变下边界
res[down][i] = cur++;
}
down--;
for (let i = down; i >= up; i--) { //从上到下 -> 改变左边界
res[i][left] = cur++;
}
left++;
}
return res;
};
54. 螺旋矩阵
题目描述
- [ ] 题目链接:54. 螺旋矩阵
给定一个包含 m x n 个元素的矩阵(m 行, n 列),请按照顺时针螺旋顺序,返回矩阵中的所有元素。
示例 1:
输入:
[
[ 1, 2, 3 ],
[ 4, 5, 6 ],
[ 7, 8, 9 ]
]
输出: [1,2,3,6,9,8,7,4,5]
示例 2:
输入:
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9,10,11,12]
]
输出: [1,2,3,4,8,12,11,10,9,5,6,7]
解题思路
看懂上一题之后,这道题就很简单了
按上题的方法遍历,用res储存值。
注意的是:m与n不相等可能造成它会多走,分析一下为什么,以示例2为例:
走到7是可以的(此时up为1,right为2,down为1,而left
为1),接着还会走到6,可以想象一下边界走向,7往左碰壁,往下碰壁往右遍历到6,往上碰壁,然后跳出循环
为了解决这个问题可以有很多解决方法(这里写两个):
- 暴力----我直接把多的在最终时去除
- 在循环里加if条件语句:if(res.length === n*m) break 可以在每个位置都加,也可以找到关键位置加,由于最后一项都是多的一行或者一列,所以加在第二段之后
代码如下(示例):
/**
* @param {number[][]} matrix
* @return {number[]}
*/
var spiralOrder = function (matrix) {
let res = [],
index = 0;
let n = matrix.length,
m = matrix[0].length;
let up = 0,
down = n - 1,
left = 0,
right = m - 1;
while (index for (let i = left; i <= right; i++) {
res[index++] = matrix[up][i];
}
up++;
for (let i = up; i <= down; i++) {
res[index++] = matrix[i][right];
}
right--;
// if(res.length === n*m) break
for (let i = right; i >= left; i--) {
res[index++] = matrix[down][i];
}
down--;
for (let i = down; i >= up; i--) {
res[index++] = matrix[i][left];
}
left++;
}
// console.log("res:"+res);
return res.splice(0, n * m);
};
// console.log("return:"+spiralOrder([[1,2,3,4],[5,6,7,8],[9,10,11,12]]));
73. 矩阵置零
题目描述
- [ ] 题目链接:54. 螺旋矩阵
给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。请使用原地算法。
示例 1:
输入:
[
[1,1,1],
[1,0,1],
[1,1,1]
]
输出:
[
[1,0,1],
[0,0,0],
[1,0,1]
]
示例 2:
输入:
[
[0,1,2,0],
[3,4,5,2],
[1,3,1,5]
]
输出:
[
[0,0,0,0],
[0,4,5,0],
[0,3,1,0]
]
进阶:
一个直接的解决方案是使用 O(mn) 的额外空间,但这并不是一个好的解决方案。
一个简单的改进方案是使用 O(m + n) 的额外空间,但这仍然不是最好的解决方案。
你能想出一个常数空间的解决方案吗?
解题思路
解题关键:希望你使用原地算法使用常数空间解决问题
用常数空间解决意味着不能使用额外空间
首先看一下使用额外空间的代码 首先找到0的行列,再将其行列置零。这并不是一个好方法,因为这意味着你必须用一个额外的空间去存0,不管是用什么数据结构。
代码如下(示例):
/**
* @param {number[][]} matrix
* @return {void} Do not return anything, modify matrix in-place instead.
*/
var setZeroes = function (matrix) {
let n = matrix.length;
let m = matrix[0].length;
var across = new Set();
var vertical = new Set();
// 遍历每一行
for (let i = 0; i for (let j = 0; j if (matrix[i][j] === 0) { //找到0
across.add(i);
vertical.add(j);
}
}
}
for (let k of across) {
for (let j = 0; j matrix[k][j] = 0;
}
}
for (let k of vertical) {
//竖轴置零
for (let j = 0; j matrix[j][k] = 0;
}
}
return matrix;
};
符合题目要求的原地算法:
友情提示:Object.is不会对NaN,-0和进行类型转换,也不会进行特殊处理+0(使其具有与===那些特殊数值相同的行为)
代码如下(示例):
/**
* @param {number[][]} matrix
* @return {void} Do not return anything, modify matrix in-place instead.
*/
var setZeroes = function (matrix) {
for (let i = 0; i for (let j = 0; j 0
].length; j++) {
if (
Object.is(matrix[i][j],
0)) {
// 对行进行操作
for (
let k =
0; k
if (!
Object.is(matrix[k][j],
0) && k !== i) matrix[k][j] =
-0
// 对列进行操作
for (
let k =
0; k 0].length; k++)
if (!
Object.is(matrix[i][k],
0) && k !== j) matrix[i][k] =
-0
}
}
} return matrix
};
三、排列
784.字母大小写全排列
题目描述
- [ ] 题目链接:784.字母大小写全排列
给定一个字符串S,通过将字符串S中的每个字母转变大小写,我们可以获得一个新的字符串。返回所有可能得到的字符串集合。
示例:
输入:S = "a1b2"
输出:["a1b2", "a1B2", "A1b2", "A1B2"]
输入:S = "3z4"
输出:["3z4", "3Z4"]
输入:S = "12345"
输出:["12345"]
提示:
S 的长度不超过12。
S 仅由数字和字母组成。
解题思路
一图胜千言,以a1b2为例图解:
代码如下(示例):
/**
* @param {string} S
* @return {string[]}
*/
var letterCasePermutation = function (S) {
let res = [];
let dfs = (t, str) => { //t是字母之前字符串,str是之后的
if (str === '') return res.push(t); //结束条件为遍历到最后一个字符,最后一个字符的str为''
let ch = str[0]; //保存一下当前值
let nextStr = str.substr(1); //传一个值,表示当前值的右边字符串
if (isNaN(ch)) {
//为字母 分为两个分支 大写 小写
dfs(t + ch.toLowerCase(), nextStr); //小写
dfs(t + ch.toUpperCase(), nextStr); //大写
} else {//为数字 拼接一下就好了
dfs(t + ch, nextStr)
}
}
dfs('', S); //刚开始 t='' str=S
return res;
};
46. 全排列
题目描述
- [ ] 题目链接:46. 全排列
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
解题思路
经典的全排列问题,首先介绍经典解法,这里是另外一道题的全排列状态树,换汤不换药:「递归分析」:• 用一个数组a[n]来保存1~ n
之间的n
个自然数,对于i=1~ n,每次用a[1]与a[i]交换
后,对a[2]~a[n]
中的n-1
个元素进行全排列,然后再交换a[1]与a[i]
的值,使它恢复
到此次排列前的状态 • 对于a[3]~a[n]
区间内的n-2
个元素进行全排列,然后再把交换的元素交换回来
;• 依次类推,直到对a[n]进行全排列时,输出整个数组的值,即得到一种排列结果。
还不懂的话可以看LeetCode官方题解,不想跳过去看的我把关键的图拿过来了:
代码如下(示例):
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permute = function (nums) {
let swp = (str, i, j) => {
let t = str[i];
str[i] = str[j];
str[j] = t;
} //swp
let res = [];
let dfs = (str, cur) => {
if (cur === str.length - 1) {
return res.push(str.slice());
}
for (let i = cur; i swp(str, cur, i);
dfs(str, cur + 1);
swp(str, cur, i);
}
}
dfs(nums, 0);
return res;
};
「解法二:」来自LeetCode作者liweiwei1419
- 先写以 1 开头的全排列,它们是:[1, 2, 3], [1, 3, 2],即 1 + [2, 3] 的全排列(注意:递归结构体现在这里);
- 再写以 2 开头的全排列,它们是:[2, 1, 3], [2, 3, 1],即 2 + [1, 3] 的全排列;
- 最后写以 3 开头的全排列,它们是:[3, 1, 2], [3, 2, 1],即 3 + [1, 2] 的全排列。
- 注意需要保存状态,已经选择的数字在 当前 要选择的数字中不能出现。
图解:
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permute = function (nums) {
let res = [];
let vis = {};
let dfs = (t) => {
if (t.length == nums.length) {
res.push(t);
}
for (let i = 0; i if (vis[i]) continue;
vis[i] = true;
t.push(nums[i]);
dfs(t.slice());
t.pop();
vis[i] = false;
}
}
dfs([]);
return res;
};
47. 全排列 II
题目描述
- [ ] 题目链接:47. 全排列 II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
示例 2:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
提示:
1 <= nums.length <= 8
-10 <= nums[i] <= 10
解题思路
一图胜千言:
代码如下(示例):
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permuteUnique = function (nums) {
nums.sort((a, b) => {
return a - b
});
let res = [];
let visited = [];
let dfs = (t) => {
if (t.length === nums.length) return res.push(t);
for (let i = 0; i if (visited[i]) continue; //访问过
if (!visited[i - 1] && i > 0 && nums[i] === nums[i - 1]) continue; //上一个没访问,且上一个值等于当前值
t.push(nums[i]);
visited[i] = true;
dfs(t.slice());
t.pop();
visited[i] = false;
}
}
dfs([]);
return res;
};
console.log(permuteUnique([1, 2, 1]));
四、子集
78. 子集
题目描述
- [ ] 题目链接:78. 子集给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
解题思路
子集树如图所示:
这道题可以巧妙的运用选与不选构造一个更深的递归树,这是我们想要看到的
代码如下(示例):
/**
* @param {number[]} nums
* @return {number[][]}
*/
var subsets = function (nums) {
let res = [];
let t=[];
let dfs = (cur) => { //当前层数
if (cur === nums.length){
// console.log(t);
return res.push(t.slice()); //当前层数等于数组长度
}
t.push(nums[cur]); //选当前层数的数字
dfs(cur + 1);
t.pop(); //不选当前层数的数字
dfs(cur + 1);
}
dfs(0);
return res;
};
console.log(subsets([1, 2, 3]))
代码如下(示例):
var subsets = function (nums) {
let res = [];
let dfs = (t, start) => {
res.push(t);
for (let i = start; i t.push(nums[i]);
dfs(t.slice(), i + 1);
t.pop();
}
}
dfs([], 0);
return res;
};
console.log(subsets([1, 2, 3]))
90. 子集 II
题目描述
- [ ] 题目链接:90. 子集 II给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
解题思路
上一道题可以用讨巧的方法来回溯,但是这道题就不可以了。而这道题的解决方法与思路是可以解决上一道题的。话不多说,来分析题目:
不同于上题,很明显需要剪枝,但是刚看完题目不知道怎么剪,那么我们先画一个子集树
,理清思路:
代码思路:注意:巧妙使用sort,可以使相等元素相邻
关键:
- 使用i指向当前元素,判断当前元素与前一元素是否相等,及判断
num[i - 1]===num[i]
- 剪枝不能将不同层的剪枝,如图中的[1,2,2]
i=start
是不同层重复(start相当于第几层),应该剪枝[1,2]i=2,start=1·
不同层
代码如下(示例):
var subsetsWithDup = function (nums) {
let res = [];
nums.sort((a, b) => a - b);
let dfs = (t, start) => {
res.push(t);
for (let i = start; i // 同层重复,跳过
if (i != start && nums[i - 1] == nums[i]) continue;
t.push(nums[i]);
dfs(t.slice(), i + 1);
t.pop();
}
}
dfs([], 0);
return res;
};
// console.log(subsetsWithDup([4,4,4,1,4]))
总结
每个人的解题方式都是不太一样的, 但是解题思路是可以相互借鉴的,这篇文章还是画了不少图的,也没有那么多文字,应该还是比较好理解的,最后希望这篇文章对你有用~
如果对某题还有疑问LeetCode上基本也有不错的解答哦
后续文章会持续更新,下一篇:递归与回溯(下),和我一起刷题吧~
点个赞再走吧 ~ 求求了 ❀❀❀ 能一键三连的话那就更好啦~,你的支持是我继续写作的动力⭐️
小狮子有话说
你好,我是 Chocolate,一个狮子座
的前端攻城狮,希望成为优秀的前端博主,每周都会更新文章,与你一起变优秀~
- 关注
小狮子前端
,回复【小狮子
】获取为大家整理好的文章、资源合集 - 我的博客地址:
yangchaoyi.vip
欢迎收藏,可在博客留言板留下你的足迹,一起交流~ - 觉得文章不错,【
点赞
】【在看
】支持一波 ✿✿ヽ(°▽°)ノ✿
叮咚~ 可以给小狮子加
星标
,便于查找。感谢加入小狮子前端,最好的我们最美的遇见,我们下期再见~