算法038(必备)——常见经典递归过程解析

一、字符串的全部子序列

描述

给定一个字符串s,长度为n,求s的所有子序列

1.子序列: 指一个字符串删掉部分字符(也可以不删)形成的字符串,可以是不连续的,比如"abcde"的子序列可以有"ace","ad"等等

2.将所有的子序列的结果返回为一个字符串数组

3.字符串里面可能有重复字符,但是返回的子序列不能有重复的子序列,比如"aab"的子序列只有"","a","aa","aab","ab","b",不能存在2个相同的"ab"

4.返回字符串数组里面的顺序可以不唯一

数据范围:

0<=s.length<=160<=s.length<=16

要求:时间复杂度为O(2n)O(2n)

思路:

递归,大体流程:

basecase:

  • 遍历完整个string,将path加入到答案中

recur:

  • 选择不将当前字符加入到path中
  • 选择将当前字符加入到path中,进入递归后消除影响(path.pop_back)

代码:

#include <string>
#include <unordered_set>
#include <vector>
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param s string字符串 
     * @return string字符串vector
     */

    unordered_set<string> set;

    void f(string s,int i,string& path){
        int len=s.size();
        if(i==len){
            set.insert(path);
            return ;
        }
        f(s,i+1,path);
        path+=s[i];
        f(s,i+1,path);
        path.pop_back();
    }

    vector<string> generatePermutation(string s) {
        // write code here
        set.clear();
        string path;
        f(s,0,path);
        vector<string> vec(set.begin(),set.end());
        return vec;
    }



};

优化思路:

优化path,用char*模拟string。大体流程:

basecase:

  • 遍历完整个string,将范围内的path加入到答案中

recur:

  • 选择不将当前字符加入到path中
  • 选择将当前字符加入到path中,进入递归后消除影响(利用_size覆盖掉已经插入的)

优化代码:

#include <cstdio>
#include <string>
#include <unordered_map>
#include <vector>
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param s string字符串 
     * @return string字符串vector
     */

    unordered_set<string> set;

    void f(string s,int i,char* path,int _size){
        int len=s.size();
        if(i==len){
            string str(path,path+_size);
            set.insert(str);
            return ;
        }  
        f(s,i+1,path,_size);
        path[_size]=s[i];
        f(s,i+1,path,_size+1); 
    }
    vector<string> generatePermutation(string s) {
        // write code here
        set.clear();
        char path[17]={0};
        f(s,0,path,0);
        vector<string> vec(set.begin(),set.end());
        return vec;
    }

};

二、子集 II

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 

子集

