原题目
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
解题思路
图解
- 个人觉得解一个题目先画出它的数据结构,这样能理清数据处理的逻辑关系,如上图解,数据是一层层遍历的,可以联想到递归。
- 题目说到数组中的每个数字在每个组合中只能使用一次,也就是说在下一层遍历的时候就要跳过当前的元素。
例如:
第一层 [1]
第二层 就只能从剩下的 [2, 2, 2, 5] 中遍历,跳过 1 ,这里假设遍历了第一个 2,那么此时解集是 [1, 2]
第三层 就只能从 [2, 2, 5] 中遍历,要跳过 第一个2 ,以此类推
- 题目说明了解集不能包含重复的组合。再看一下图解,你会发现如果每层遍历的元素相同,那么就会得到重复解。所以每层要排除相同的元素(去重)。为了方便去重,先对数组进行排序,然后每层中相邻元素相同,就剪掉该分支。
例如:
第一层 [1]
第二层中可以让1 和 剩下的 [2, 2, 2, 5] 这四个数中的任意一个相加,也就是这一层可以得到 [1, 2], [1, 2], [1, 2], [1, 5]。你是不是发现在这层,解集开始重复了? 导致这个结果的原因就是因为这层遍历的元素有三个 2 ,所以会得到三个相同的解集。
代码
var combinationSum2 = function (candidates, target) {
// 初始化一个数组,因为结果是数组
let res = [];
// 对数组进行排序,方便去重
candidates.sort((a, b) => a - b);
let ope = (cur, start, sum) => {
// 防止爆栈
if (sum >= target) {
if (sum === target) {
// 存储本轮遍历的结果
res.push(cur);
}
return;
}
// 遍历数组
for (let i = start; i < candidates.length; i++) {
// 思路3: 剪掉本层相同的元素
if (i > start && candidates[i] === candidates[i - 1]) {
continue;
}
cur.push(candidates[i]);
// 思路2: 跳过下一层中与当前相同的元素
ope(cur.slice(), i + 1, sum + candidates[i]);
cur.pop();
}
};
ope([], 0, 0);
return res;
};
console.log(combinationSum2([10, 1, 2, 7, 6, 1, 5], 8));
备注
cur.pop();
代码中的这句,一开始我没想通,但是再画画图解就变得很明了了。
pop() 方法,就是删除数组中的最后一项,那么为什么要删除最后一项呢?目的是为了去掉导致解集不正确的最后一层的遍历,然后再去遍历该层其他的元素。
比如:
输入的数组是 [10, 1, 2, 7, 6, 1, 5] , target 是 8
排序后的数组为 [1, 1, 2, 5, 6, 7, 10]
第一层 [1]
第二层 [1, 1]
第三层 [1, 1, 2]
第四层 [1, 1, 2, 5]
第五层 [1, 1, 2, 5, 6] 注意此时 sum(数组元素总和) 已经 大于 target 了。也就是说此时的解集已经是错误的了,那么这一层就是导致解集不正确的最后一层的遍历,那么 cur.pop() 就会删掉这层遍历的结果,也就是使遍历回到第四层 [1, 1, 2, 5],然后再重新选择第五层的遍历元素。
如果第五层的所有可遍历的元素都是错误的,那么就会删掉第四层的遍历结果,也就是使遍历回到第三层 [1, 1, 2],然后再重新选择第四层的遍历元素
以此类推,直到回到第二层 [1, 1]时,重新选择第三层的遍历元素为 6 时,sum === target 得到解集,然后添加到 res 数组中 [[1, 1, 6]]