必须了解的编程基础 -- 递归篇小节:递归、回溯、分治算法及其在子集、组合、N皇后、归并排序等方面的应用

递归、回溯和分治小节1

尊重经验、独立思考、热爱分享

1. 递归

有些递归很简单理解,比如说链表的递归。画画图就能理解。

1.1 剑指 Offer 22. 链表中倒数第k个节点2

关键一条是要保证在每一级调用函数对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;
    }
};

有些时候递归必须要按照一定的规律进行,这个规律需要通过对示例总结,或者是结合不设限制的递归的结果来思考规律。有效括号的生成就是这样。

1.2 LeetCode 22 有效括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合3

示例:

输入:n = 3
输出:[
       "((()))",
       "(()())",
       "(())()",
       "()(())",
       "()()()"
     ]

按照二叉树结构形式构建递归树(参考78子集题目理解),左右括号都会出现在开头位置,而且左右括号的数量会出现不等情况,也会出现有不匹配的左右括号情况。
观察示例中左右括号的数量和位置的关系发现:

  1. 左右括号的数量都是n;
  2. 每一个有效括号,左括号位置都要先于右括号;

将2.映射到数量上的限制关系就是:

  1. 先递归左括号,然后是右括号;
  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();
        }

    }
};

2 子集

回溯是试探性解法, 对于子集生成来说,这种试探性表现为对给定数组nums中的每个元素是放入还是不放入item中。

2.1 78. 子集4

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

输入: nums = [1,2,3]
输出:
[
  [3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
]
(1) 解法1:回溯 + 二叉树结构

二叉树的第i层模拟决定是否将nums的第i个元素放入子集中。二叉树的叶子节点就是最终的解。

图1 二叉树构造解空间
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;
    }
};
(2) 解法2:回溯+多叉树结构
图2 多叉树构造解空间

正好子集就在多叉树的前序遍历上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();
      }
   }
};
(3) 注:子集生成的位运算解法

根据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;
    }
};

2.2 90 子集II

给定一个可能包含重复元素的整数数组 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]来理解:

  1. 元素位置不同,但是在组成子集中顺序相同:
    [2,1,2,2]的第1、2、3组成子集[2,1,2];
    [2,1,2,2]的第1、2、4组成子集[2,1,2];
  2. 元素位置不同,但是在组成子集中顺序不相同:
    [2,1,2,2]的第1、2、3组成子集[2,1,2];
    [2,1,2,2]的第2、3、4组成子集[1,2,2]
(1) 重复子集解决方法1:排序+set去重

有重复的元素就去重,去重的话可以借助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;
    }
};
(2) 重复子集解决方法2:回溯法+多叉树+剪枝

对于数组[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();
        }
    }
};

3. 组合

3.1 leetcode 39. 组合总和

给定一个无重复元素的数组 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]
]

组合题目和子集有些类似,都是从一个集合中选出一部分。组合与子集不同的是:

  1. 组合可以允许重复选元素:这体现在嵌套递归时候传入参数i,而不是i+1。
  2. 组合需要对多叉树的生长进行限制,因为组合内元素之和要等于target。

除此之外, 该部分方法和图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();
    }

3.2 LeetCode 40. 组合总和 II

给定一个数组 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]
]

和上一题不同之处:

  1. 数组有重复数字:使用多叉树+同层去重剪枝;if(i > ind && nums[i] == nums[i-1]) continue;
  2. 数组中的元素不能重复使用:回溯程序中传入i+1,而不是i。
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();
        }
    }
};

4. 回溯经典题:51 N皇后问题

4.1 方法1:方向数组+攻击范围标记

这种方向数组的回溯解法适合很多二维平面的地图题,多多体会理解,努力做到举一反三。

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));  

4.2 方法2:数学公式检查皇后位置是否合法 + 回溯

