【递归刷题】

1. 递归思想

什么是递归?

函数自己调用自己的情况。

为什么会用到递归?

递归的本质:
在解决一个主问题的时候,衍生出了相同的子问题,解决这个子问题的时候,又衍生出了相同的子问题。如此循环。
用同样的方法解决主问题和子问题。

如何理解递归?

在最开始接触递归的时候,相必大家都遭受过递归展开图的毒打。但是在解决递归问题的时候,我们只需要在分析题目的时候,找到相同子问题即可!在实现一个递归代码的时候,千万不要被递归展开图带跑了方向。递归只是一个框架型算法。

所以如何避免实现递归代码的时候陷入展开死胡同?这需要我们从宏观角度看待递归问题

  • 知道但不要在意递归的细节展开图!!!
  • 把递归的函数当成一个黑盒子
  • 相信这个黑盒子一定能完成这个任务:(怎么完成的:先递,递到出口,再归,递归结束,问题解决。)

如何写好一个递归?

  1. 先找到相同的子问题-》函数头的设计
  2. 只关心某一个子问题是如何解决的-》函数体的书写
  3. 避免写死递归:注意一下递归函数的出口即可。递归函数的出口就是子问题不能再衍生子问题的时候。

2. 汉诺塔问题

去力扣闯关
在这里插入图片描述

如何解决这个问题?

先从具体例子入手分析一下:
在这里插入图片描述在这里插入图片描述在这里插入图片描述绿盘子是第N个盘子,红盘子是第N-1个盘子,向上类推

从上面三个例子中抽象出解决汉诺塔问题的办法:
当N=2,N=3时,怎么把N个盘子移动从A柱子移动到C柱子?

  1. 先把前N-1个盘子移动到B柱子上,
  2. 再把第N个盘子移动到C柱子上,
  3. 最后把B柱子上的N-1个盘子再移动到C柱子上

依次类推,当N=4,N=5 … …的时候都是如此。

再仔细看N=3中的第一步:怎么把第N个盘子上面的N-1个盘子从A柱子移动到B柱子上?

  1. 先把前N-2个盘子移到C柱子上
  2. 再把第N-1个盘子移到B柱子上
  3. 最后把C柱子上的前N-2个盘子再移动到B柱子上
    在这里插入图片描述

到这,让我们尝试从例子中抽象出解决汉诺塔问题的方法:已知三个柱子和N个盘子,借助中间柱子,把N个盘子从所在柱子移到目标柱子。

先把第N个盘子上面的的N-1个盘子移到中间柱子,然后把第N个盘子移到目标柱子,最后把中间柱子上的盘子移到目标柱子上。

其中,把N-1个盘子移到目标柱子(此时目标柱子变成了上面问题中的中间柱子,对应的上面问题中的目标柱子,变成了中间柱子)也是同样的:

先把第N-1个盘子上面的的N-2个盘子移到中间柱子,然后把第N-1个盘子移到目标柱子,最后把中间柱子上的盘子移到目标柱子上。所在柱子,目标柱子,中间柱子都是相对的

显然,解决汉诺塔问题的时候,解决主问题时,又衍生出和主问题相同的子问题,所以汉诺塔问题可以用递归思想解决。

实现递归代码:

  1. 相同子问题:把N个盘子,借助中间柱子,从所在柱子移到目标柱子
    在这里插入图片描述

  2. 子问题的解决办法:

以N=3为例:在这里插入图片描述
在这里插入图片描述

  1. 递归函数的出口:
    在这里插入图片描述

题解代码如下:

class Solution {
    public void hanota(List<Integer> A, List<Integer> B, List<Integer> C) {
        dfs(A,B,C,A.size()) ;
    }
    void dfs(List<Integer> A, List<Integer> B, List<Integer> C,int N){
        //A是盘子所在柱子,B是中间柱子,C是目标柱子,N是盘子

        if(N==1){
            C.add(A.remove(A.size()-1));
            return;
        }

        //先把N-1个盘子从A柱子,借助C柱子,移到B柱子
        dfs(A,C,B, N-1);
        //然后把第N个盘子,直接移到C柱子:也就是,把A表中最后一个元素移到C表中
        C.add(A.remove(A.size()-1));
        //最后把N-1个盘子从B柱子,借助A柱子,移到C柱子
        dfs(B,A,C, N-1);
    }
}

