LeetCode刷题记录--递归

LeetCode刷题记录--递归,第1张

文章目录
  • 1.剑指 Offer 64. 求1+2+…+n
  • 2.1823. 找出游戏的获胜者
  • 3.面试题 08.05. 递归乘法
  • 4.剑指 Offer 62. 圆圈中最后剩下的数字
  • 5.344. 反转字符串
  • 6.反转链表
  • 7.剑指 Offer 06. 从尾到头打印链表
  • 8.486. 预测赢家
  • 9.60. 排列序列

1.剑指 Offer 64. 求1+2+…+n

原题链接

求 1+2+…+n ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
示例 1:
输入: n = 3
输出: 6
示例 2:
输入: n = 9
输出: 45

这道题在不加题目中各种限制的话十分简单(其实加了也十分简单用等差数列的公式即可.),循环遍历即可求出,那么如果在题目的限制下我们应该怎么作呢?首先while,for,switch,else不能使用了也就是说迭代是不可能的了,那么剩下的就只有递归了。
但是如果使用递归的话,递归的终止条件应该怎么做呢?第一个想法是三目运算符,但是也被限制了。这时候就要介绍一下&&的一个特点了,我也不知道如何称呼,但是别人都称之为短路特性那么在此也引用了这个称呼.
观看了下面的代码我们自然而然地就理解了,如果n为0的话n&&(n+=sumNums(n-1))不会被执行,返回0递归结束。这就是利用了&&的这个特点,如果前件为假直接结束当前语句否则判断后件是否为真,也就是执行了我们的递归过程.

class Solution {
public:
    int sumNums(int n) {
        n&&(n+=sumNums(n-1));
        return n;
    }
};
2.1823. 找出游戏的获胜者

原题链接

  共有 n 名小伙伴一起做游戏。小伙伴们围成一圈,按 顺时针顺序 从 1 到 n 编号。确切地说,从第 i 名小伙伴顺时针移动一位会到达第 (i+1) 名小伙伴的位置,其中 1 <= i < n ,从第 n 名小伙伴顺时针移动一位会回到第 1 名小伙伴的位置。
  游戏遵循如下规则:
  从第 1 名小伙伴所在位置 开始 。
  沿着顺时针方向数 k 名小伙伴,计数时需要 包含 起始时的那位小伙伴。逐个绕圈进行计数,一些小伙伴可能会被数过不止一次。
  你数到的最后一名小伙伴需要离开圈子,并视作输掉游戏。
  如果圈子中仍然有不止一名小伙伴,从刚刚输掉的小伙伴的 顺时针下一位 小伙伴 开始,回到步骤 2 继续执行。
  否则,圈子中最后一名小伙伴赢得游戏。
  给你参与游戏的小伙伴总数 n ,和一个整数 k ,返回游戏的获胜者。
示例 1:
输入:n = 5, k = 2
输出:3
示例 2:
输入:n = 6, k = 5
输出:1
解释:小伙伴离开圈子的顺序:5、4、6、2、3 。小伙伴 1 是游戏的获胜者。
提示:
1 <= k <= n <= 500

这就是很经典的约瑟夫环的问题了,这里简单介绍一下详细的可以自行查阅资料:
  所谓的约瑟夫环就是,现在有 n n n个人在玩一个游戏,游戏的规则是从第一个到最后一个人一次报数,每报到 k ( k < = n ) k(k<=n) k(k<=n)就淘汰掉他并从下一个人开始重新报数,如果人数不够就从第一个人开始接着上一人继续报号,直到剩下最后一人为止,这个人也就是这场游戏的获胜者.

那么对于这类问题的解法有很多很多,链表,迭代,队列,递归等等。不过既然这篇文章的标题叫递归,在这里就介绍递归解法了。
首先我们对递归函数的定义是找到给定人数在给定k的情况下的赢家。
  那么首先对于 n n n来说,最后的赢家必然是淘汰掉第 k k k号后剩余玩家的获胜者,现在我们考虑的是剩下 n − 1 n-1 n1人的获胜情况。由于淘汰掉第K号,重新开始报数了,我们不妨把第K+1号作为一号,那么从第 K + 1 K+1 K+1到第 n n n号新的编号就是 1 − n − k 1-n-k 1nk,原来的 1 1 1 k − 1 k-1 k1就变为了 n − k + 1 n-k+1 nk+1剩下的依次类推。
  现在我们是有点晕的,再来梳理一下


