zoukankan      html  css  js  c++  java
  • 不知道怎样用递归?按步骤来!

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

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

    举一个生活中的例子

    有几个人在柜台前排队,现在甲想知道他排到第几个了,所以他会问排在他前面的乙是第几个,然后加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语句返回。注意:解决某个小问题后得到的值返回给上一个大问题,用来解决上一个大问题。

    最终解决了大问题。

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

    一句话总结:

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

    如有错误,还请指正。


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

    在这里插入图片描述


  • 相关阅读:
    python 基础2.5 循环中continue与breake用法
    python 基础 2.4 while 循环
    python 基础 2.3 for 循环
    python 基础 2.2 if流程控制(二)
    python 基础 2.1 if 流程控制(一)
    python 基础 1.6 python 帮助信息及数据类型间相互转换
    python 基础 1.5 python数据类型(四)--字典常用方法示例
    Tornado Web 框架
    LinkCode 第k个排列
    LeetCode 46. Permutations
  • 原文地址:https://www.cnblogs.com/xingrenguanxue/p/13124383.html
Copyright © 2011-2022 走看看