(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

思路:

分析该题与第一题的区别,第一题字符间的相对顺序不能改变,而该题中子集内部数字的相对顺序可以改变,所以通过排序nums数组可以达到提升效率的效果。

如何利用有序性?

  • 可以根据加入多少个相同的元素向下递归,而不是每次根据是否选定该元素向下递归(假设有m组相同元素构成nums,各组个数为x1,x2……xm(即x1+x2……+xn),那么总时间接近x1*x2……*xm;而与2^N(2的x1次方*2的x2次方……*2的xm次方)相比,前者时间更短)
  • 替代unordered_set,根据加入相同元素的个数可以区分不同的path,不再需要unordered_set

大体流程:

先排序nums后利用递归将答案找出

basecase:

  • 若遍历完数组,将path加入到ret中

recur:

  • 确定相同元素的个数,再根据不同个数向下递归

代码:

class Solution {
public:
    vector<vector<int>>ret;
    void f(vector<int>& nums,int i,vector<int>&path){
        int n=nums.size();
        if(i>=n){
            ret.push_back(path);
            return;
        }
        int cnt=1,k=i+1;
        while(k<n&&nums[k]==nums[i]){++k;++cnt;}
        f(nums,i+cnt,path);
        for(k=1;k<=cnt;++k){
            path.push_back(nums[i]);
            f(nums,i+cnt,path);
        }
        path.erase(path.end()-cnt,path.end());
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        ret.clear();
        sort(nums.begin(),nums.end());
        vector<int> path;
        f(nums,0,path);
        return ret;
    }
};

三、全排列

给定一个不含重复数字的整数数组 nums ,返回其 所有可能的全排列 。可以 按任意顺序 返回答案。

思路:

依照全排列的概念:取n个不同的元素,按照一定顺序排列的全部排列情况叫做全排列

那么大体流程为:

basecase:

  • 遍历完数组后,将排列情况添加到答案中

recur:

  • 循环遍历该pos后的全部元素:取元素与当前位置交换,在添加到path后对下一个pos进行recur,最后复原交换

代码:

class Solution {
public:
    vector<vector<int>> ret;
    void f(vector<int>& nums, int i, vector<int>& path) {
        int len = nums.size();
        if (i == len) {
            ret.push_back(path);
            return;
        }
        path.push_back(nums[i]);
        f(nums, i + 1, path);
        path.pop_back();
        for (int j = i+1; j < len; ++j) {
            swap(nums[i],nums[j]);
            path.push_back(nums[i]);
            f(nums, i + 1, path);
            swap(nums[i],nums[j]);
            path.pop_back();
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        vector<int> path;
        ret.clear();
        f(nums,0,path);
        return ret;
    }
};

四、全排列 II 

给定一个可包含重复数字的整数集合 nums ,按任意顺序 返回它所有不重复的全排列。

思路:

分析与题目三的区别,该题有重复的数字,如果按照前面的解法,那将有排列重复。那么怎么避免重复?对相同的数字只走一次,即在循环遍历过程中记录遇到过的元素,再次遇到时跳过

大体流程与题目三类似

代码:

class Solution {
public:
    vector<vector<int>> ret;
    void f(vector<int>& nums, int i, vector<int>& path) {
        int len = nums.size();
        if (i == len) {
            ret.push_back(path);
            return;
        }
        vector<int> cnts(21,1);
        //unordered_set<int>set;set.insert(nums[i]);
        cnts[nums[i]+10]=0;
        path.push_back(nums[i]);
        f(nums, i + 1, path);
        path.pop_back();
        for (int j = i + 1; j < len; ++j) {
            //if(set.contains(nums[j]))continue;
            if(!cnts[nums[j]+10])continue;
            //set.insert(nums[j]);
            cnts[nums[j]+10]=0;
            swap(nums[i], nums[j]);
            path.push_back(nums[i]);
            f(nums, i + 1, path);
            swap(nums[i], nums[j]);
            path.pop_back();
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<int> path;
        ret.clear();
        f(nums, 0, path);
        return ret;
    }
};

五、用递归函数和栈操作逆序栈

描述

一个栈依次压入1,2,3,4,5那么从栈顶到栈底分别为5,4,3,2,1。将这个栈转置后,从栈顶到栈底为1,2,3,4,5,也就是实现了栈中元素的逆序,请设计一个算法实现逆序栈的操作,但是只能用递归函数来实现,而不能用另外的数据结构。

给定一个栈Stack以及栈的大小top,请返回逆序后的栈。

思路:

双层递归:

递归1:从栈中获取底部元素,维持上面的元素不变

  • basecase:若栈的大小为1,弹出并返回底部元素
  • recur:获取顶部元素,在获取底部元素(recur1)后,将顶部元素再次插入

递归2:将获取的底部元素,依次插入到栈中,使最先获取的底部元素最后插入

  • basecase:若栈为空,返回
  • recur:获取底部元素(recur1),在将其之后的全部底部元素插入(recur2)后,插入底部元素

代码(与题目规范不一致):

#include <iostream>
using namespace std;
#include <stack>

int get_bottom(stack<int>& stk) {
    if (stk.size() == 1) {
        int tmp = stk.top();
        stk.pop();
        return tmp;
    }
    int t1 = stk.top();
    stk.pop();
    int ret = get_bottom(stk);
    stk.push(t1);
    return ret;
}

void reverse(stack<int>& stk) {
    if(stk.empty())return;
    int n=get_bottom(stk);
    reverse(stk);
    stk.push(n);
}
int main() {
    stack<int>stk;
    int x;
    while (cin >> x) {
        stk.push(x);
    }
    reverse(stk);
    while (stk.size()) {
        cout << stk.top() << endl;
        stk.pop();
    }
    return 0;
}

六、用递归排序一个栈

思路:

升序,双层递归:

递归1:从栈中获取最小元素,维持其他元素不变

  • basecase:若栈的大小为1,弹出并返回底部元素
  • recur:弹出顶部元素,在获取最小元素(recur1)后,将顶部元素再次插入

递归2:将获取的最小元素,依次插入到栈中,使最先获取的最小元素最后插入

  • basecase:若栈为空,返回
  • recur:获取最小元素(recur1),在将其之后的全部最小元素插入(recur2)后,插入最小元素

代码:

#include <iostream>
using namespace std;
#include <stack> 

int get_min(stack<int>& stk,int& m,int& cnt) {
     if (stk.size() == 0) {
         return m;
     }
     int tmp = stk.top();
     stk.pop();
     m = min(tmp, m);
     m = min(m, get_min(stk, m, cnt));
     if (tmp == m){
         ++cnt;
         if (cnt > 1)stk.push(tmp);
     }
     else stk.push(tmp);
     return m;
 }
 void sort_stack(stack<int>& stk) {
     if (stk.size() == 0) {
         return;
     }
     int cnt = 0;
     int m = INT_MAX;
     m = get_min(stk, m, cnt);
     sort_stack(stk);
     stk.push(m);
}

int main() {
    stack<int>stk;
    stk.push(3);
    stk.push(2);
    stk.push(1);
    stk.push(4);
    stk.push(2);
    stk.push(1);
    stk.push(5);
    sort_stack(stk);
    while (stk.size()) {
        cout << stk.top() << endl;
        stk.pop();
    }
    return 0;
}

简化思路:

将代码简单化,递归的功能分给多个递归完成

降序,多层递归

确定stack的深度dep

递归2:

  • 向下递归dep层找到最小的元素并返回

递归3:

  • 向下递归dep层找到最小的元素个数并返回

递归4:

  • 向下递归dep层将全部的最小元素压入栈中,其他元素维持相对顺序不变

大体流程:

  • 根据确定的dep,循环dep--:递归2,3,4

简化代码:

 int get_min(stack<int>& stk,int m, int dep) {
     if (dep == 1) {
         m = min(m, stk.top());
         return m;
     }
     int top = stk.top();
     stk.pop();
     m = min(m, top);
     int tmp = get_min(stk, m, dep - 1);
     m = min(m,tmp);
     stk.push(top);
     return m;
 }
 int get_cnt(stack<int>& stk, int m, int dep) {
     if (dep == 1) {     
         return m == stk.top();
     }
     int cnt = 0;
     int top = stk.top();
     stk.pop();
     if (top == m)++cnt;
     cnt += get_cnt(stk, m, dep - 1);
     stk.push(top);
     return cnt;

 }
 void set_mins(stack<int>& stk, int m, int cnt,int dep) {
     if (dep == 0) {
         while (cnt--) {
             stk.push(m);
         }
         return;
     }
     int top = stk.top();
     stk.pop();
     set_mins(stk, m, cnt, dep - 1);
     if (top != m)stk.push(top);
 }
 void sort_stack(stack<int>& stk) {
     if (stk.size() == 0|| stk.size() == 1) {
         return;
     }
     int dep = stk.size();
     int m, cnt;
     while (dep) {
         m=get_min(stk,INT_MAX,dep);
         cnt = get_cnt(stk, m, dep);
         set_mins(stk, m, cnt, dep);
         dep -= cnt;
     }
}


int main() {
    stack<int>stk;
    stk.push(3);
    stk.push(2);
    stk.push(1);
    stk.push(4);
    stk.push(2);
    stk.push(1);
    stk.push(5);
    sort_stack(stk);
    while (stk.size()) {
        cout << stk.top() << endl;
        stk.pop();
    }
    return 0;
}

七、汉诺塔

在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制:
(1) 每次只能移动一个盘子;
(2) 盘子只能从柱子顶端滑出移到下一根柱子;
(3) 盘子只能叠在比它大的盘子上。

请编写程序,用栈将所有盘子从第一根柱子移到最后一根柱子。

你需要原地修改栈。

思路:

递归函数:将位于A上的n个盘子移到C去

大体流程:

basecase:

  • 当n==1时,将位于A上的n个盘子移到C去

recur:

  • 将位于A上的n-1个盘子移到B去(recur)
  • 将位于A上的最后一个盘子移到C去
  • 将位于B上的n-1个盘子移到C去(recur)

代码:

class Solution {
public:
    void remove(vector<int>& A, vector<int>& B, vector<int>& C,int n){
        if(n==1){
            C.push_back(A.back());
            A.pop_back();
            return;
        }
        remove(A,C,B,n-1);
        C.push_back(A.back());
        A.pop_back();
        remove(B,A,C,n-1);
    }
    void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {
        remove(A,B,C,A.size());
    }
};

打印思路:

与之前的类似,只是将插入和删除行为换成了打印

打印代码:

 void print_Hanoi(int n, char from[], char to[], char other[]) {
     if (n == 1) {
         printf("将第%d个汉诺塔从%s移到%s\n", n, from, to);
         return;
     }
     print_Hanoi(n - 1, from, other, to);
     printf("将第%d个汉诺塔从%s移到%s\n", n, from, to);
     print_Hanoi(n - 1, other, to, from);
 }

  • 24
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值