leetcode 90.子集II
class Solution {
private:
vector<int> path;
vector<vector<int>> result;
void backtracking(vector<int>& nums,int startIndex,vector<bool>& used){//1.确定函数的参数和返回值
//2.确定终止条件
result.push_back(path);
if(startIndex==nums.size()) return ;
//确定单层逻辑
for(int i=startIndex;i<nums.size();i++){
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false){
continue;
}else{
used[i]=true;// 处理
path.push_back(nums[i]);//处理节点
backtracking(nums,i+1,used);//递归到下一层
used[i]=false;//回溯
path.pop_back();//回溯
}
}
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
result.clear();
path.clear();
vector<bool> used(nums.size(),false);
sort(nums.begin(),nums.end());//先排序,这样的话我们可以使相同的元素变成相邻状态
backtracking(nums,0,used);
return result;
}
};
注意:
- 子集问题中树的每个节点都要收集,因此每次递归不需要if判断,直接push进result。
- result.push的时候不需要判断path是否合法,因为在生成path的时候已经判断了这个path是否合法,即每个往path里面加的数据都经过了
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false){ continue;
的判断了。 - for循环里如果一个数字不合法,那么continue,而不是break,因为那一层也许只有那一个数字不合法,但不代表后面的数字不合法,直接跳过当前数字continue下一个数字即可,而IP复原问题中一个数字不合法代表那一条支路全部不合法,不需要再向下递归了,直接break回到上一层遍历别的支路。
- 另一种去重方法 不使用used数组去重,而是使用startIndex,这个理解起来很抽象,就是如果i>startIndex,但是nums[i]=nums[i-1]说明nums[i-1]必定在那一层的前面已经遍历过了,因此当前的nums[i]就不用遍历了,直接continue到下一个元素即可
for(int i=startIndex;i<nums.size();i++){ path.push_back(nums[i]);//3.1处理 backtracking(nums,i+1);//3.2递归 path.pop_back();//3.3回溯 }
- 还可以使用set去重,但依旧需要对数组sort排序,原理可以自己画个图 以 212 数组为例,如果不排序 212 那么在1节点下收集结果的时候会重复收集12 ,但是如果排序了后,通过i=startIndex可以自动帮我们去掉这个重复的结果12。set去重的代码如下
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex, vector<bool>& used) {
result.push_back(path);
unordered_set<int> uset; // 定义set对同一节点下的本层去重
for (int i = startIndex; i < nums.size(); i++) {
if (uset.find(nums[i]) != uset.end()) { // 如果发现出现过就pass
continue;
}
uset.insert(nums[i]); // set跟新元素
path.push_back(nums[i]);
backtracking(nums, i + 1, used);
path.pop_back();
}
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
result.clear();
path.clear();
vector<bool> used(nums.size(), false);
sort(nums.begin(), nums.end()); // 去重需要排序
backtracking(nums, 0, used);
return result;
}
};
需要注意的是:使用set去重的版本相对于used数组的版本效率都要低很多,大家在leetcode上提交,能明显发现。
原因在回溯算法:递增子序列 (opens new window)中也分析过,主要是因为程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且insert的时候其底层的符号表也要做相应的扩充,也是费时的。
而使用used数组在时间复杂度上几乎没有额外负担!
使用set去重,不仅时间复杂度高了,空间复杂度也高了,在本周小结!(回溯算法系列三) (opens new window)中分析过,组合,子集,排列问题的空间复杂度都是O(n),但如果使用set去重,空间复杂度就变成了O(n^2),因为每一层递归都有一个set集合,系统栈空间是n,每一个空间都有set集合。
那有同学可能疑惑 用used数组也是占用O(n)的空间啊?
used数组可是全局变量,每层与每层之间公用一个used数组,所以空间复杂度是O(n + n),最终空间复杂度还是O(n)。