方法1是通过if (mark[irow][j] == 0)来确定当前位置(irow, j)放置皇后是否合法,除此之外,还可以通过数学规律来实现:
假设皇后放在位置 (r, c), 设新皇后位置为 (i, j). 他们关系如下:

  1. 不同行: r ≠ i r \neq i r=i
  2. 不同列: c ≠ j c \neq j c=j
  3. 斜对角:不能再(r,c)的斜率为1或-1的直线上,即 ∣ j − c ∣ ∣ i − r ∣ ≠ 1 \frac{|j-c|}{|i-r|} \neq 1 irjc=1 => ∣ i − r ∣ ≠ ∣ j − c ∣ |i-r| \neq |j-c| ir=jc
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;
    }
};

5. 分治算法

5.1 分治算法典型应用1:归并排序

5.1.1 归并排序流程
  1. 分解:对数列进行简单地二分,分成左右两部分。不像快速排序要求左部要小于右部。
  2. 对子序列排序:分解到子序列只有1个元素,此时各个子序列便都是有序的。
  3. 合并:合并两个有序的子序列到临时空间b[]中,最后将临时空间b[]中的数复制到a[]对应的范围内;前面的分解是通过递归实现,而这个合并是在回溯的过程中完成的。
5.1.2 归并排序 C++代码实现9
#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;
}

5.2 分治算法典型应用2

5.2.1 例题:315. 计算右侧小于当前元素的个数

给定一个整数数组 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 个更小的元素
5.2.2 分治思想解法

利用归并算法的合并过程:在从两两数字比较到两两子序列比较的过程中,左子序列的count值,恰好是对应右侧的序列索引j的值,不断累加,最终的到计算结果。同时,为了保证合并过程中count数组的数值不乱序,构建nums[i] – i – count[i] 的映射关系,通过构建的pair对实现。

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皇后之类的回溯解法问题以及分治思想的算法来理解。这样对于递归会有更多的认识:

  1. 可以将递归分为两个方向:前进方向和返回方向;
  2. 有效括号生成(1.2)利用的是对前进方向的约束来求解问题;
  3. 链表中倒数第k个节点用的是返回的方向来求解问题;
  4. 子集和组合问题是两个方向结合使用,即所谓的回溯,从而达到在候选解空间中试探求解的效果。组合问题和子集问题不同的是,组合问题除了对组合内元素和为一个定值之外, 而且还允许多次选择同一个元素。
  5. N皇后问题是典型的两个方向都使用,和子集组合问题相比,N皇后只多了一个放置皇后是否有效的检测check(), 其余部分基本相同。
  6. 分治思想是两个方向上都有利用,前进方向完成二分序列,返回方向实现合并操作。和前面方法相比,其特点是在返回方向上,不再像回溯中的恢复成原来值的操作(例如 前面的item.pop_back(), 和 location[irow][j] = ‘.’ ), 而是按照升序或者降序将子序列合并到一个临时空间b中,然后再将临时空间b中的值返回到原序列a中。

  1. https://www.bilibili.com/video/BV1GW411Q77S?p=4 ↩︎

  2. https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof ↩︎

  3. 来源:力扣(LeetCode)
    链接:https://leetcode-cn.com/problems/generate-parentheses
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 ↩︎

  4. 来源:力扣(LeetCode)
    链接:https://leetcode-cn.com/problems/subsets
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 ↩︎

  5. https://leetcode-cn.com/problems/subsets/solution/hui-su-si-xiang-tuan-mie-pai-lie-zu-he-zi-ji-wen-t/ ↩︎

  6. 来源:力扣(LeetCode)
    链接:https://leetcode-cn.com/problems/subsets-ii
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 ↩︎

  7. 来源:力扣(LeetCode)
    链接:https://leetcode-cn.com/problems/combination-sum
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 ↩︎

  8. 来源:力扣(LeetCode)
    链接:https://leetcode-cn.com/problems/combination-sum-ii
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 ↩︎

  9. 罗勇军,郭卫斌.算法竞赛-入门到进阶.——北京:清华大学出版社,2019 ↩︎

  10. 来源:力扣(LeetCode)
    链接:https://leetcode-cn.com/problems/count-of-smaller-numbers-after-self
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。 ↩︎

你可能感兴趣的:(编程基础,面试,c++,递归法,分治算法,数据结构)