思维导图 js插件_JS刷题之路递归回溯(上篇)和我一起系统的刷题吧(内附思维导图)...

JS刷题之路-递归回溯(上篇)

递归与回溯的题就不像栈的题那么好刷了,差点就鸽了,看到有催更,这不我啪的一下赶出了递归与回溯的JS上篇;递归与回溯是基础,有了基础才好在下一篇刷二叉树~

思维导图

8737a73a6a9860022908575a2067d69e.png

获取高清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,往上碰壁,然后跳出循环

为了解决这个问题可以有很多解决方法(这里写两个):

  1. 暴力----我直接把多的在最终时去除
  2. 在循环里加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为例图解:b2a3c2682b2c0a9a12435bacbcd34ac8.png

代码如下(示例):

/**
 * @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]
]

解题思路

经典的全排列问题,首先介绍经典解法,这里是另外一道题的全排列状态树,换汤不换药:53c65ef96a3d16bc9232195437775d5f.png「递归分析」:• 用一个数组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官方题解,不想跳过去看的我把关键的图拿过来了:085540aec0d95d11926dd546917b56db.png

代码如下(示例):

/**
 * @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] 的全排列。
  • 注意需要保存状态,已经选择的数字在 当前 要选择的数字中不能出现。

图解:c5ae9cd4e51d6eec862efdf90ef710e5.png

/**
 * @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

解题思路

一图胜千言:f344a4e9b2fd1574d61a52dd980a879a.png

代码如下(示例):

/**
 * @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],
  []
]

解题思路

子集树如图所示:20f4258a19b7ac75c2b790dc6e17ebe1.png

这道题可以巧妙的运用选与不选构造一个更深的递归树,这是我们想要看到的

代码如下(示例):

/**
 * @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],
  []
]

解题思路

上一道题可以用讨巧的方法来回溯,但是这道题就不可以了。而这道题的解决方法与思路是可以解决上一道题的。话不多说,来分析题目:

不同于上题,很明显需要剪枝,但是刚看完题目不知道怎么剪,那么我们先画一个子集树,理清思路:

c74cab9a2c379b4287e89236f06cd512.png代码思路:注意:巧妙使用sort,可以使相等元素相邻

关键:

  1. 使用i指向当前元素,判断当前元素与前一元素是否相等,及判断num[i - 1]===num[i]
  2. 剪枝不能将不同层的剪枝,如图中的[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,一个狮子座的前端攻城狮,希望成为优秀的前端博主,每周都会更新文章,与你一起变优秀~

  1. 关注小狮子前端,回复【小狮子】获取为大家整理好的文章、资源合集
  2. 我的博客地址:yangchaoyi.vip 欢迎收藏,可在博客留言板留下你的足迹,一起交流~
  3. 觉得文章不错,【点赞】【在看】支持一波 ✿✿ヽ(°▽°)ノ✿
faf03b0cdfd7001e57081a84a8927308.png

叮咚~ 可以给小狮子加星标,便于查找。感谢加入小狮子前端,最好的我们最美的遇见,我们下期再见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值