递归函数基础

求子集

#medium 78. 子集 - 力扣(LeetCode) 已知一组数(无重复元素),求这组数字可以组成的所有子集 局部思考:只生成[1],[1,2],[1,2,3] 利用递归

方法1:回溯法

回溯试探法 用递归树来看 暴力破解法的一种

本题的解决方法

class Solution {

public:

    vector<vector<int>> subsets(vector<int>& nums) {

        std::vector<std::vector<int>> result;//存储最终结果的result

        std::vector<int> item;//回溯时候产生各子集的数组

        result.push_back(item);//把空集push进result

        generate(0,nums,item,result);//计算各个子集

        return result;

    }

  
  

private:

    void generate(int i,std::vector<int>&nums,std::vector<int>&item,std::vector<std::vector<int> >&result){

        if(i >= nums.size()){//递归结束条件,当i大于nums.size

            return ;

        }

        item.push_back(nums[i]);//当前生成子集加入result

        result.push_back(item);

        generate(i+1,nums,item,result);//第一次递归

        item.pop_back();

        generate(i+1,nums,item,result);//回溯的第二次递归

    }   

};

BTW,由于是引用调用,所以,在pop不仅仅是用于选择该点是否进入item,还保证了回到该层级的时候item能够回复,以这个图为例子,在第三次递归调用之后会经历第四次递归return,然后在回溯后的第五层递归里面把[1,2,3]里面的3pop掉,然后再依次回溯到第三次和第二次,然后再第6次递归的时候把2弹出

方法二:位运算法

解决方法如下

class Solution {

public:

    vector<vector<int>> subsets(vector<int>& nums) {

        std::vector<std::vector<int> >result;

        int all_set   =  1<<nums.size();// 把1左移num.size()个单位

            //设置全部集合的最大值+1

        for(int i =0;i<all_set ;i++){

            std::vector<int> item;

            for(int j = 0;j<nums.size();j++){

                if(i&(1<<j)){//构造数字i代表的集合,各元素存储进item

                item.push_back(nums[j]);

  

                }               

            }

            result.push_back(item);

        }

        return result;

    }

};

其中 1<< n 就是$2^n$ 相当于把1右移了n个单位变成 100……0 整数i代表从0到$2^n-1$ 这$2^n$个集合 1<<j 即为构造nums数组的第j个元素 若i&(1<<j)为真则nums[j] 放入数组

如果你没看懂的话,以下是工作示例 : 感谢必应

- 当i=0时,二进制表示为000,所以不会有任何元素被添加到子集中,得到的子集为空集,即`[]`。
- 当i=1时,二进制表示为001,所以只有第0位为1,因此只有`nums[0] = 1`被添加到子集中,得到的子集为`[1]`。
- 当i=2时,二进制表示为010,所以只有第1位为1,因此只有`nums[1] = 2`被添加到子集中,得到的子集为`[2]`。
- 当i=3时,二进制表示为011,所以第0位和第1位都为1,因此`nums[0] = 1`和`nums[1] = 2`都被添加到子集中,得到的子集为`[1, 2]`。
- 当i=4时,二进制表示为100,所以只有第2位为1,因此只有`nums[2] = 3`被添加到子集中,得到的子集为`[3]`。
- 当i=5时,二进制表示为101,所以第0位和第2位都为1,因此`nums[0] = 1`和`nums[2] = 3`都被添加到子集中,得到的子集为`[1, 3]`。
- 当i=6时,二进制表示为110,所以第1位和第2位都为1,因此`nums[1] = 2`和`nums[2] = 3`都被添加到子集中,得到的子集为`[2, 3]`。
- 当i=7时,二进制表示为111,所以第0位、第1位和第2位都为1,因此`nums[0] = 1`、`nums[1] = 2`和`nums[2] = 3`都被添加到子集中,得到的子集为`[1, 2, 3]`。

最终,该方法返回的结果向量为:`[[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]`。

求子集 II

#medium 90. 子集 II - 力扣(LeetCode) 已知一组数(其中有重复元素),求这组数可以组成的所有子集 结果中无重复子集 注:集合没有顺序

排序的目的是为了防止[1,2,2][2,1,2]的情况出现,当使用全部从小到大进行排序之后,所有的结果全部变为相同的子集都会是[1,2,2]

解决方法如下

class Solution{

public:

    void generate(int i,std::vector<int>&nums,std::vector<std::vector<int>>&result,std::set<std::vector<int>>&res_set);

