尊重经验、独立思考、热爱分享
有些递归很简单理解,比如说链表的递归。画画图就能理解。
关键一条是要保证在每一级调用函数对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;
}
};
有些时候递归必须要按照一定的规律进行,这个规律需要通过对示例总结,或者是结合不设限制的递归的结果来思考规律。有效括号的生成就是这样。
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合3。
示例:
输入:n = 3
输出:[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
按照二叉树结构形式构建递归树(参考78子集题目理解),左右括号都会出现在开头位置,而且左右括号的数量会出现不等情况,也会出现有不匹配的左右括号情况。
观察示例中左右括号的数量和位置的关系发现:
将2.映射到数量上的限制关系就是:
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> res;
recur("", n, n, res);
return res;
}
void recur(string item, int left, int right,
vector<string>&res) { // left, right 左右括号剩余数量
if (left == 0 && right == 0) {
res.emplace_back(item);
return ;
}
// 放左括号, 控制左括号的数量为n;
if (left > 0) {
recur(item+'(', left-1, right, res);
}
// 放右括号, 要控制右括号的数量之外,还要控制右括号与左括号的相对位置。
if (left < right) {
recur(item+')', left, right-1, res);
}
}
};
或者使用全局变量来实现递归结果的存储:
class Solution {
public:
vector<string> res;
string item = "";
vector<string> generateParenthesis(int n) {
recur(n, n);
return res;
}
void recur(int left, int right) { // left, right 左右括号剩余数量
if (left == 0 && right == 0) {
res.emplace_back(item);
return ;
}
// 放左括号
if (left > 0) {
item.push_back('(');
recur(left-1, right);
item.pop_back();
}
// 放右括号, 要控制右括号的数量之外,还要控制右括号与左括号的相对位置。
if (left < right) {
item.push_back(')');
recur(left, right-1);
item.pop_back();
}
}
};
回溯是试探性解法, 对于子集生成来说,这种试探性表现为对给定数组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;
}
};
正好子集就在多叉树的前序遍历上5。
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,返回该数组所有可能的子集(幂集)。6
说明:解集不能包含重复的子集。
示例:
输入: [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();
}
}
};
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合7。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例2:
输入:candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
组合题目和子集有些类似,都是从一个集合中选出一部分。组合与子集不同的是:
除此之外, 该部分方法和图2所示方法几乎完全相同。
class Solution {
public:
vector<vector<int>> ans;
vector<int> item;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
traceback(candidates, target, 0);
return ans;
}
void traceback(vector<int>& nums, int target, int ind) {
if (target == 0) {
ans.emplace_back(item);
}
for (int i=ind; i < nums.size(); i++) {
// target为负时剪枝
if (target<0) return;
target -= nums[i];
item.emplace_back(nums[i]);
// 因为允许元素重复,所以是i
traceback(nums, target, i);
item.pop_back();
target += nums[i];
}
}
};
对回溯代码的for循环可以简写成这样:
for (int i=ind; i < nums.size(); i++) {
if (target<0) continue; // 也可以是break;
item.emplace_back(nums[i]);
traceback(nums, target - nums[i], i);
item.pop_back();
}
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。8
candidates有重复数字,而且其中每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
和上一题不同之处:
class Solution {
public:
vector<vector<int>> ans;
vector<int> item;
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
traceback(candidates, target, 0);
return ans;
}
void traceback(vector<int>& nums, int target, int ind) {
if (target == 0) {
ans.emplace_back(item);
}
for (int i=ind; i < nums.size(); i++) {
// target为负时剪枝
if (target<0 || (i > ind && nums[i] == nums[i-1])) continue;
item.emplace_back(nums[i]);
// 因为不允许元素重复,所以是i+1
traceback(nums, target-nums[i], i+1);
item.pop_back();
}
}
};
这种方向数组的回溯解法适合很多二维平面的地图题,多多体会理解,努力做到举一反三。
class Solution {
public:
vector<vector<string>> ans;
vector<string> location; // 一种解法
vector<vector<int>> mark; // 现有皇后的攻击范围;
vector<vector<string>> solveNQueens(int n) {
// 初始化 mark和location数组 为n行n列
for (int i=0; i<n; i++) {
location.push_back("");
location[i].append(n, '.');
mark.push_back((vector<int>()));
for (int j=0; j<n; j++) {
mark[i].push_back(0);
}
}
// 回溯求解
traceBack(0, n);
return ans;
}
// (x, y)放置皇后后,更新mark数组。
void putDownTheQueen(int x, int y) {
// 方向数组
static const int dx[] = {-1, 1, 0, 0, 1, -1, -1, 1};
static const int dy[] = { 0, 0, 1,-1,-1, -1, 1, 1};
// 标记当前放置的皇后位置
mark[x][y] = 1;
// 更新当前皇后的攻击范围
for (int i=1; i<mark.size(); i++) { // 逐行更新mark
// 按照8个方向更新mark当前行i
for (int j=0; j<8; j++) {
int new_x = x + i*dx[j];
int new_y = y + i*dy[j];
// 攻击范围要在棋盘内
if (new_x < mark.size() && 0 <= new_x
&& new_y < mark.size() && 0 <= new_y ) {
mark[new_x][new_y] = 1;
}
}
}
}
void traceBack(int irow, int n) {
if (irow == n) {
ans.emplace_back(location);
return ;
}
// 逐列尝试求解
for (int j=0; j<n; j++) {
// 不被攻击的位置尝试放置皇后
if (mark[irow][j] == 0) {
vector<vector<int>> tmp_mark = mark;
location[irow][j] = 'Q';
// 放置皇后后,更新mark
putDownTheQueen(irow, j);
// 递归到下一行放置皇后
traceBack(irow+1, n);
// 回溯
mark = tmp_mark;
location[irow][j] = '.';
}
}
}
};
其中mark和location的初始化还可以这么写:
location.resize(n, string(n, '.'));
mark.resize(n, vector<int>(n, 0));
方法1是通过if (mark[irow][j] == 0)来确定当前位置(irow, j)放置皇后是否合法,除此之外,还可以通过数学规律来实现:
假设皇后放在位置 (r, c), 设新皇后位置为 (i, j). 他们关系如下:
class Solution {
public:
vector<vector<string>> ans;
vector<string> location;
vector<vector<string>> solveNQueens(int n) {
// 初始化location数组为n行n列
location.resize(n, string(n, '.'));
// 回溯求解
traceBack(0, n);
return ans;
}
void traceBack(int irow, int n) {
if (irow == n) {
ans.emplace_back(location);
return ;
}
for (int j=0; j<n; j++) { // 逐列尝试求解
// 不被攻击的位置尝试放置皇后
if (check(irow, j)) {
location[irow][j] = 'Q';
// 递归到下一行放置皇后
traceBack(irow+1, n);
location[irow][j] = '.';
}
}
}
bool check(int r, int c) {
for (int i=0; i<location.size(); i++) {
for (int j=0; j<location[0].size(); j++) {
if (location[i][j] == 'Q') { // 找到已经放置皇后的位置坐标;
if (i == r || j == c
|| (abs(i -r) == abs(j - c))) { // 横、竖、斜不能放
return false;
}
}
}
}
return true;
}
};
#include
const int MAXN = 1000005;
int a[MAXN], b[MAXN];
void Merge(int l, int mid, int r) {
// 对a[]的索引范围[l, r]进行二分
int i = l, j = mid + 1, t = 0;
// 左右两个子序列中存在一个没有比较完时
while (i <= mid && j <= r) {
// 升序
if (a[i] > a[j]) {
b[t++] = a[j++];
}
else b[t++] = a[i++];
}
// 把没有处理完的子序列直接复制到a[]对应的范围[l, l+t]
while (i <= mid) b[t++] = a[i++];
while (j <= r) b[t++] = a[j++];
for (i=0; i<t; i++) a[l+i] = b[i]; // 把排好的b[]复制回a[]
}
void Mergesort(int l, int r) {
if (l<r) { // 不重不漏地平分序列a[]
int mid = (l+r)/2; // 平分成左右两个子序列
Mergesort(l, mid);
Mergesort(mid+1, r);
Merge(l, mid, r); // 回溯的过程中进行合并
}
}
int main() {
for (int i=10; i>0; i--) a[i] = i;
Mergesort(0, 9);
for (int i=0; i<10; i++) {
std::cout << a[i] << " ";
}
std::cout << std::endl;
}
给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。10
示例:
输入:nums = [5,2,6,1]
输出:[2,1,1,0]
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素
利用归并算法的合并过程:在从两两数字比较到两两子序列比较的过程中,左子序列的count值,恰好是对应右侧的序列索引j的值,不断累加,最终的到计算结果。同时,为了保证合并过程中count数组的数值不乱序,构建nums[i] – i – count[i] 的映射关系,通过构建
class Solution {
public:
vector<pair<int, int>> a, b;
vector<int> count;
vector<int> countSmaller(vector<int>& nums) {
// 构建pair对形成i到count的映射, 并初始化count
for (int i=0; i<nums.size(); i++) {
a.push_back(make_pair(nums[i], i));
b.push_back(make_pair(nums[i], i));
count.push_back(0);
}
merge_sort(0, nums.size()-1);
return count;
}
void merge_two_vec(int l, int m, int r) {
int i = l, j = m+1, t = 0;
while (i <= m && j <= r) {
if (a[i].first <= a[j].first) {
// 通过pair对映射计算count值
// 先计算,在合并到b
count[a[i].second] += (j-m-1);
b[t++] = a[i++];
}
else {
b[t++] = a[j++];
}
}
while(i<= m) {
// 通过pair对映射计算count值
// 先计算,在合并到b
count[a[i].second] += (j-m-1);
b[t++] = a[i++];
}
while(j<= r) b[t++] = a[j++];
for(int i=0; i<t; i++) a[l+i] = b[i];
}
void merge_sort(int l, int r) {
if (l<r) {
int m = (l+r)/2;
merge_sort(l, m);
merge_sort(m+1, r);
merge_two_vec(l, m, r);
}
}
};
递归,要融合子集问题、组合问题、N皇后之类的回溯解法问题以及分治思想的算法来理解。这样对于递归会有更多的认识:
https://www.bilibili.com/video/BV1GW411Q77S?p=4 ↩︎
https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof ↩︎
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/generate-parentheses
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 ↩︎
来源:力扣(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
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 ↩︎
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/combination-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 ↩︎
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/combination-sum-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 ↩︎
罗勇军,郭卫斌.算法竞赛-入门到进阶.——北京:清华大学出版社,2019 ↩︎
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/count-of-smaller-numbers-after-self
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 ↩︎