• 不知道怎样用递归?按步骤来!


    「递归」这个词语我们经常在很多地方看到,在很多地方用到。但是初学递归时可能会有些难以理解。本文从一些易懂、常见的例子中介绍一下「递归」。

    当我们看到「递归」时,不要把它看成一个词语,而是分开看成两个字「递」和「归」。

    举一个生活中的例子

    有几个人在柜台前排队,现在甲想知道他排到第几个了,所以他会问排在他前面的乙是第几个,然后加1即可。

    但是乙也不知道他是第几个,所以乙会问排在他前面的丙是第几个,然后加1即可。

    这样一直向前问……

    直到问到戊了,此时戊就站在柜台前,所以戊知道他是第1个,然后回答丁。

    丁知道他前面的人是第1个,那么丁就知道他是第2个了,然后回答丙。

    这样一直向后回答……

    甲知道了他前面的人是第4个,那么甲就知道他是第5个了。

    在这里插入图片描述
    在这个例子中,大问题是「甲想知道他是第几个人」,但是这条队伍排的太长了一眼望不到头,或者甲忘记带眼镜了看不清。所以甲不能一下解决这个大问题,那么他只能问排在他前面的乙是第几个,这样就把这个大问题分解成一个小问题:「乙想知道他是第几个人」,当甲解决了小问题,那么大问题也就迎刃而解了。

    那么乙是第几个人呢?这就不是甲操心的事了,因为甲也不知道乙是第几个人。不幸的是,乙也不知道他自己是第几个人,并且乙也懒得数,所以乙也直接问他前面的丙是第几个。那个小问题又被分解成更小的问题:「丙想知道他是第几个人」。

    就这样你问我,我再问他,一直问下去,问题也变得越来越小。虽然越来越小,但是问题的本质并没有变:都是「某人想知道他自己是第几个人」。

    当问题来到戊这里,问题被分解到足够小足够简单了:「戊站在柜台前,戊想知道他是第几个人」,戊不用数,也不用问其他人就知道自己是第1个人。为什么?因为他站在柜台前,也就是说,当他看到柜台时,就知道自己是第1个人了。

    然后开始不断向后回答,最后甲知道自己是第5个人。

    总结一下该例的几个要点:

    1. 后面的人在询问前面的人(人在问人)
    2. 每个人在做同样的动作
    3. 问题在不断变小(简单),但是不论多小,都与最初的大问题一样,目的不变
    4. 问题没有被无限地问下去,到柜台前,最简单的问题被解决
    5. 最简单的问题被解决后,开始向后解决问题,直到解决最初的大问题

    将这个几个要点对应到具体的编程中就是:

    1. 方法自己调用自己
    2. 有重复的逻辑
    3. 问题在不断变小(简单),但是目的不变
    4. 方法没有无限的自己调用自己,当问题变为最简时(满足终止条件)停止调用。
    5. 执行完终止条件(if语句)后,开始返回

    根据以上要点得出了一个普遍流程:(这里先写出来,详细解释在文末总结中)

    第一步:找到「大问题」是什么?

    第二步:找到「最简单的问题」是什么?满足最简单问题时应该做什么?

    第三步:找到「重复逻辑」是什么?

    第四步:自己调用自己

    第五步:返回


    下面看几个简单的例子:

    例1:给定数字n,打印数字从n至1

    第一步:找到大问题是什么?

    打印数字从n至1

    public static void print(int n) {
    }
    

    第二步:找到最简单的问题是什么?满足最简单问题时应该做什么?

    当数字为0时,不需要打印,所以「打印数字0」是最简单的问题,满足时停止打印

    //打印数字从n至1
    public static void print(int n) {
        if (n == 0)//最简单问题
            return;
    }
    

    第三步:找到重复逻辑是什么?

    打印每个数字

    //打印数字从n至1
    public static void print(int n) {
        if (n == 0)//最简单问题
            return;
        System.out.println(n);//重复逻辑
    }
    

    第四步:自己调用自己

    //打印数字从n至1
    public static void print(int n) {
        if (n == 0)//最简单问题
            return;
        System.out.println(n);//重复逻辑
        print(n - 1);//自己调用自己
    }
    

    第五步:返回

    方法没有返回值,所以不返回

    例2:等差数列,给定第一项为a,公差为d,求第n项的值

    如,第一项为1,公差为2的等差数列为:1,3,5,7,9,11……

    第一步:找到大问题是什么?

    求第n项的值

    //已知a:第一项,d:公差,n:求第几项
    public static int f(int a, int d, int n) {
    
    }
    

    第二步:找到最简单的问题是什么?满足最简单问题时应该做什么?

    第1项是已知的,不需要计算,所以「求第1项的值」是最简单的问题,满足时直接返回值

    //a:第一项,d:公差,n:求第几项
    public static int f(int a, int d, int n) {
        if (n == 1)//最简单问题
            return a;
    }
    

    第三步:找到重复逻辑是什么?

    第n-1项 + 公差d = 第n项

    //a:第一项,d:公差,n:求第几项
    public static int f(int a, int d, int n) {
        if (n == 1)//最简单问题
            return a;
        return f(a, d, n - 1) + d;//重复逻辑
    }
    

    第四步:自己调用自己

    //a:第一项,d:公差,n:求第几项
    public static int f(int a, int d, int n) {
        if (n == 1)//最简单问题
            return a;
        return f(a, d, n - 1) + d;//重复逻辑、自己调用自己
    }
    

    第五步:返回

    //a:第一项,d:公差,n:求第几项
    public static int f(int a, int d, int n) {
        if (n == 1)//最简单问题
            return a;
        return f(a, d, n - 1) + d;//重复逻辑、自己调用自己、返回
    }
    

    例3:斐波那契数列(1,1,2,3,5,8,13……),求第n项的值

    第一步:找到大问题是什么?

    求第n项的值

    //求第n项
    public static int f(int n) {
    
    }
    

    第二步:找到最简单的问题是什么?满足最简单问题时应该做什么?

    第1项和第2项是已知的,不需要计算,所以「求第1项或第2项的值」是最简单的问题,满足时返回1

    public static int f(int n) {
        if (n < 3)//最简单问题
            return 1;
    }
    

    第三步:找到重复逻辑是什么?

    某项值等于其前两项值之和

    public static int f(int n) {
        if (n < 3)//最简单问题
            return 1;
        return f(n - 1) + f(n - 2);//重复逻辑
    }
    

    第四步:自己调用自己

    public static int f(int n) {
        if (n < 3)//最简单问题
            return 1;
        return f(n - 1) + f(n - 2);//重复逻辑、自己调用自己
    }
    

    第五步:返回

    public static int f(int n) {
        if (n < 3)//最简单问题
            return 1;
        return f(n - 1) + f(n - 2);//重复逻辑、自己调用自己、返回
    }
    

    例4:反转一个单向链表(LeetCode第206题)

    第一步:找到大问题是什么?

    反转一个单向链表

    public ListNode reverseList(ListNode head) {
    
    }
    

    第二步:找到最简单的问题是什么?满足最简单问题时应该做什么?

    当链表为空链表或只有一个结点时,无所谓正反,所以「反转空链表或只有一个结点的链表」是最简单的问题,满足时直接返回头结点

    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {//最简单问题
            return head;
        }
    }
    

    第三步:找到重复逻辑是什么?

    我们以只有两个结点的链表为例,过程如下:
    在这里插入图片描述

    多结点的链表也是在重复上图过程,所以重复逻辑是:将每个指针反转方向

    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {//最简单问题
            return head;
        }
        head.next.next = head;//重复逻辑
        head.next = null;//重复逻辑
    }
    

    第四步:自己调用自己

    下面是一个多结点链表
    在这里插入图片描述
    如果从head开始将每个指针反转,就会出现下面的情况:结点2后面的结点都丢了。

    在这里插入图片描述

    所以从head开始行不通,那就从最后一个结点开始。刚好从最后一个结点开始反转时,正好是最简单的情况。

    在这里插入图片描述

    如上图:每个框都可以看做是一个链表,大链表的子链表也是一个链表,所以解决方式是一样的,刚好符合大问题不断分解成小问题。

    那么,如何从最后一个结点开始呢?答案是自己调用自己

    在自己调用自己的过程中,链表不断变小,最后成为最简单问题

    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {//最简单问题
            return head;
        }
        ListNode p = reverseList(head.next);//自己调用自己
        head.next.next = head;//重复逻辑
        head.next = null;//重复逻辑
    }
    

    在这里插入图片描述

    第五步:返回

    需要返回反转后的链表的头结点,用变量p接收

    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {//最简单问题
            return head;
        }
        ListNode p = reverseList(head.next);//自己
        head.next.next = head;//重复逻辑
        head.next = null;//重复逻辑
        return p;//返回
    }
    

    总结

    上面的例子可以分为两类:

    1. 数据的定义是按递归定义的。如斐波那契

    2. 问题解法按递归算法实现。如反转链表

    除此之外还有一类,

    1. 数据的结构形式是按递归定义的。如二叉树

    现在再看一下流程:

    第一步:找到大问题是什么?

    第二步:找到重复逻辑是什么?

    第三步:找到最简单的问题是什么?满足最简单问题时应该做什么?

    第四步:自己调用自己

    第五步:返回

    通过上面几个例子,可以看出,递归就是解决大问题的过程。

    那么什么是大问题?

    大问题就是一开始想要做的事,如例4中的反转整个链表。

    但是由于问题太“大”,所以可能不容易解决,那么就需要把大问题分解成小问题。如例4中将「反转5个结点的链表的问题」分解成「反转4个结点的链表的问题」。

    那么什么是小问题?

    小问题和大问题在本质上是一样的,他们都是在「用同样的方式解决同样的问题」,不同的只是问题的规模大小。如「反转5个结点的链表的问题」和「反转4个结点的链表的问题」,问题和解决方式是一样的。

    如果小问题还不容易解决,那么就继续分解。这个分解的过程就是递归的「递」。

    那么如何分解?

    方法自己调用自己就是在不断分解问题,一定要注意方法的参数,参数才是「递」的关键。

    如例3中的f(n-1)+f(n-2),如例4中的reverseList(head.next),参数一直在变小,参数变小说明问题在不断变小。

    分解的目的是「使大问题变成一系列等价的小问题,从而解决大问题」,但是不能一直分解下去,当满足一定条件时,要停止分解。

    那么什么时候停止?

    当将大问题分解成最简单的问题(也称为终止条件)时,就可以停止分解了。

    那么什么是最简单的问题?

    如柜台例子中第1个人看到柜台时、例1中打印数字0时、如例3中求第1项或第2项的值时、如例4中反转空链表或只有一个结点的链表时,这些都是最简单问题。

    这些问题的共同特点是「问题已经足够简单,不需要再分解就可以直接解答」,最简单的问题(终止条件)通常是if语句的形式,当满足时直接返回结果。并且,递归中的 「归」从此开始

    以上几个「那么」构成了递归的基本形式,但是并不能解决问题(大问题、小问题),因为空有形式却没有方式,真正解决问题的方式是重复逻辑

    那么什么是重复逻辑?

    可以看成是分解过程(递)在重复做的事,也可以看成是每个问题(大问题、小问题)的解法,解决最简单问题不使用该解法。如例1中的打印语句,例2中的f(n-1) +d ,例3中的f(n-1) + f(n-2),例4中的反转指针。

    解决完问题后(注意解决问题的顺序:先解决最简单问题,最后解决大问题),需要把值返回。

    那么怎么返回?

    使用return语句返回。注意:解决某个小问题后得到的值返回给上一个大问题,用来解决上一个大问题。

    最终解决了大问题。

    仔细看一下上面几个「那么」的叙述过程,也是递归的。

    一句话总结:

    通过分解()将大问题不断分解成小问题直至最简单的问题。然后从最简单的问题开始解决,解决每个问题得到的值被返回,用来解决上一个较大的问题(解决问题时使用重复逻辑)(),直到解决了最大问题。

    如有错误,还请指正。


    文章首发于公众号『行人观学』

    在这里插入图片描述


  • 相关阅读:
    js_未结束的字符串常量
    [转]关于项目管理的思考
    Nhibernate理解
    Visual Studio 2005常用插件搜罗
    基本概念
    resharper 2.0
    Nhibernate资源
    [转]关于项目管理的知识点
    style
    带分数 蓝桥杯
  • 原文地址:https://www.cnblogs.com/xingrenguanxue/p/13124383.html
Copyright © 2020-2023  润新知