接上一篇 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 元素的方式,但是额外数组记录的方式更具有通用性,更适合用做模板
为了方便记,简单解释一下。递归三板斧。
-
退出条件。
//找到 一组排列直接退出 if (arr.length == nums.length) { ans.push(arr.slice(0)) return }
-
这一层递归要做的事
//如果这个位置 已经用过了,略过 if (used[i]) continue //标记已使用 used[i] = true //取用这个数 arr.push(nums[i]) //进到下一层 helper(arr)
-
恢复现场
arr.pop() used[i] = false
用语言来表述一下:每次取一个没有用过的数,直到长度为 nums 长度。
递归过程早已有人画了图,还有视频 ,看这里
可能每个人开始的时候都习惯用大脑去模拟递归调用的整个过程,但是这样效率不高,也容易出错。比较高效的方法是记住模板的每句代码可以产生的效果,想要什么样的结果 写什么样的代码即可。写过多次后,大脑就可以很容易的模拟整个递归过程了。这就是只要足够熟练,其意自见。
全排列有两种,一种没有重复数字,一种有重复数字
全排列前面已经讲过,现在看下 全排列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]
作用是每个位置只能用一次。
所以只要记住这两个要点,排列就解决了。
used[i]
作用是每个位置只能用一次。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 参数,本层递归需要把位置传给下一层递归。
把整个过程描述一下
-
在第一层递归中 index==0 ,可以取 1,2,3中的任意一个和后面的组合成答案
把for 循环展开就是
'1'+ [2,3]中任选一个 '2'+ [3]中任选一个 '3'+ []中任选一个
一共就选 两个数,所以到第二层就选完了。
其实你如果在网上找解法,会发现五花八门,我这里都用了 for 循环,是为了和排列统一写法,好记。
有了组合的模板,我们来练练手。
给定两个整数 n
和 k
,返回范围 [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 了,写法一样。
自己点过去看下题。增加的难度在于 给定数组有重复数字。
还记得组合是怎么解决的吗?同样的配方,同样有效。再解释一下。
[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,也能去重,比如可以先把数组排个序,不过开始写的时候不建议这样做。在开始的时候选一套模板练熟完全掌握后再研究其它的解法。
好了,大概就这么多花样了。这些都掌握了,排列组合的基本功就没问题了。
最后,一般的,如果要求列出所有排列组合的解,就用这种递归的方式,如果只是求出个数,可能得用其它方法了。因为只求出个数可以取巧。不用这样穷举每个可能。可以用 递归+记忆 或是动态规划。