尊重经验、独立思考、热爱分享
有些递归很简单理解,比如说链表的递归。画画图就能理解。
关键一条是要保证在每一级调用函数对k的影响都是全局性的。实现方式是,在返回的时候返回要访问的节点。
递归函数的功能:将head指向倒数第k个节点;
递归出口:head为空
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int& k) {
// int& k 保证每一级调用函数对k都是全局性影响
if (!head) {
return nullptr;
}
// cur 接受下级函数的返回值
ListNode* cur = getKthFromEnd(head->next, k);
// 下面是回溯过程:往上级调用函数返回的过程
k--;
// 找到了倒数第k个节点,向上级函数调用中返回
if (k==0) return head;
// 向上级调用返回 cur
return cur;
}
};
涉及多个递归一起使用的时候稍微有些难理解,常见的使用情景如二叉树的深度优先搜索,图的深度优先搜索,多叉树的深度优先搜索等。在此基础上再加入回溯和剪枝的要求,就会使得理解的难度加大很多。如下一节内容所示。
回溯是试探性解法, 对于子集生成来说,这种试探性表现为对给定数组nums中的每个元素是放入还是不放入item中。
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
二叉树的第i层模拟决定是否将nums的第i个元素放入子集中。二叉树的叶子节点就是最终的解。
class Solution {
public:
vector<vector<int>> ans;
vector<int> item;
void recur(int ind, vector<int>& nums) {
if (ind >= nums.size()) {
ans.emplace_back(item);
return ;
}
item.push_back(nums[ind]); // 放入nums[ind]
recur(ind+1, nums); // 处理nums后续的元素
item.pop_back(); // 不放入nums[ind]
recur(ind+1, nums); // 处理nums后续的元素
}
vector<vector<int>> subsets(vector<int>& nums) {
recur(0, nums);
return ans;
}
};
正好子集就在多叉树的前序遍历上3。
class Solution {
public:
vector<vector<int>> res;
vector<int> item;
vector<vector<int>> subsets(vector<int>& nums) {
backtrack(0, nums);
return res;
}
void backtrack(int ind, vector<int>& nums) {
// 子集正好处在前序遍历顺序上
res.emplace_back(item);
// i 控制递归的深度;所以不需要再额外通过if规定递归出口;
for (int i=ind; i < nums.size(); i++) {
item.emplace_back(nums[i]);
backtrack(i+1, nums);
item.pop_back();
}
}
};
根据1.2.2带回溯的递归解法可以知道,递归回溯的实质是模拟nums中每个元素再子集中出不出现的情况。其实,还可以通过子集和二进制的对应关系来实现子集生成。以{a0, a1, a2}的子集为例:
子集 | 空集 | a0 | a1 | a1,a0 | a2 | a2,a0 | a2,a1 | a2,a1,a0 |
---|---|---|---|---|---|---|---|---|
二进制数 | 000 | 001 | 010 | 011 | 100 | 101 | 110 | 111 |
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
int n = nums.size();
vector<int> item;
vector<vector<int>> ans;
for (int i=0; i<(1<<n); i++) {
// 列举每个二进制数
for (int j=0; j<n; j++) {
// 将二进制数映射到对应的子集上, j -> ind
if (i&(1<<j)) {
// 从低位到高位寻找i二进制数中的1,
// 也就是一次只对一个二进制都与1进行与运算, 即i和1,2,4,8,16,..., 1<
// 例: 5 = (101); 从低位开始第一个:1&3 ; 第二个:2&3; 第三个: 4&3.
item.push_back(nums[j]);
}
}
ans.emplace_back(item);
item.clear();
}
return ans;
}
};
将item改成局部变量,利用局部变量初始值为空,来避免每个子集结束后都要对全局类型变量item进行清空的操作。
好处:代码更加简练;
弊端:频繁创建临时变量,时间和空间的复杂度都上升了。
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> ans;
for (int i=0; i<(1<<n); i++) {
// 列举每个二进制数
vector<int> item;
for (int j=0; j<n; j++) {
// 将二进制数映射到对应的子集上, j -> ind
if (i&(1<<j)) {
// 从低位到高位寻找i二进制数中的1;
// 例: 5 = (101); 从低位开始第一个:1&3 ; 第二个:2&3; 第三个: 4&3
item.push_back(nums[j]);
}
}
ans.emplace_back(item);
}
return ans;
}
};
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。4
说明:解集不能包含重复的子集。
示例:
输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
如果使用1.2.1 中的方法的话,[1,2,2’]会得到这样的结果:[], [1], [2], [1,2], [2’], [1,2’], [2,2’], [1,2,2’]。重复子集有:[2] 和[2’], [1,2]和[1,2’]。
重复子集出现的形式有两种:
可以通过这个例子[2,1,2,2]来理解:
有重复的元素就去重,去重的话可以借助set集合。但是,注意到情况2中的两个重复子集无法通过set去重。
所以,自然会想到希望重复的子集中元素的顺序也都相同。这一点可以通过对原数字集合nums排序实现。
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> ans;
set<vector<int>> ans_set;
sort(nums.begin(), nums.end());
for (int i=0; i <(1<<n); i++) {
vector<int> item;
for (int j=0; j<n; j++) {
if (i&(1<<j)) {
item.push_back(nums[j]);
}
}
ans_set.insert(item);
}
for (auto&e: ans_set) {
ans.emplace_back(e);
}
return ans;
}
};
或者递归回溯:
class Solution {
public:
vector<vector<int>> ans;
vector<int> item;
set<vector<int>> ans_set;
void dfs(int ind, vector<int>& nums) {
if (ind >= nums.size()) {
ans_set.insert(item);
return;
}
item.emplace_back(nums[ind]);
dfs(ind+1, nums);
item.pop_back();
dfs(ind+1, nums);
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end());
dfs(0, nums);
for (auto&e: ans_set) ans.emplace_back(e);
return ans;
}
};
对于数组[1,2,2’],将图2中的3等效替换为2’,就可以得到利用回溯法+多分树结构方法得到的结果,分析重复子集生成的规律发现:
重复子集:[2]和[2’]; [1,2]和[1,2’]都出现在同一层。
因此,只需要通过剪枝操作,将同层的第二次出现的相同子集的分枝剪去即可。剪枝方式为:if(i > ind && nums[i] == nums[i-1]) continue;
class Solution {
public:
vector<vector<int>> res;
vector<int> item;
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end());
backtrack(0, nums);
return res;
}
void backtrack(int ind, vector<int>& nums) {
// 子集正好处在前序遍历顺序上
res.emplace_back(item);
// i 控制递归的深度;所以不需要再额外通过if规定递归出口;
for (int i=ind; i < nums.size(); i++) {
if(i > ind && nums[i] == nums[i-1]) continue;
item.emplace_back(nums[i]);
backtrack(i+1, nums);
item.pop_back();
}
}
};
https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof ↩︎
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/subsets
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 ↩︎
https://leetcode-cn.com/problems/subsets/solution/hui-su-si-xiang-tuan-mie-pai-lie-zu-he-zi-ji-wen-t/ ↩︎
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/subsets-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 ↩︎