3. 合并两个有序单链表

去力扣闯关
在这里插入图片描述合并链表的过程其实就是重新连接已知所有结点的过程。

如何解决这个问题?

先从具体例子分析:
在这里插入图片描述

已知两个升序单链表,A,B,合并A,B,返回新的升序链表C的头结点。
先比较A的头结点中值和B的头结点中值,哪个结点中的值更小,就让这个节点做新的升序链表C的头结点,即返回这个节点,并让这个节点指向C的第二个结点。

如何得到C的第二个结点?
在上图所示的例子中,链表C的头结点是B链表的头结点,且在确认C的第二个节点的时候,B链表的头结点同时更新为了B链表原来的第二个结点,也就是诞生了新的B链表。

所以如何得到C的第二个结点呢?
已知两个升序单链表,A,B(新B),合并A,B,返回新的升序链表C(新C)的头结点。
先比较A的头结点中值和B的头结点中值,哪个结点中的值更小,就让这个节点做新的升序链表C的头结点,即返回这个节点,并让这个节点指向C的第二个结点。

依次类推。
显然,解决合并两个有序单链表问题的时候,解决主问题时,又衍生出和主问题相同的子问题,所以该问题可以用递归思想解决。

实现递归代码

  1. 先找到相同的子问题-》已知两个升序单链表,A,B,合并A,B,返回新的升序链表C的头结点。
    在这里插入图片描述

  2. 只关心某一个子问题是如何解决的-》
    在这里插入图片描述

  3. 避免写死递归:
    在这里插入图片描述

题解代码如下:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        //递归出口
        if(list1 == null){
            return list2;
        }
        if(list2 == null){
            return list1;
        }
        //某一个子问题是如何解决的:
        //先比较A的头结点中值和B的头结点中值,哪个结点中的值更小,
        //就让这个节点做新的升序链表C的头结点,
        //即返回这个节点,并让这个节点指向C的第二个结点。
        if(list2.val<=list1.val){
            list2.next = mergeTwoLists(list1,list2.next);
            return list2;
        }else{
            list1.next = mergeTwoLists(list1.next,list2);
            return list1;
        }
    }
}

4. 就地反转单链表

去力扣闯关
在这里插入图片描述

如何解决这个问题?

先从具体例子入手:
在这里插入图片描述但是,如果我们逆置前两个结点的时候,其中第二个结点是已经逆置完成的单链表的头结点的话,是不是就可以忽略逆置完前两个结点,找不到第三个结点的问题了呢?
因此,直接逆置单链表的前两个结点即可。
至于第二个结点如何变成,已经逆置完成的单链表的头结点,这不就是原问题衍生出来的相同的子问题吗?此时要逆置链表的头结点变成了原链表的第二个结点。
依次类推。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

显然,解决逆置一个单链表问题的时候,解决主问题时,又衍生出和主问题相同的子问题,所以该问题可以用递归思想解决。

实现递归代码

  1. 先找到相同的子问题-》已知单链表的头结点,逆置单链表后,再返回逆置好的单链表的头结点
    在这里插入图片描述

  2. 只关心某一个子问题是如何解决的-》
    在这里插入图片描述

  3. 避免写死递归:注意一下递归函数的出口即可。递归函数的出口就是子问题不能再衍生子问题的时候:即链表中只有一个结点或没有结点的时候,不需要逆置。
    在这里插入图片描述

题解代码:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        /*
        //空链表
        if(head == null){
            return head;
        }
        //只有一个结点,不用逆置
        if(head!=null && head.next == null){
            return head;
        }
        */
        //把空链表和只有一个结点的情况合并
        if(head == null || head.next == null){
            return head;
        }
        
        //先拿到头结点后面的节点,该节点是已经逆置好的单链表的头结点
        ListNode newHead = reverseList(head.next);
        //逆置头结点和后面的节点
        head.next.next = head;//后面结点的地址域指向head,
        head.next = null;//head的地址域设为null。
        //最后返回逆置好的单链表。
        return newHead;//要注意的是:逆置好的单链表的头结点不是原来的头结点
    }
}

5.两两交换链表中的节点

去力扣闯关
在这里插入图片描述
交换链表中的节点,意味着要修改链表的地址域。而不是数值域。

