2023-03-13 递归详解

36 篇文章 0 订阅

1.递归基础和递归的宏观语意

本质上,将原来的问题,转化为更小的同一问题

举例

递归举例之数组求和

代码

public class ArrSum {
    private int res = 0;

    public int sum(int[] arr) {
        return sum(arr, 0);
    }

    /**
     * 计算从arr[i]到arr[n-1]的和
     *
     * @param arr 数组
     * @param i   当前遍历到的索引
     * @return arr[i]到arr[n-1]的和
     */
    private int sum(int[] arr, int i) {
        if (i == arr.length) {
            // 遍历完数组,返回0
            return 0;
        }
        return arr[i] + sum(arr, i + 1);
    }

    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        System.out.println(new ArrSum().sum(arr));
    }
}

递归解析

递归是a->a->a即相同函数的互相调用,实际与a->b->c即不同函数之间的调用没啥区别不断取内层调用函数的返回值给上一层即可我们只要想清楚内层函数的逻辑即可~~

  • 1.求解最基本问题(也即递归的退出条件)
  • 2.把原问题转换为更小的问题(也即递归的具体逻辑)
    递归代码解析

递归就像老师说的那样,将原有问题变成一个更小的问题,然后在更小的问题基础上再变成更更小的问题,一直到不能再变小为止,当不能再变更小的时候,这个时候这个问题的答案就变得显而易见了,然后再通过这个答案反其道而行之,以这个答案为基础向上推导出原问题的答案。

2.链表的天然递归结构性质

链表递归的终止条件:访问到为NULL的节点

直到最后可以理解为空本身也是一个链表。null就是那个最基础的、最平凡的链表,即递归退出条件
链表递归的终止条件

递归解决LeetCode203号问题:删除链表中等于指定值的所有元素

链表删除(递归写法):

  • 1.注意宏观语义
  • 2.删除掉一个节点后得到小问题的解
  • 3.考虑怎么用小问题的解构建原问题的解
  • 4.分情况讨论
class Solution {
    public ListNode removeElements(ListNode head, int val) {
        if (head == null) {
            return null;
        }
        head.next = removeElements(head.next, val);
        // head节点要删除就直接跳过head节点,否则就返回原来的
        return head.val == val ? head.next : head;
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 6, 3, 4, 5, 6};
        ListNode head = new ListNode(nums);
        System.out.println(head);
        ListNode result = (new Solution()).removeElements(head, 6);
        System.out.println(result);
    }
}

3. 递归的微观解读

递归的调用和普通的子函数链式调用没区别,只不过名字是自己而已。

递归调用与普通调用的对比
递归调用与普通调用

递归相当于隐式的系统调用栈
普通函数调用

递归过程详细分析举例1:数组求和

public class ArrSum {
    private int res = 0;

    public int sum(int[] arr) {
        return sum(arr, 0);
    }

    /**
     * 计算从arr[i]到arr[n-1]的和
     *
     * @param arr 数组
     * @param i   当前遍历到的索引
     * @return arr[i]到arr[n-1]的和
     */
    private int sum(int[] arr, int i) {
        if (i == arr.length) {
            // 遍历完数组,返回0
            return 0;
        }
        return arr[i] + sum(arr, i + 1);
    }

    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 4, 5};
        System.out.println(new ArrSum().sum(arr));
    }
}

本题中递归函数为int sum(int[] arr, int i),arr.length=5

  • 第1层递归:i=0<5,arr[i]=arr[0]=1,进入第2层递归i+1=1
  • 第2层递归:i=1<5,arr[i]=arr[1]=2,进入第3层递归i+1=2
  • 第3层递归:i=2<5,arr[i]=arr[2]=3,进入第4层递归i+1=3
  • 第4层递归:i=3<5,arr[i]=arr[3]=4,进入第5层递归i+1=4
  • 第5层递归:i=4<5,arr[i]=arr[4]=5,进入第6层递归i+1=5
  • 第6层递归:i=5=5,把0返回到上一层递归
  • 第5层递归:把arr[4]+0返回到上一层递归
  • 第4层递归:把arr[3]+arr[4]+0返回到上一层递归
  • 第3层递归:把arr[2]+arr[3]+arr[4]+0返回到上一层递归
  • 第2层递归:把arr[1]+arr[2]+arr[3]+arr[4]+0返回到上一层递归
  • 第1层递归:把arr[0]+arr[1]+arr[2]+arr[3]+arr[4]+0返回到上一层递归
  • 没有上一层递归了,函数执行完毕,把求和结果返回即为数组的值

