递归、迭代和分治(2):递归的典型例子

​本期我们一起看几个典型的递归的例子。

递归就是每次调用的时候方法是自己,但是参数变了,就是下面这个样子:

1.斐波那契数列

斐波那契数列的是这样一个数列:1、1、2、3、5、8、13、21、34….,即第一项 f(1) = 1,第二项 f(2) = 1…..,第 n 项目为 f(n) = f(n-1) + f(n-2)。求第 n 项的值是多少。

我们来看看递归该怎么写。

1、确定递归函数功能

假设 f(n) 的功能是求第 n 项的值,代码如下:

int f(int n){}

2、找出递归结束的条件

显然,当 n = 1 或者 n = 2 ,我们可以轻易着知道结果 f(1) = f(2) = 1。所以递归结束条件可以为 n <= 2。代码如下:

int f(int n){    if(n <= 2){        return 1;    }}

3、找出函数的等价关系式

题目已经把等价关系式给我们了,所以我们很容易就能够知道 f(n) = f(n-1) + f(n-2)。我说过,等价关系式是最难找的一个,而这个题目却把关系式给我们了,这也太容易,好吧,我这是为了兼顾几乎零基础的读者。

所以最终代码如下:

int f(int n){    // 1.先写递归结束条件    if(n <= 2){        return 1;    }    // 2.接着写等价关系式    return f(n-1) + f(n - 2);}

搞定,是不是很简单?

2.青蛙跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

1、确定递归函数功能

假设 f(n) 的功能是求青蛙跳上一个n级的台阶总共有多少种跳法,代码如下:

int f(int n){}

2、找出递归结束的条件

我说了,求递归结束的条件,你直接把 n 压缩到很小很小就行了,因为 n 越小,我们就越容易直观着算出 f(n) 的多少,所以当 n = 1时,你知道 f(1) 为多少吧?够直观吧?即 f(1) = 1。代码如下:

int f(int n){    if(n == 1){        return 1;    }}

3.找出函数的等价关系式

每次跳的时候,小青蛙可以跳一个台阶,也可以跳两个台阶,也就是说,每次跳的时候,小青蛙有两种跳法。

第一种跳法:第一次我跳了一个台阶,那么还剩下n-1个台阶还没跳,剩下的n-1个台阶的跳法有f(n-1)种。

第二种跳法:第一次跳了两个台阶,那么还剩下n-2个台阶还没,剩下的n-2个台阶的跳法有f(n-2)种。

所以,小青蛙的全部跳法就是这两种跳法之和了,即 f(n) = f(n-1) + f(n-2)。至此,等价关系式就求出来了。于是写出代码:

int f(int n){    if(n == 1){        return 1;    }    ruturn f(n-1) + f(n-2);}

代码看上去没问题对不对?

其实是有问题的,当 n = 2 时,显然会有 f(2) = f(1) + f(0)。我们知道,f(0) = 0,按道理是递归结束,不用继续往下调用的,但我们上面的代码逻辑中,会继续调用 f(0) = f(-1) + f(-2)。这会导致无限调用,进入死循环。

关于递归结束条件是否够严谨问题是递归算法的重要一环,有很多人在使用递归的时候,由于结束条件不够严谨,导致出现死循环。也就是说,当我们在第二步找出了一个递归结束条件的时候,可以把结束条件写进代码,然后进行第三步,但是请注意,当我们第三步找出等价函数之后,还得再返回去第二步,根据第三步函数的调用关系,会不会出现一些漏掉的结束条件。就像上面,f(n-2)这个函数的调用,有可能出现 f(0) 的情况,导致死循环,所以我们把它补上。代码如下:

int f(int n){    //f(0) = 0,f(1) = 1,等价于 n<=2时,f(n) = n。    if(n <= 2){        return n;    }    ruturn f(n-1) + f(n-2);}

那结束条件该怎么确定呢?其实最简单的方式就是写几个试一试,n足够小的时候也不过为 0 1 2 3 这几个种情况,或者是执行结束或者开始的时候几个元素,带进去试一试看看对不对就行了。

3.反转链表

反转单链表。例如链表为:1->2->3->4。反转后为 4->3->2->1。我们前面已经介绍过,这里再从递归的角度分析一下。

链表的节点定义如下:

class Node{    int date;    Node next;}

还是老套路,三要素一步一步来。

1、定义递归函数功能

假设函数 reverseList(head) 的功能是反转但链表,其中 head 表示链表的头节点。代码如下:

Node reverseList(Node head){}

2. 寻找结束条件

当链表只有一个节点,或者如果是空表的话,你应该知道结果吧?直接啥也不用干,直接把 head 返回呗。代码如下:

Node reverseList(Node head){    if(head == null || head.next == null){        return head;    }}

3. 寻找等价关系

这个的等价关系不像 n 是个数值那样,比较容易寻找。但是我告诉你,它的等价条件中,一定是范围不断在缩小,对于链表来说,就是链表的节点个数不断在变小,所以,如果你实在找不出,你就先对 reverseList(head.next) 递归走一遍,看看结果是咋样的。例如链表节点如下:

我们就缩小范围,先对 2->3->4递归下试试,即代码如下:

Node reverseList(Node head){    if(head == null || head.next == null){        return head;    }    // 我们先把递归的结果保存起来,先不返回,因为我们还不清楚这样递归是对还是错。,    Node newList = reverseList(head.next);}

我们在第一步的时候,就已经定义了 reverseLis t函数的功能可以把一个单链表反转,所以,我们对 2->3->4反转之后的结果应该是这样:

我们把 2->3->4 递归成 4->3->2。不过,1 这个节点我们并没有去碰它,所以 1 的 next 节点仍然是连接这 2。

接下来呢?该怎么办?

其实,接下来就简单了,我们接下来只需要把节点 2 的 next 指向 1,然后把 1 的 next 指向 null,不就行了?,即通过改变 newList 链表之后的结果如下:

也就是说,reverseList(head) 等价于 reverseList(head.next) + 改变一下1,2两个节点的指向。好了,等价关系找出来了,代码如下(有详细的解释):

//用递归的方法反转链表public static Node reverseList2(Node head){    // 1.递归结束条件    if (head == null || head.next == null) {             return head;         }         // 递归反转 子链表         Node newList = reverseList2(head.next);         // 改变 1,2节点的指向。         // 通过 head.next获取节点2         Node t1  = head.next;         // 让 2 的 next 指向 1         t1.next = head;         // 1 的 next 指向 null.        head.next = null;        // 把调整之后的链表返回。        return newList;    }

上面我们介绍了几个典型的递归题目,那是不是有优化空间呢?我们下期接着看。

4.汉诺塔问题

汉诺塔问题是最经典的递归问题了,如果这个问题理解了,递归基本就理解清楚了。题目是这样:

在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制:

(1) 每次只能移动一个盘子;

(2) 盘子只能从柱子顶端滑出移到下一根柱子;

(3) 盘子只能叠在比它大的盘子上。

请编写程序,用栈将所有盘子从第一根柱子移到最后一根柱子。

将原来的 A, B, C 重命名为 origin, buffer, destination

这个题目的递归解答简洁地让人惊讶:

class Solution {    public void hanota(List<Integer> origin, List<Integer> buffer, List<Integer> destination) {        // 使用递归函数来完成,需要计数        move(origin.size(), origin, destination, buffer);    }    private void move(int n, List<Integer> origin, List<Integer> destination, List<Integer> buffer) {        // 如果碰到了一个栈的底,那么说明这个位置上已经没有盘子移动了        if (n <= 0) return;        // 从 origin 移动到 buffer 上        move(n - 1, origin, buffer, destination);        // 从 origin 移动到 destination 上        destination.add(origin.get(origin.size() - 1));        origin.remove(origin.size() - 1);        // 从 buffer 移动到 destination 上        move(n - 1, buffer, destination, origin);    }}

这个题目画画图比较好理解,现在我们先不画了,读者可以在网上找找看,文字描述过程是这样的:

首先用盘子数 1 开始模拟: 直接就可以从 origin 移动到 destination.

盘子数为 2 时: 先从 origin 移动 1 到 buffer(origin -> buffer) , 然后再把 2 从 origin 移动到 destination(origin -> destination), 再从 buffer 移动 1 到 destination(buffer -> destination).

从上一步可以知道,我们可以将上俩盘子从 origin 移动到 buffer (origin -> buffer), 下一步只需要把 3 移动到 destination (origin -> destination), 再移动剩余的那俩就好了(buffer -> destination).

从上一步可以知道,我们可以将上面仨盘子从 origin 移动到 buffer...

以此类推, 总是可以移动完的.是一种递归结构.每一步需要完成的参考()括号中的注释。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

纵横千里,捭阖四方

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

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

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

打赏作者

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

抵扣说明:

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

余额充值