如何解决这个问题

从具体例子入手:交换链表的前两个结点,然后返回交换后链表的头结点。
在这里插入图片描述
如何确定第四个结点:同样的方法,交换第三个结点和第四个结点,返回第四个结点。
在这里插入图片描述
显然,解决两两交换链表中的结点问题的时候,解决主问题时,又衍生出和主问题相同的子问题,所以该问题可以用递归思想解决。

递归实现代码

  1. 先找到相同的子问题-》已知一个单链表,链表中的两个结点为一组,进行交换操作,然后返回交换后新链表的头结点。
    在这里插入图片描述

  2. 只关心某一个子问题是如何解决的-》第二个结点指向头结点,头结点指向第四个结点,(这里第四个结点是交换后的新链表的头结点),返回第二个节点。
    在这里插入图片描述

  3. 避免写死递归:
    在这里插入图片描述题解代码:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode swapPairs(ListNode head) {
        //递归出口
        //当链表中只有一个结点,或空链表时
        if(head == null||head.next == null){
            return head;
        }

        //子问题的解决办法
        ListNode tmp = swapPairs(head.next.next);//确定第四个结点
        ListNode newHead = head.next; //提前保存第二个结点
        head.next.next = head;
        head.next = tmp;
        return newHead;
    }
}

6.快速幂

快速幂算法:即快速的求出x的n次方是多少的算法。

去力扣闯关
在这里插入图片描述

如何解决这个问题:

既然这个问题是快速幂,那暴力解法(时间复杂度O(n))就不多聊了。。。

先从具体例子入手,例如:如何快速计算316的值?(当n是偶数时)
在这里插入图片描述
316=38 * 38,那如何计算 38呢? 38 = 34 * 34,依次类推。

再看一个例子,例如:如何计算 321的值?(当n是奇数时)
在这里插入图片描述
321 = 310 * 310 * 3,那如何计算 310的值,依次类推。

显然,解决主问题时,又衍生出和主问题相同的子问题,所以该问题可以用递归思想解决。

递归实现代码:

  1. 先找到相同的子问题-》计算x的n次幂的值,n是-2^31^ <= n <= 2^31^-1之间的整数。
    在这里插入图片描述

  2. 只关心某一个子问题是如何解决的-》如何求x的n次幂:

    • 当n是正数时,如果n是奇数,x的n次幂等于x的n/2次幂乘以x的n/2次幂;如果n是偶数,x的n次幂等于x的n/2次幂乘以x的n/2次幂乘以x。

    • 当n是负数时,如果n是奇数,x的n次幂等于1 / (x的-n/2次幂乘以x的-n/2次幂);如果n是偶数,x的n次幂等于1 / (x的-n/2次幂乘以x的-n/2次幂乘以x)。

    在这里插入图片描述
    在这里插入图片描述

这里还有一个细节问题:力扣中给的函数接口里,n是double类型的,double n 存储的整数范围是 -2^31^ <= n <= 2^31^-1。也就是当n = -231,要计算x的 -231次幂的值时,计算机实际上计算的是1 / (x的231次幂)的值。但是,double类型的n根本无法存储231 这个数,所以我们需要用long作为n的数据类型。

  1. 避免写死递归:递归函数的出口就是子问题不能再衍生子问题的时候,当n等于0时,x的0次方等于1。
    在这里插入图片描述

题解代码:

class Solution {
    public  double  myPow(double  x, long  n) {
        if(n==0){
            return 1;//x的0次方等于1
        }
        if(n<0){
            double  tmp = myPow(x,-n/2);
            return (n%2 == 0) ? (1/(tmp*tmp)):(1/(tmp*tmp*x));
        }else{
            double  tmp = myPow(x,n/2);
            return n%2 == 0 ? tmp*tmp : tmp*tmp*x;
        }
    }
}


本篇到这就结束了~

刷算法的日子真的是痛苦极了,但是找工作的时候第一关就是笔试,不刷算法又没办法哈哈~
哦哦,还要长期坚持刷,因为我深知以我的脑子速成不了这玩意呜呜~

接下来打算再刷刷深度优先遍历方面的题。

在这里插入图片描述
我是码代码的西西弗,一起冲鸭~

  • 9
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值