javascript 之排列组合

接上一篇 javascript 之二分查找

看完这篇后,相信你在解决排列组合问题的时候会象写 for 循环一样快速。

方法是一样的,记模板,提高效率,减少出错。

var permute = function (nums) {
  let ans = []
  let used = []
  let arr = []
  function helper() {
    if (arr.length == nums.length) {
      ans.push(arr.slice(0))
      return
    }
    for (let i = 0; i < nums.length; i++) {
      if (used[i]) continue
      used[i] = true
      arr.push(nums[i])
      helper(arr)
      arr.pop()
      used[i] = false
    }
  }
  helper()
  return ans
};
输入 [1,2,3]
输出 [ 
      [1, 2, 3]
      [1, 3, 2]
      [2, 1, 3]
      [2, 3, 1]
      [3, 1, 2]
      [3, 2, 1]
    ]

这正好是 全排列的解法。46. 全排列

可以不用 used 额外数组,直接交换 nums 元素的方式,但是额外数组记录的方式更具有通用性,更适合用做模板

为了方便记,简单解释一下。递归三板斧。

  1. 退出条件。

    //找到 一组排列直接退出
    if (arr.length == nums.length) {
      ans.push(arr.slice(0))
      return
    }
    
  2. 这一层递归要做的事

    //如果这个位置 已经用过了,略过
    if (used[i]) continue
    //标记已使用
    used[i] = true
    //取用这个数
    arr.push(nums[i])
    //进到下一层
    helper(arr)
    
  3. 恢复现场

      arr.pop()
      used[i] = false
    

用语言来表述一下:每次取一个没有用过的数,直到长度为 nums 长度。

递归过程早已有人画了图,还有视频 ,看这里

可能每个人开始的时候都习惯用大脑去模拟递归调用的整个过程,但是这样效率不高,也容易出错。比较高效的方法是记住模板的每句代码可以产生的效果,想要什么样的结果 写什么样的代码即可。写过多次后,大脑就可以很容易的模拟整个递归过程了。这就是只要足够熟练,其意自见。

全排列有两种,一种没有重复数字,一种有重复数字

  1. 没有重复数字 全排列
  2. 有重复数字,全排列II

全排列前面已经讲过,现在看下 全排列II 只要稍加改动即可

function permuteUnique(nums) { 
const ans = []
const arr = []
const used= []
function helper() { 
  if (arr.length === nums.length) {
    ans.push(arr.slice(0))
    return
  }
  let last = undefined  //新增
  for (let i = 0; i < nums.length; i++){
    if (used[i]) continue
    if (last == nums[i]) continue //新增
    last = nums[i]  //新增
    used[i] = true
    arr.push(nums[i])
    helper()
    arr.pop(nums[i])
    used[i]=false
  }
  
}
helper();
return ans
}

一共新增了三行代码。要想避免答案重复,首先得知道为什么会重复。在递归的每一层里都会枚举每一个没有用过的位置上的数,放在位置 index ( index>=0&&index<nums.length )

我们假设输入为 [1,2,2,3] 现在正在递归的第一层。正在枚举第二个位置。第二个位置上的数为 2,那么答案就是

'2' + 全排列 [1,2,3]

如果继续枚举第三个位置,第二个位置上的数也为 2,答案还是

'2' + 全排列 ([1,2,3])

所以结果会重复。

为了避免重复,只能是在同一层递归里,相同的数只能枚举一次。

注意不要和 used[i]的作用混淆了。used[i]作用是每个位置只能用一次。

所以只要记住这两个要点,排列就解决了。

  1. used[i]作用是每个位置只能用一次。
  2. last 的作用是在同一层递归相同的数只能用一次

后面遇到同类问题,不用每次都用大脑模拟思考一遍,只要记住结论,就可以快速准确写出代码。多写几次,其意自见。

排列的问题解决了,组合就简单多了。组合相当于是加了限制条件:与顺序无关。

比如 从[1,2,3]中取出 2 个 的组合有哪些

var combine = function (nums, k) {
  const arr = []
  const ans = []
  function helper(index) {
    if (arr.length == k) {
      ans.push(arr.slice())
      return
    }
    for (let i = index; i <nums.length; i++) {
      arr.push(nums[i])
      helper(i + 1)
      arr.pop()
    }
  }
  helper(0)
  return ans
}

和排列模板差不多吧。可以把这个当做组合的模板。

因为与顺序无关,所以增加了 index 参数,本层递归需要把位置传给下一层递归。

把整个过程描述一下

  1. 在第一层递归中 index==0 ,可以取 1,2,3中的任意一个和后面的组合成答案

    把for 循环展开就是

    '1'+ [2,3]中任选一个
    '2'+ [3]中任选一个
    '3'+ []中任选一个
    

一共就选 两个数,所以到第二层就选完了。

其实你如果在网上找解法,会发现五花八门,我这里都用了 for 循环,是为了和排列统一写法,好记。

有了组合的模板,我们来练练手。

77. 组合

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。

var combine = function (n, k) {
  let arr = []
  let ans = []
  function helper(index) {
    if (arr.length === k) {
      ans.push(arr.slice(0))
      return
    }
    for (let i = index; i <= n; i++) {
      arr.push(i)
      helper(i + 1)
      arr.pop()
    }
  }
  helper(1)
  return ans
};

和模板稍有不同的就是把 nums 换成 n 了,写法一样。

40. 组合总和 II

自己点过去看下题。增加的难度在于 给定数组有重复数字。

还记得组合是怎么解决的吗?同样的配方,同样有效。再解释一下。

[1,1,2,3]

拿出第一个1 和后面的 1,2,3组合 ,和拿出第二个 1 和 第一个 1,加上后面的 2,3 组合结果 是一样的,所以同一层递归中同样的数字,只能枚举一次。

var combinationSum2 = function (candidates, target) {
  const arr = []
  const ans = []

  function help(index) {
    let sum = arr.reduce((sum, n) => sum + n, 0)
    if (sum > target) return
    if (sum == target) {
      ans.push(arr.slice(0))
      return
    }
    let last=new Set()
    for (let i = index; i < candidates.length; i++){
      if (last.has(candidates[i])) continue
      last.add(candidates[i])
      arr.push(candidates[i])
  
      help(i + 1)
      arr.pop()
    }
  }
  help(0)
  return ans
};

这里的 last 变成一个Set,因为可能有多个相同的。可以不用last,也能去重,比如可以先把数组排个序,不过开始写的时候不建议这样做。在开始的时候选一套模板练熟完全掌握后再研究其它的解法。

好了,大概就这么多花样了。这些都掌握了,排列组合的基本功就没问题了。

最后,一般的,如果要求列出所有排列组合的解,就用这种递归的方式,如果只是求出个数,可能得用其它方法了。因为只求出个数可以取巧。不用这样穷举每个可能。可以用 递归+记忆 或是动态规划。

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IAM17前端

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值