递归过程详细分析举例2:删除链表中等于指定值的所有元素

class Solution {
    public ListNode removeElements(ListNode head, int val) {
        if (head == null) {
            return null;
        }
        head.next = removeElements(head.next, val);
        // head节点要删除就直接跳过head节点,否则就返回原来的
        return head.val == val ? head.next : head;
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 6, 3, 4, 5, 6};
        ListNode head = new ListNode(nums);
        System.out.println(head);
        ListNode result = (new Solution()).removeElements(head, 6);
        System.out.println(result);
    }
}

本题中初始链表结构为1->2->6->3->4->5->6->NULL,递归函数为ListNode removeElements(ListNode head, int val),我们可以认为每个链表的最后一个节点都是NULL,方便作为递归的终止条件

  • 第1层递归:head.val=1,head->next=2->6->3->4->5->6->NULL,head=1-2->6->3->4->5->6->NULL!=null,所以进入下一层递归
  • 第2层递归:head.val=2,head->next=6->3->4->5->6->NULL,head=2->6->3->4->5->6->NULL!=null,所以进入下一层递归
  • 第3层递归:head.val=6,head->next=3->4->5->6->NULL,head=6->3->4->5->6->NULL!=null,所以进入下一层递归
  • 第4层递归:head.val=3,head->next=4->5->6->NULL,head=3->4->5->6->NULL!=null,所以进入下一层递归
  • 第5层递归:head.val=4,head->next=5->6->NULL,head=4->5->6->NULL!=null,所以进入下一层递归
  • 第6层递归:head.val=5,head->next=6->NULL,head=5->6->NULL!=null,所以进入下一层递归
  • 第7层递归:head.val=6,head->next=NULL,head=6->NULL!=null,所以进入下一层递归
  • 第8层递归:head=null,满足递归回退条件条件,把NULL返回给上一层
  • 第7层递归:赋值head.next=null,head=6->NULL,head.val=6为真,所以递归返回head.next即NULL给上一层递归
  • 第6层递归:赋值head.next=null,head变了,head=5->NULL,head.val=5!=6,所以递归返回head即5->NULL给上一层递归
  • 第5层递归:赋值head.next=5->NULL,head变了,head=4->5->NULL,head.val=4!=6,所以递归返回head即4->5->NULL给上一层递归
  • 第4层递归:赋值head.next=4->5->NULL,head变了,head=3->4->5->NULL,head.val=4!=6,所以递归返回head即3->4->5->NULL给上一层递归
  • 第3层递归:赋值head.next=3->4->5->NULL,head变了,head=6->3->4->5->NULL,head.val=6=6,所以递归返回head.next即3->4->5->NULL给上一层递归
  • 第2层递归:赋值head.next=3->4->5->NULL,head变了,head=2->3->4->5->NULL,head.val=2!=6,所以递归返回head即2->3->4->5->NULL给上一层递归
  • 第1层递归:赋值head.next=2->3->4->5->NULL,head变了,head=1->2->3->4->5->NULL,head.val=1!=6,所以递归返回head即1->2->3->4->5->NULL给上一层递归
  • 已经没有上一层递归了,所以整个递归调用过程完毕,取出值为6的节点后的链表为1->2->3->4->5->NULL

4. 递归调试

使用IDE来单步追踪

IDEA调试递归函数

通过depth参数打印–来显示递归深度,方便看递归过程。在递归进入新层递归退出当前层的时候都打印下深度字符串

可以看到下面的分析过程和我们手动分析地完全一样,代码地址: https://gitee.com/lsgwr/algorithms/blob/master/Part2Basic/src/main/java/Chapter05Recursion/Section6RecursionDebug/SolutionDebug.java

/***********************************************************
 * @Description : 虚拟头结点可以大大简化代码
 * @author      : 梁山广(Laing Shan Guang)
 * @date        : 2019/12/29 14:18
 * @email       : liangshanguang2@gmail.com
 ***********************************************************/
package Chapter05Recursion.Section4LeetCode203Recursion;

