前端面试算法篇——回溯算法(backtrack)

675 阅读1分钟

(整理自labuladong的算法小抄,顺便推荐这本书)

输出一个数组的所有全排列结果(leetcode#46)

DFS

var permute = function (nums) {
  if (nums.length === 1) return [nums]
  // nums的全排列 = nums的第i个数字 + nums除i之外的全排列
  const res = []
  for (let i in nums) {
    const newNums = nums.slice()
    newNums.splice(i, 1)
    for (let pRes of permute(newNums)) {
      pRes.unshift(nums[i])
      res.push(pRes)
    }
  }
  return res
}

回溯

var permute = function(nums) {
    let res = []
    bt([], nums, res)
    return res
};

var bt = function(track, options, res) {
    if(track.length === options.length) {
        res.push(track.slice())
    }
    for(let num of options){
        if(!(track.findIndex(n=>n === num) > -1)){
            track.push(num)
            bt(track,options,res)
            track.pop()
        }
    }
}

整个过程都在使用同一个track数组对象,通过不停的添加和删除元素来改变数组的值,在满足结束条件时将这个值的浅拷贝记录下来。 本质上是对决策树的遍历,在决策树的每一个节点都维护当前track的状态和可选择列表,当遍历到叶子节点时我们向上回溯决策树,直到遍历完整个决策树。

输出一个数组的所有子集(leetcode#78)

var subsets = function(nums) {
    let res = []
    bt([],nums,0,res)
    return res
};

var bt = function(track, options, start, res) {
    res.push(track.slice())
    for(let i = start; i < options.length; i++){
        track.push(options[i])
        bt(track, options, i+1, res)
        track.pop()
    }
}

相同的套路,变化的是结束条件没有了,但是通过控制nums的起点来缩小选择的范围

组合 return all possible combinations of k numbers out of the range [1, n]

var combine = function(n, k) {
    let res = []
    if(n <= 0 || k <= 0) return res
    bt([],n, 1, k, res)
    return res
};

var bt = function(track,n,start,k,res) {
    if(track.length === k){
        res.push(track.slice())
    }
    for(let i = start; i < n + 1; i++){
        track.push(i)
        bt(track,n,i+1,k,res)
        track.pop()
    }
}

总结

本质都是对决策树枝的遍历和收集,但是考虑的结束条件、剪枝策略不同。