工作几年了还不知道回溯算法是什么?

78 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情


回溯算法

回溯算法,也叫试探法。他的基本思想是从一种状态出发,搜索从这种“状态”出发能达到的所有状态,当搜索到尽头,回退1-N步,从另外一种可能的状态出发,继续搜索,直到所有的“路径”都走过,这种不断前进,不断回溯(后退)搜索的算法就是回溯法。

leetcode78题(子集)

我们以此经典题目为例,来尝试使用回溯算法

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例:

输入: nums = [1,2,3]
输出: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
输入: nums = [0]
输出: [[],[0]]

分析

通过分析题目可知几个信息点

  1. 数组所有子集
  2. 所有元素互不相同
  3. 结果不能包含重复

然后还需要我们分析题意,假如我们需要找到所有[1,2,3]的子集,那么要不断的去遍历搜索,从1开始,深度搜索,搜索到1、2,再到1、2、3, 然后再从第二位2开始同层搜索,我们可以看出来结果是个树,题目需要的是所有节点的去重的和

树的展示如下图:

screenshot-20220806-233651.png

代码

那么有了思路,我们开始写代码,我们先定义返回的结果集ans,这也是个二维数组,还有暂存的路径(path)

const ans = [], path = []

接下来我们要考虑把暂存的路径放到结果集中,通过上面的树可知,每次树的节点遍历,就是每次执行方法的时候(递归),就是所以我们只要是每次方法刚执行的时候,都需要存储数据。

所以我们的回溯方法backtracking这么处理

const backtracking = () => {
    // 拷贝path,存储数据进结果集,也可使用ans.push(Array.from(path))
    ans.push([...path])
}

然后我们需要同层遍历树

    for (let i = 0; i < nums.length; i ++) {
      path.push(nums[i]);
      backtracking(i + 1);
      path.pop();
    }

但是由于每次都是从0开始遍历,会导致数据重复,所以我们需要改造一下backtracking方法,增加startIndex,让我们每次搜索的时候都是从上一次传入的index开始

const backtracking = (startIndex) => {
    ans.push([...path]);

    for (let i = startIndex; i < nums.length; i++) {
      path.push(nums[i]);
      backtracking(i + 1);
      path.pop();
    }
  };

那么我们完整的代码就出来了

var subsets = function (nums) {
  const ans = [],
    path = [];
  const backtracking = (startIndex) => {
    ans.push([...path]);

    for (let i = startIndex; i < nums.length; i++) {
      path.push(nums[i]);
      backtracking(i + 1);
      path.pop();
    }
  };
  backtracking(0);
  return ans;
};

结语

  • 回溯是不得已而为之的方法,也算暴力方法的一种,时间复杂度很高。
  • leetcode78(子集)这个是标准的回溯问题,我们可以好好把代码理解一下,其他类似的问题都可以用此模板解决
  • 相似的问题,我们要分析是树的同层处理,还是深度的树处理,如果某些场景下存在重复搜索,我们还需要进行剪枝,减少时间复杂度。