import Chapter05Recursion.Section1To2LeetCode203.ListNode;

class SolutionDebug {
    /**
     * 删除链表中值为val的所有元素
     *
     * @param head  链表的头结点
     * @param val   要删除的节点的值
     * @param depth 递归的深度
     * @return
     */
    public ListNode removeElements(ListNode head, int val, int depth) {
        // 下面两行是递归进入新层
        System.out.print(generateRecursionDepthStr(depth));
        System.out.println("第" + depth + "层递归:head=" + head + ",要找的值为节点值为:" + val);
        if (head == null) {
            return null;
        }
        head.next = removeElements(head.next, val, depth + 1);
        // 下面两行是递归退出当前层
        System.out.print(generateRecursionDepthStr(depth));
        System.out.println("第" + depth + "层递归:head=" + head + ",要找的值为节点值为:" + val);
        // head节点要删除就直接跳过head节点,否则就返回原来的
        return head.val == val ? head.next : head;
    }

    /**
     * 生成递归深度的字符串
     *
     * @param depth 递归层数
     * @return depth*2个"--"
     */
    private String generateRecursionDepthStr(int depth) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < depth; i++) {
            sb.append("--");
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        int[] nums = {1, 2, 6, 3, 4, 5, 6};
        ListNode head = new ListNode(nums);
        ListNode result = (new SolutionDebug()).removeElements(head, 6, 1);
        System.out.println();
        System.out.println("递归结束,结果为::" + result);
    }
}
/**
 * --第1层递归:head=1->2->6->3->4->5->6->NULL,要找的值为节点值为:6
 * ----第2层递归:head=2->6->3->4->5->6->NULL,要找的值为节点值为:6
 * ------第3层递归:head=6->3->4->5->6->NULL,要找的值为节点值为:6
 * --------第4层递归:head=3->4->5->6->NULL,要找的值为节点值为:6
 * ----------第5层递归:head=4->5->6->NULL,要找的值为节点值为:6
 * ------------第6层递归:head=5->6->NULL,要找的值为节点值为:6
 * --------------第7层递归:head=6->NULL,要找的值为节点值为:6
 * ----------------第8层递归:head=null,要找的值为节点值为:6
 * --------------第7层递归:head=6->NULL,要找的值为节点值为:6
 * ------------第6层递归:head=5->NULL,要找的值为节点值为:6
 * ----------第5层递归:head=4->5->NULL,要找的值为节点值为:6
 * --------第4层递归:head=3->4->5->NULL,要找的值为节点值为:6
 * ------第3层递归:head=6->3->4->5->NULL,要找的值为节点值为:6
 * ----第2层递归:head=2->3->4->5->NULL,要找的值为节点值为:6
 * --第1层递归:head=1->2->3->4->5->NULL,要找的值为节点值为:6
 *
 * 递归结束,结果为::1->2->3->4->5->NULL
 */

5. 更多和链表相关的问题

  • 近乎所有和链表相关的操作,都可以使用递归的形式完成
  • 可以自己把链表的增删改查改为递归实现,参考实现:https://gitee.com/lsgwr/algorithms/blob/master/Part2Basic/src/main/java/Chapter05Recursion/Section7MoreAboutLinkedList/LinkedListRecursion.java
  • LeetCode中链表相关的问题:https://leetcode-cn.com/tag/linked-list/
  • 玩转算法面试第5章 在链表中穿针引线:https://coding.imooc.com/learn/list/82.html 也看下,相关代码:https://gitee.com/lsgwr/algorithms/blob/master/Part3Practise/第05章_链表相关.md
  • 斯坦福大学的关于链表的一个很好的文档:https://gitee.com/lsgwr/algorithms/blob/master/Part2Basic/src/main/java/Chapter05Recursion/Section7MoreAboutLinkedList/LinkedListProblems.pdf
    刘老师关于里面问题的解析:https://mp.weixin.qq.com/s?__biz=MzU4NTIxODYwMQ==&mid=2247483779&idx=1&sn=555eeab210e54dc420f2b0cb106a2779&chksm=fd8caec5cafb27d3e5ecf692911883ccfc1abfd125a1268b24ae57860dc9e3f44ac49576010b&token=842435376&lang=zh_CN#rd)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

空無一悟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值