    vector<vector<int>> subsetsWithDup(vector<int>& nums) {

            std::vector<std::vector<int> > result;

            std::vector<int> item ;

            std::set<std::vector<int>>res_set;//去重用的集合set

            std::sort(nums.begin(),nums.end());//对num数组进行排序

            result.push_back(item);

            generate(0,nums,result,item,res_set);

            return result;

    }

private:

    void generate(int i,std::vector<int>&nums,std::vector<std::vector<int>>&result,std::vector<int>&item,std::set<std::vector<int>>&res_set){

        if(i >= nums.size()){

            return ;

        }

        item.push_back(nums[i]);

        if(res_set.find(item)==res_set.end()){//如果在集合中没有找到

            result.push_back(item);//item 放进result里面

            res_set.insert(item);//item放进res_set里面

        }

        generate(i+1,nums,result,item,res_set);

        item.pop_back();

        generate(i+1,nums,result,item,res_set);

    }

};

组合数之和 II

#medium 40. 组合总和 II - 力扣(LeetCode) 已知一组数(有重复)求这组数的所有子集里面,子集中各个元素和整数位target的子集,结果中无重复的子集

方法1

用[[第四章:递归,回溯和分治#求子集 II|求子集II]]的方法求出全部子集然后再处理就行 非常草率 甚至不需要额外的再写一遍代码,再加一段遍历result,然后判断target并加入新result的代码就行 别说你不会 然而。。。。。 提交之后会有出现Time Limit Exceed 无论是回溯法还是位运算法,整体的时间复杂度位O($2^n$) 所以求出全部子集在一些情况下会爆掉

方法2

在搜索回溯的过程中进行剪枝操作 在DFS的时候还会用上的

解决方法如下

class Solution {

public:

    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {

        std::vector<std::vector<int>>result;

        std::vector<int> item;

        std::set<std::vector<int>>res_set;

        std::sort(candidates.begin(),candidates.end());

        generate(0,candidates,result,item,res_set,0,target);

        return result;

    }

private:

    void generate(int i,std::vector<int>&nums,std::vector<vector<int>>&result,std::vector<int>&item,std::set<std::vector<int>>&res_set,int sum,int target){ //sum是当前子集item中的元素和

        if(sum>target||i>=nums.size()){//当元素已经选完或者sum超过target,剪枝操作

            return;

        }

        sum = sum+nums[i];

        item.push_back(nums[i]);

        if(sum == target&&res_set.find(item)==res_set.end()){//当item的元素和即为target且该结果未添加时

            result.push_back(item);

            res_set.insert(item);

        }

        generate(i+1,nums,result,item,res_set,sum,target);

        sum = sum-nums[i];

        item.pop_back();//回溯时候删除nums[i]和sum

        generate(i+1,nums,result,item,res_set,sum,target);

    }

};

然而 这个代码在leetcode跑仍然会超时,开课的时候有172个样例,现在有176个 byd乐扣往tmd样例里面加了100个1,直接把tmd时间堆爆掉 所以这里采用zen-feistel2js思路与方法 我对代码进行了写修改,加了注释改了写法,但思路还是一样的 然而修改之后比他之前速度反而更慢了

class Solution {

public:

    vector<vector<int>> result; // 存储最终结果

    vector<int> path; // 存储当前路径

    void backtracking(vector<int> candidates, int target, int sum, int startindex, int i) {

        if (i >= candidates.size()) return; // 如果 i 超出数组范围,则返回

        if (candidates[i] + sum > target) return; // 如果当前元素加上当前路径的和超过目标和,则返回

        if (i > startindex && candidates[i] == candidates[i - 1]) { // 跳过重复元素,如果这个元素与上一个元素相同,说明这个元素已经处理过了,不需要再额外处理,直接进入下一次递归,且不需要回溯

            backtracking(candidates, target, sum, startindex, i + 1);

            return;

        }

        sum += candidates[i]; // 更新 sum 的值

        path.push_back(candidates[i]); // 将当前元素添加到路径中

        if (sum == target) { // 如果当前路径的和等于目标和

            result.push_back(path); // 将当前路径添加到结果集中

        }

        backtracking(candidates, target, sum, i + 1, i + 1); // 递归搜索剩余元素

        sum -= candidates[i]; // 回溯时撤销选择

        path.pop_back(); // 回溯时撤销选择

        backtracking(candidates, target, sum, startindex, i + 1); // 不选择当前元素,继续搜索

    }

    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {

        sort(candidates.begin(), candidates.end()); // 对候选数组进行排序

        backtracking(candidates, target, 0, 0, 0); // 开始搜索

        return result; // 返回结果

    }

};

zen-feistel2js的思路除了基本的对和判断和叶节点判断之外,还删除了重复数据的影响

        if (i > startindex && candidates[i] == candidates[i - 1]) { // 跳过重复元素,如果这个元素与上一个元素相同,说明这个元素已经处理过了,不需要再额外处理,直接进入下一次递归,且不需要回溯

            backtracking(candidates, target, sum, startindex, i + 1);

            return;

        }

解决了byd乐扣给你塞100个1的问题

生成括号

#medium 22. 括号生成 - 力扣(LeetCode) 已知n组括号,生成n组括号所有合法的可能性 找出全部可能性的实现 BTW 在C++里面,string是值传递,不需要显式的修改字符,每一次递归都会建立新的item副本

接下来是剪枝

解决方法如下

class Solution {

public:

    vector<string> generateParenthesis(int n) {

    std::vector<std::string> result;

    generate("",n,n,result);

    return result;

    }

private:

    void generate(std::string item,int left,int right,std::vector<std::string>&result){//left和right是分别还剩下几个左右括号

        if(left == 0&&right == 0){//如果左右括号全部用完

            result.push_back(item);//到递归树的叶节点才开始收入进result

            return;

        }

        if(left>0){//还可以放左括号

            generate(item+'(',left-1,right,result);

        }

        if(left < right){//如果)的剩余数量比(大,说明left先放置了

            generate(item+')',left,right-1,result);

        }

    }

};

N皇后问题

#hard
51. N 皇后 - 力扣(LeetCode) 最早追溯到1848年,马克思 贝瑟尔提出了8皇后问题 此题是该题的升级版 将$N$个皇后摆放在$NN$ 的棋盘中,互相*不可攻击,有多少种摆放方式,每种摆放的具体方式是什么 放置皇后的初步实现 回溯算法,启动!! 解决思路如下

class Solution {

public:

    vector<vector<string>> solveNQueens(int n) {

        std::vector<std::vector<std::string> >result;//存储最终结果的数组

        std::vector<std::vector<int> > mark; //标记棋盘是否可以放置皇后的二维数组,皇后的攻击范围

        std::vector<std::string>location;//存储某一次的摆放结果,皇后的摆放位置

        for(int i = 0;i<n;i++){

            mark.push_back(std::vector<int>());//mark每行push进一个空数组

            for(int j = 0;j<n; j++){

                mark[i].push_back(0);//把mark初始化为全0

            }

                location.push_back("");  //location全部初始化为.

                location[i].append(n,'.'); 

        }

        generate(0,n,location,result,mark);

        return result;

    }

private: 

    void generate(int k, int n,//k代表完成了几个皇后的放置,正在放置第,看k个皇后

    std::vector<std::string>& location,//某次的结果

    std::vector<std::vector<std::string>>& result,//最终结果

    std::vector<std::vector<int>>&mark){//标记数组

        if(k == n){ //当k== n代表完成了皇后的放置

            result.push_back(location);

            return ;

        }

        for(int i = 0;i < n;i++){//尝试0到n-1列

            if(mark[k][i] == 0){//如果该点为0,可以放置皇后

                std::vector<std::vector<int> > temp_mark = mark;//回溯前的mark镜像

                location[k][i] = 'Q';//记录当前皇后位置

                put_down_the_queen(k,i,mark);//放置皇后

                generate(k+1,n,location,result,mark);//递归下一行皇后的位置

                mark = temp_mark;//mark回溯回赋值前状态

                location[k][i] = '.';//当前皇后的位置重新赋值

            }

        }
        return ;

    }
};

put_down_the_queem代码

 void put_down_the_queen(int x, int y,std::vector<std::vector<int> > &mark){

        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++){

            for(int j=0 ;j<8;j++){//八个方向

                int new_x = x+i*dx[j];//x和y的新位置

                int new_y = y+i*dy[j];//组成新点

                if(new_x >= 0 &&new_x< mark.size() && new_y>=0 && new_y<mark.size()){//检查新位置在不在循环内

                    mark[new_x][new_y] = 1;  //将该点设置为1

                }

            }

        }

  

    }

关于递归的具体思路 在递归过程中,函数到正常递归,直到找到某一结果(返回结果)或这某一行全都是1,无法插入皇后,在无法插入皇后的时候,函数在这一层递归里面不进入if,自动到最后return回上一层递归,执行generate之后的代码,比如本层递归是1111,上一层递归是1Q10(此时i= 1),他会回到上一层递归,从i = 1的循环里的generate函数的下一行开始执行,直到循环到i=3 进行下一层递归,完成回溯 这算是递归基础知识把

分治法与归并排序

普通归并排序

代码如下

void merge_sort_two_vec(std::vector<int>&sub_vec1,std::vector<int>&sub_vec2,std::vector<int>&vec){

    int i = 0;

    int j = 0;

    while(i<sub_vec1.size()&&j<sub_vec2.size()){

        if(sub_vec1[i]<=sub_vec2[j]){

            vec.push_back(sub_vec1[i]);

            i++;

        }

        else{

            vec.push_back(sub_vec2[j]);

            j++;

        }

    }

    for(;i<sub_vec1.size();i++){  //push进剩余元素

        vec.push_back(sub_vec1[i]);

    }

    for(;j<sub_vec2.size();j++){

        vec.push_back(sub_vec2[j]);

    }

}
分治算法的归并排序

在[[第一章:链表#排序链表的合并(下)#方法3——分治后相连 分治思想|链表分治归并]]出现过

归并排序实现

void merge_sort(std::vector<int>&vec){

    if(vec.size()<=2 ){

        return ;//当子问题最够小直接求解

    }

    int mid = vec.size()/2;

    std::vector<int>sub_vec1;

    std::vector<int>sub_vec2;

    for(int i = 0 ;i<mid;i++){

        sub_vec1.push_back(vec[i]);

    }

    for(int i = mid;i<vec.size();i++ ){

        sub_vec2.push_back(vec[i]);

    }

    merge_sort(sub_vec1);

    merge_sort(sub_vec2);

    vec.clear();

    merge_sort_two_vec(sub_vec1,sub_vec2,vec);

  

}

很容易理解吧:)接下来给你整个大的

逆序数

#hard 315. 计算右侧小于当前元素的个数 - 力扣(LeetCode) 本题还可以用[[第六章:二分查找与二叉排序树#逆序数|二叉排序树]]来写 已知数组nums,求新数组count,count[i] 代表nums[i]右侧且比nums[i]小的数字 示例

nums = [5,2,6,1],counts = [2,1,1,0]
nums = [6,6,6,1,1,1] counts = [3,3,3,0,0,0]

复杂度O($N^2$) N的规模是 1< N< $10^{5}$ 必然超时

利用归并排序

解决思路如下

class Solution {

public:

    vector<int> countSmaller(vector<int>& nums) {

        std::vector<std::pair<int,int> > vec;

        std::vector<int> count ;

        for(int i =0;i<nums.size();i++){

            vec.push_back(std::make_pair(nums[i],i));//绑定nums和i,来记忆数组一开始的位置

            count.push_back(0);

        }

        merge_sort(vec,count);

        return count;

    }

  

private: 

    void merge_sort_two_vec(std::vector<std::pair<int,int> > &sub_vec1,

    std::vector<std::pair<int,int> >&sub_vec2,

    std::vector<std::pair<int,int> > &vec,

    std:: vector<int>&count){ 

        int i =0;

        int j = 0;

        while(i<sub_vec1.size()&&j<sub_vec2.size()){

            if(sub_vec1[i].first <= sub_vec2[j].first){

                count[sub_vec1[i].second] += j;

                vec.push_back(sub_vec1[i]);

                i++;

            }

            else{

                vec.push_back(sub_vec2[j]);

                j++;

            }

  

        }

        for(;i<sub_vec1.size();i++){

            count[sub_vec1[i].second] += j;

            vec.push_back(sub_vec1[i]);

        }

        for(;j<sub_vec2.size();j++){

            vec.push_back(sub_vec2[j]);

        }

    }

    void merge_sort(std::vector<std::pair<int,int> > &vec,std::vector<int>&count){

        if(vec.size() <2){

            return ;  //子问题足够小直接求解

        }

        int mid = vec.size()/2;

        std::vector<std::pair<int,int> >sub_vec1;

        std::vector<std::pair<int,int> >sub_vec2;

        for(int i=0;i<mid;i++){  //分解并排序,与上一题一样

            sub_vec1.push_back(vec[i]);

        }

        for(int i = mid;i<vec.size();i++){

            sub_vec2.push_back(vec[i]);

        }

        merge_sort(sub_vec1,count);

        merge_sort(sub_vec2,count);

        vec.clear();

        merge_sort_two_vec(sub_vec1,sub_vec2,vec,count);

    }

};

后半部分由于是升序所以他的count一直为0,这也是可以利用j来处理count的原因

累死了,下课!