如上图所示,假如k是5,那么新的1号就是原先的k+1号,2是k+2号,那么在这个图片的帮助下我们发现了一个规律,现在的编号加上k在不超过 n n n的情况下就是原来的编号,如果超过了 n n n我们直接模上 n n n即可,剩下就是我们的递归了,我会在代码部分进行讲解。

class Solution {
public:
    int findTheWinner(int n, int k) {
        if(n==1) return 1;//如果只剩余一个人就返回1,这是不管在任何编号条件下都成立了,因为我们总是吧他当作编号为1的人
        int winner=findTheWinner(n-1,k)+k;
        //对于每个n,递归去找赢家,返回了获胜者的编号之后我们先将其加k
        return winner%n==0?n:(winner%n);
        //如果winner%n==0,自然就是n号了,如果有余数那就摸上n
    }
};

对于这个过程,看过代码之后可能更是一头雾水了。那么如何来进行理解呢?对于递归函数,在进行学习的时候我们首先接触的大概都是汉诺塔问题,对于那个递归其代码具体执行过程也是很难追究。但是我们不用去细究他的执行过程,只需要知道递归函数带给我们的结果就是在新编号的规则下的获胜者的编号就够了,还原回去是我们下面要考虑的问题。而还原就是我们的 w i n n e r winner winner要做的事情,他接受到的值就是新编号规则下的编号加上 k k k的数值,如果他不是n我们就%n,否则返回n。整个代码的逻辑就是这么简单。要多多理解还原和递归函数的定义。

3.面试题 08.05. 递归乘法

原题链接

递归乘法。 写一个递归函数,不使用 * 运算符, 实现两个正整数的相乘。可以使用加号、减号、位移,但要吝啬一些。
示例1:
输入:A = 1, B = 10
输出:10
示例2:
输入:A = 3, B = 4
输出:12
提示:
保证乘法范围不会溢出

在做这个题之前,我们可以回想一下我们最初接触到乘法的定义就是对于 2 + 2 + 2 + 2 + 2 + 2 + 2 2+2+2+2+2+2+2 2+2+2+2+2+2+2这种情况我们可以写成 2 ∗ 7 2*7 27,也就是7个2相加,那么对于这道题也可以这样做.对于A和B,我们时刻保证A为较小数,然后累加即可。

class Solution {
public:
    int multiply(int A, int B) {
        if(A>B){
            int tmp=A;
            A=B;
            B=tmp;
        }
        return A?B+multiply(A-1,B):0;
    }
};
4.剑指 Offer 62. 圆圈中最后剩下的数字

原题链接

0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
示例 1:
输入: n = 5, m = 3
输出: 3
示例 2:
输入: n = 10, m = 17
输出: 2
限制:
1 <= n <= 10^5
1 <= m <= 10^6

同样是约瑟夫环问题,按照之前的思路处理。

class Solution {
public:
    int lastRemaining(int n, int m) {
        if(n==1){
            return 0;//由于编号是从0开始,只剩一个人的时候当前编号为0
        }
        int last=lastRemaining(n-1,m)+m;
        return last%n;//也不用考虑n的情况直接返回last%n即可
    }
};
5.344. 反转字符串

原题链接

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
示例 1:
输入:s = [“h”,“e”,“l”,“l”,“o”]
输出:[“o”,“l”,“l”,“e”,“h”]
示例 2:
输入:s = [“H”,“a”,“n”,“n”,“a”,“h”]
输出:[“h”,“a”,“n”,“n”,“a”,“H”]

常规的使用迭代即可处理,这里只是联系一下递归

class Solution {
public:

    void reverse_(vector<char>&s,int l,int r){
        if(l>r){
            return ;
        }
        char tmp=s[l];
        s[l++]=s[r];
        s[r--]=tmp;
        reverse_(s,l,r);
    }
    void reverseString(vector<char>& s) {
        reverse_(s,0,s.size()-1);
    }
};
6.反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

反转链表我们在刚学习链表的时候肯定都写过,不管是额外开辟了一个链表还是原地修改还是迭代递归等。这里就详细介绍一下递归过程中反转链表的执行过程.可以在过程中自己想一下过程。

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if(head==nullptr||head->next==nullptr)
        return head;//找到尾结点后一个条件是为了防止链表本身为空
        ListNode* ans=reverseList(head->next);//接收到尾结点也是递归执行的过程
        head->next->next=head;//找到了尾节点之后我们把当前节点的下一个节点指向当前节点
        head->next=nullptr;//当前节点指向空
        return ans; //这里返回的ans一直都是尾结点,也是我们翻转后的头节点
    }
};
7.剑指 Offer 06. 从尾到头打印链表

