LeetCode回溯 JS经典题型(全排列子集与组合)汇总

Posted YuLong~W

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LeetCode回溯 JS经典题型(全排列子集与组合)汇总相关的知识,希望对你有一定的参考价值。

回溯算法: 实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就 “回溯” 返回,尝试别的路径。

回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。 ——LeetCode

46. 全排列

原题链接:46. 全排列

/**
 * @param number[] nums
 * @return number[][]
 */
var permute = function(nums) 
    const res = [];
    // 用于标记使用过的值 避免重复使用同一个数字
    const used = ;

    function dfs(path) 
        if (path.length == nums.length)  // 个数选够了
            res.push(path.slice()); // 拷贝一份path,加入解集res
            return;                 // 结束当前递归分支
        
        for (const num of nums)  // for枚举出每个可选的选项
            // if (path.includes(num)) continue; // 这么写会增加时间复杂度 查找是O(n)
            if (used[num]) continue; // 使用过的,跳过
            path.push(num);         // 选择当前的数,加入path
            used[num] = true;       // 记录一下 使用了
            dfs(path);              // 基于选了当前的数,递归
            path.pop();             // 上一句的递归结束,回溯,将最后选的数pop出来
            used[num] = false;      // 撤销这个记录
        

        // 同理,也可以用for循环来写
        // for(let i=0;i<nums.length;i++)
        //     if(!used[i])
        //         used[i]=true;
        //         path.push(nums[i]);
        //         dfs(path);
        //         path.pop();
        //         used[i]=false;
        //     
        // 
      
    

    dfs([]); // 递归的入口,空path传进去
    return res;
;

注意:

  • Map 结构 used的使用:填坑时,每用到一个数字,都要给这个数字打上“已用过”的标——避免它被使用第二次;数字让出坑位时,对应的排列和 used 状态也需要被及时地更新掉。

  • 当走到递归边界时,一个完整的排列也完成了。将这个完整排列推入结果数组时,用了res.push(path.slice()),而不是简单的res.push(path) 。因为全局只有一个唯一的 path , path 的值会随着 dfs 的进行而不断被更新。 slice方法的作用是拷贝出一个不影响path正本的副本,以防直接修改到path的引用。

47. 全排列 II

原图链接:47. 全排列 II

对数组进行排序,并且边界条件进行前后重复比对,其余解题步骤与上题一致

/**
 * @param number[] nums
 * @return number[][]
 */
var permuteUnique = function(nums) 
	//先对数组进行排序
    nums.sort((a,b)=>a-b);

    const res=[];
    const used=;

    function dfs(path)
        if(path.length===nums.length)
            res.push(path.slice());
            return;
        
        for(let i=0;i<nums.length;i++)
            // 每次填入的数一定是这个数所在重复数集合中 从左往右第一个未被填过的数字
            if(i>0 && nums[i]===nums[i-1] && !used[i-1]) continue;
            if(!used[i])
                used[i]=true;
                path.push(nums[i]);
                dfs(path);
                path.pop();
                used[i]=false;
            
        
    
    dfs([]);
    return res;
;

78. 子集

原题链接: 78. 子集

/**
 * @param number[] nums
 * @return number[][]
 */
var subsets = function(nums) 
    // 结果数组
    const res=[];
    // 组合数组
    const subset=[];
    const len=nums.length;
    // 进入dfs
    dfs(0);

    function dfs(index)
        // 每次进入,组合更新一次,推入结果数组
        res.push(subset.slice());
        for(let i=index;i<len;i++)
            subset.push(nums[i]);
            // 基于当前数字存在于组合中 进一步dfs
            dfs(i+1)
            // 当前数字不存在组合情况
            subset.pop();
        
    
    return res;
;

77. 组合

原题链接: 77. 组合

/**
 * @param number n
 * @param number k
 * @return number[][]
 */
var combine = function(n, k) 
    // 参数不是nums数组 而是数字范围边界n
    const res=[];
    const subset=[];
    // 起始从1开始
    dfs(1);

    function dfs(index)
        if(subset.length===k)
            res.push(subset.slice());
            return;
        
        // 遍历inde——n之间所有数字
        for(let i=index;i<=n;i++)
            subset.push(i);
            dfs(i+1);
            subset.pop();
        
    
    return res;
;

总结:

1、什么时候用:

看两个特征:

  • 题目中暗示了一个或多个解,并且要求我们详尽地列举出每一个解的内容时,一定要想到 DFS、想到递归回溯。
  • 题目经分析后,可以转化为树形逻辑模型求解。

2、为什么这样用:

递归与回溯的过程,本身就是穷举的过程。题目中要求我们列举每一个解的内容,解从哪来?解是基于穷举思想、对搜索树进行恰当地剪枝后得来的。

注意到另一种问法:不问解的内容,只问解的个数。这类问题往往不用 DFS 来解,而是用动态规划

3、怎么用:

一个模型——树形逻辑模型;两个要点——递归式和递归边界。

  • 树形逻辑模型的构建,关键在于找“坑位”,一个坑位就对应树中的一层,每一层的处理逻辑往往是一样的,这个逻辑就是递归式的内容。
  • 递归边界,要么在题目中约束得非常清楚、要么默认为“坑位”数量的边界。

用伪代码总结一下编码形式,大部分的题解都符合以下特征:

function xxx(入参) 
  前期的变量定义、缓存等准备工作 
  
  // 定义路径栈
  const path = []
  
  // 进入 dfs
  dfs(起点) 
  
  // 定义 dfs
  dfs(递归参数) 
    if(到达了递归边界) 
      结合题意处理边界逻辑,往往和 path 内容有关
      return   
    
    
    // 注意这里也可能不是 for,视题意决定
    for(遍历坑位的可选值) 
      path.push(当前选中值)
      处理坑位本身的相关逻辑
      path.pop()
    
  

以上是关于LeetCode回溯 JS经典题型(全排列子集与组合)汇总的主要内容,如果未能解决你的问题,请参考以下文章

一文通数据结构与算法之——回溯算法+常见题型与解题策略+Leetcode经典题

算法 ---- LeetCode回溯系列问题题解

算法 ---- LeetCode回溯系列问题题解

算法 ---- LeetCode回溯系列问题题解

算法 ---- LeetCode回溯系列问题题解

LeetCode 31:递归回溯八皇后全排列一篇文章全讲清楚