原题链接

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
示例 1:
输入:head = [1,3,2]
输出:[2,3,1]

这道题的话就不需要我们对链表进行反转了,只需要对其进行遍历然后再回溯的过程中依次加入节点的val即可。

class Solution {
public:
    vector<int> ans;
    void post_order(ListNode* head){
        if(head==nullptr)return ;
        post_order(head->next);
        ans.push_back(head->val);
    }
    vector<int> reversePrint(ListNode* head) {
        ans.clear();
        post_order(head);
        return ans;
    }
};
8.486. 预测赢家

原题链接

给你一个整数数组 nums 。玩家 1 和玩家 2 基于这个数组设计了一个游戏。
玩家 1 和玩家 2 轮流进行自己的回合,玩家 1 先手。开始时,两个玩家的初始分值都是 0 。每一回合,玩家从数组的任意一端取一个数字(即,nums[0] 或 nums[nums.length - 1]),取到的数字将会从数组中移除(数组长度减 1 )。玩家选中的数字将会加到他的得分上。当数组中没有剩余数字可取时,游戏结束。
如果玩家 1 能成为赢家,返回 true 。如果两个玩家得分相等,同样认为玩家 1 是游戏的赢家,也返回 true 。你可以假设每个玩家的玩法都会使他的分数最大化。
示例 1:
输入:nums = [1,5,2]
输出:false
解释:一开始,玩家 1 可以从 1 和 2 中进行选择。
如果他选择 2(或者 1 ),那么玩家 2 可以从 1(或者 2 )和 5 中进行选择。如果玩家 2 选择了 5 ,那么玩家 1 则只剩下 1(或者 2 )可选。
所以,玩家 1 的最终分数为 1 + 2 = 3,而玩家 2 为 5 。
因此,玩家 1 永远不会成为赢家,返回 false 。
示例 2:
输入:nums = [1,5,233,7]
输出:true
解释:玩家 1 一开始选择 1 。然后玩家 2 必须从 5 和 7 中进行选择。无论玩家 2 选择了哪个,玩家 1 都可以选择 233 。
最终,玩家 1(234 分)比玩家 2(12 分)获得更多的分数,所以返回 true,表示玩家 1 可以成为赢家。

也算是一道比较经典的博弈题了,对于这道题我们可以这样想,玩家1获胜的条件是他的得分比玩家2大或者相等也就是 s c o r e 1 > = s c o r e 2 score1>=score2 score1>=score2,过程中玩家均采取最优策略。先来看代码.

class Solution {
public:
    int dfs(int l,int r,int scorea,int scoreb,bool isFirst,vector<int>& nums){
        if(l>r){
            return scorea>=scoreb;
        }
        if(isFirst){
            return dfs(l+1,r,nums[l]+scorea,scoreb,false,nums)||dfs(l,r-1,nums[r]+scorea,scoreb,false,nums);
        }else {
            return dfs(l+1,r,scorea,scoreb+nums[l],true,nums)&&dfs(l,r-1,scorea,scoreb+nums[r],true,nums);
        }
    }
    bool PredictTheWinner(vector<int>& nums) {
        return dfs(0,nums.size()-1,0,0,true,nums);
    }
};

  递归函数的定义是判断玩家1是否为赢家。其参数意义分别为:
l , r l,r l,r还未选择的数的左右边界, s c o r e a , s c o r e b scorea,scoreb scorea,scoreb为a,b二人的分数, i s F i r s t isFirst isFirst b o o l bool bool型来判断A是否先手, n u m s nums nums就是每个元素的得分情况。
  由于双方均采取最优策略首先考虑:
   1 ) 1) 1)如果是玩家1先手的条件下,那么不论是他取左还是取右只要有一个情况下是他获胜那么玩家1必定获胜,而玩家1先手的情况下对 s c o r e a scorea scorea来进行累加并交换出手顺序。
   2 ) 2) 2)如果玩家2先手,那么玩家1想要获胜就要不管玩家2选左边还是右边,玩家1都要获胜这种情况下对scoreb进行累加并交换出手顺序.

这种解法十分利于理解,但是执行效率较低,下面介绍一种在递归中运用dp的方法:

class Solution {
public:
    int dp[25][25][2];//dp数组前两维仍然是从l到r范围内的分数,最后一维是玩家1还是玩家2先手的状态
    //也就是说dp数组统计的是不同状态下玩家1的分数,不再求取玩家2的分数了
    int dfs(vector<int> &nums,int l,int r,bool isFirst){
        if(l>r){
            return 0;
        }
        if(dp[l][r][isFirst]!=-1){
            return dp[l][r][isFirst];//如果已经存在过这种状态直接返回.
        }
        int get=isFirst?1:0;//根据是否是玩家1的选择进行加分
        if(isFirst){//玩家1先手
            return dp[l][r][isFirst]=max(
                nums[l]*get+dfs(nums,l+1,r,!isFirst),//如果选左,去求[l+1,r]的最合理情况并加上左边界分数
                nums[r]*get+dfs(nums,l,r-1,!isFirst)//如果选右,去求[l,r-1]的最合理情况并加上右边界分数,二者取最大
            );
        }else return dp[l][r][isFirst]=min(//玩家2先手
            nums[l]*get+dfs(nums,l+1,r,!isFirst),//如果选左,去求[l+1,r]的最合理情况
            nums[r]*get+dfs(nums,l,r-1,!isFirst)//如果选右,去求[l,r-1]的最合理情况二者取最小
        );
    }
    bool PredictTheWinner(vector<int>& nums) {
        int sum=0;
        for(auto i:nums){
            sum+=i;
        }
        memset(dp,-1,sizeof(dp));
        int score=dfs(nums,0,nums.size()-1,true);
        return score*2>=sum;//如果score*2>=sum,说明a的分数一定大于等于b
    }
};

这种做法的含义就是,我们的dp数组内存储的只是玩家1在不同状况下的分数最合理的分数。如果是A先手,那么我们选在该区间下的分数最大的状况,如果是B先手,我们选择在该区间下分数最小的状况,最后返回 d p [ l ] [ r ] [ i s F i r s t ] dp[l][r][isFirst] dp[l][r][isFirst]即可,不用去管递归内部如何执行的,只需要知道递归函数求取的是我们要求的最合理的情况即可。
这两种方法在时间上分别击败了50%和100%的用户。

9.60. 排列序列

给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列。
按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:
“123”
“132”
“213”
“231”
“312”
“321”
给定 n 和 k,返回第 k 个排列。
示例 1:
输入:n = 3, k = 3
输出:“213”
示例 2:
输入:n = 4, k = 9
输出:“2314”
示例 3:
输入:n = 3, k = 1
输出:“123”
提示:
1 <= n <= 9
1 <= k <= n!

其实这道题,如果知道next_permutation()的存在的话完全就是个水题,当然,先列出全排列然后返回第 k k k个也完全可以,(因为我特意试验过)不过既然吧这道题列出来了就肯定不会这么写的,先看代码.

class Solution {
public:
    string getPermutation(int n, int k) {
        int fac[10];
        vector<int> num;
        fac[0]=1;
        num.push_back(1);
        for(int i=1;i<n;i++){
            fac[i]=fac[i-1]*i;
            num.push_back(i+1);
        }
        k--;//方便计算个数不影响结果
        string ans;
        for(int i=1;i<=n;i++){//从第1位看到第n位
            int m=k/fac[n-i];//得到每一位是数组中的第几个数字
            ans.push_back(num[m]+'0');
            num.erase(num.begin()+m);//删除
            k%=fac[n-i];//得到剩下的数字个数
        }
        return ans;
    }
};

这里就借助实例1来举例了,当 n = 3 n=3 n=3时候,全排列如下:
123 123 123 132 132 132 213 213 213 231 231 231 312 312 312 321 321 321
我们注意到了以1 2 3为首数字的排列个数是 ( n − 1 ) ! (n-1)! (n1)!个这个可以自己列一下4以上的阶乘验证。那么对于第k个排列,我们想要得到它就可以这么做了:
( 1 ) (1) (1)首先得到第一位的数字也就是 k / ( n − 1 ) ! k/(n-1)! k/(n1)!,比如我们现在有一个数组 n u m s = 【 1 , 2 , 3 】 nums=【1,2,3】 nums=1,2,3,k=3. k / ( n − 1 ) ! k/(n-1)! k/(n1)!就是1,得到第k个全排列的第一位为2,于是2被用过了从数组中删除,接下来吧k%=(n-1)!也就是1;
( 2 ) (2) (2)接着去寻找下一位,此时数组为 [ 1 , 3 ] [1,3] [1,3],k/=(num.size()-1)!=1,下一位为3,k再次标记为k%=(num.size()-1)!=1,num删去3;之后的位数同样这样得到.

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/675963.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-19
下一篇 2022-04-19

发表评论

登录后才能评论

评论列表(0条)

保存