方法递归(斐波那契数列,青蛙跳台阶,汉诺塔问题)

递归的概念

什么是递归?
程序调用自身的编程技巧称为递归( recursion)。 递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。 递归的主要思考方式在于:把大事化小
递归的两个必要条件

  • 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
  • 每次递归调用之后越来越接近这个限制条件。

递归执行过程的详细分析

代码示例: 递归求 N 的阶乘:

 public static void main(String[] args) {
        int n = 3;
        int ret = factor(n);
        System.out.println("ret = " + ret);
    }
    public static int factor(int n) {
        if (n == 1) {
            return 1;
        }
        return n * factor(n - 1); // factor 调用函数自身
    }

**假设我们用求3的阶乘来举例,要求3的阶乘,可以转换为3乘2的阶乘,可以先求2的阶乘,要求2的阶乘可以先求1的阶乘,再乘2,1的阶乘求完后,不满足递归条件,就把值一步一步的回退到调用函数内部求积,这样如果是一个复杂的问题就可以简化的很简单的问题了,**简化了一些重复的步骤;比如要求n的阶乘,就先求(n-1)的阶乘,之后再乘上n,要求(n-1)的阶乘,就先求(n-2)的阶乘在乘个(n-1)即可,一直求出1的阶乘为止。这里可能这个阶乘这个简单的问题你没有体会到递归的简便性,但是我们先从简单问题把递归看懂,在看难的问题。
图示分析代码执行过程:

在这里插入图片描述
上面是求3的阶乘的代码执行流程,其实递归递归,就是“递”向问题的小的一方面走下去,“归”回归,程序执行到某一条件时,回归到起始的递位置。
大致过程如下图:
在这里插入图片描述

函数调用时,函数在栈上开辟空间,函数的参数,局部变量,返回数据,返回地址等也是在栈上开辟的,栈的特点先进后出,理解这一点可以帮助我们理解函数递归的“归”的过程,你细细品就行。
在这里插入图片描述

经典递归问题分析与解答

对于递归本身有点抽象有点难想,多找找规律和画图理解一下,递归重点是找递归公式,如果你找的到递归的公式那很简单的照的公式写代码行。对于有些问题如递归非递归都能做还是最好不使用递归,非递归的效率更高。

斐波那契数列问题

斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)
代码实现斐波那契数列

 public static int fib(int n) {
        if (n == 1 || n == 2) {
            return 1;
        }
        return fib(n - 1) + fib(n - 2);
    }

    public static void main(String[] args) {
        Scanner scanner=new Scanner(System.in);
        int a=scanner.nextInt();
        System.out.println(fib(a));
    }



从斐波那契数列的形式可以轻松地看出,它的规律,得到它的递推公式,从第三个斐波那契数开始,斐波那契数都是前两个数字的和,第一二两个数字都是1.假设要求第一百个斐波那契数数列,要先求地99和第98个,要求第99个又要求第第98,97个…,以此类推,可知求斐波那契数列也是遵循大事化小的,也是一个递归问题,当然用递归的方式求菲波那切数列固然简单,但在求值过程中做了许多重复的计算效率太低,(画个树形图简单看一下秒懂)求该数列也可以使用非递归的方式,循环更加高效

非递归的斐波那契数列:

public static int fib(int n) {
    int a=1;
    int b=1;
    int c=1;
    while (n>2) {
        c=a+b;
        a=b;
        b=c;
        n--;
    }
    return c;

}
    public static void main(String[] args) {
        Scanner scanner=new Scanner(System.in);
        int n= scanner.nextInt();
        System.out.println(fib(n));
    }



青蛙跳台阶问题

题目连接
一只青蛙一次可以跳上 1 级台阶,也可以跳上2 级。求该青蛙跳上一个n 级的台阶总共有多少种跳法?
对于一个问题,我们拿到首先是思考下,该计算计算,该画图画图,找到规律,关键点之后再写代码。
情况一,只有一阶台阶,一次跳一个台阶只有1种跳法

在这里插入图片描述

情况二,有两阶台阶,可以一步一步的跳,也可以一步跳两个台阶,有两种跳法

在这里插入图片描述

情况三,有三阶台阶,由于题目说,小青蛙每次只能跳一阶或者两阶台阶**,所以有两种情况,如果跳一阶台阶,之后后面有两阶台阶,跳法和情况二相似;如果青蛙跳两阶台阶,之后后面只有了一阶台阶,这和情况一的跳法一样**。这样一来我们大概就明白了,情况三有三阶台阶,两种情况,总的跳法就是前两种台阶跳法加起来为3种。

在这里插入图片描述
那么我们大致来想一想如果有4阶台阶呢,更多的台阶呢?
小青蛙每次跳一阶或者两阶台阶,如果跳一阶台阶后面有三阶台阶,之后的跳法不是和三阶台阶的跳法一样吗?如果跳两阶台阶,后面剩的两阶台阶,和两阶台阶的跳法是一样的。这样由数学归纳法就可以得出,如果有n阶台阶,小青蛙的跳法就为,(n-1阶)的跳法,加上(n-2)阶的跳法。这样我们发现这个问题也是一个递归问题,要求n阶的跳法太复杂了,我们把它分解为小问题后倒着求来求值,把这个求n阶的问题,分解为以一阶二阶为基础的求法。青蛙跳台阶的归纳公式为F(1)=1,F(2)=2, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*),我们可以看出其实这个青蛙跳台阶就是从那个斐波那契数列演变而来的。
代码实现:

  public static void main(String[] args) {
        Scanner scanner=new Scanner(System.in);

        int n = scanner.nextInt(); //台阶数
        System.out.println(jumpFloor(n));
    }

        public static  int jumpFloor(int floor) {
            if (floor == 1) { //情况一
                return 1;
            }
            if (floor == 2) { //情况二
                return 2;
            }
            return jumpFloor(floor - 1) + jumpFloor(floor - 2); //函数递归
        }


变种青蛙跳台阶

题目描述
题目连接
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶(n为正整数)总共有多少种跳法。
分析:
变种青蛙跳台阶没有规定青蛙跳的步数,所以青蛙跳的肯更情况也增多了,比如一步跳的情况,任何台阶,从地面上直接跳到最高阶,勇敢的青蛙都能跳了,还有前面所有跳的情况,现在的青蛙都能跳出来了,假设直接从地面跳的跳法为f(0)=1,我们由图推导出递归的公式,那如果有n阶台阶的跳法为f(n) = f(n-1) + f(n-2) +....+f(0),(f(0),f(1)都为1)。递归的时间复杂度:O(N^N),空间复杂度:O(N)。

在这里插入图片描述
那么递归代码如下:

public class Test {


    public static int jumpFloorII(int number) {
        int sum = 1;
        for(int i = 1; i<number;i++)
            sum += jumpFloorII(number-i);  //计算f(number-1)
        return sum;
    }

    public static void main(String[] args) {
        Scanner scanner=new Scanner(System.in);
        int a=scanner.nextInt();
        System.out.println(jumpFloorII(a));

    }

与递归思路一样,但是普通的递归进行了多次无用计算。 比如在上述递归中,f(1),f(2)…被多次计算。我们可以进行优化,方法有很多种,我选择了最好理解的简单的,数学的方式来解答,f[n] = f[n-1] + f[n-2] + … + f[0],那么f[n-1] = f[n-2] + f[n-3] + … + f[0]
所以一合并,f[n] = 2*f[n-1],f[n]/f[n-1]=2初始条件f[0] = f[1] = 1,为一个等比数列,之后求出等比数列的通项公式为f[n]=2^(n-1),时间复杂度:O(n),空间复杂度:O(1)

代码实现:

public class Test {
        public static  int jumpFloorII(int number) {
            if(number==1||number==0) return 1;
            return 1<<number-1; //口诀:左移乘2,右移除2
        }
    public static void main(String[] args) {
        Scanner scanner=new Scanner(System.in);
        int a=scanner.nextInt();
        System.out.println(jumpFloorII(a));

    }
}

汉诺塔问题

题目连接
汉诺塔问题是一个经典的问题。汉诺塔(Hanoi Tower),又称河内塔,源于印度一个古老传说。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,任何时候,在小圆盘上都不能放到大圆盘上,取放圆盘必须在柱子上,不能把圆盘放在其他位置,且在三根柱子之间一次只能移动一个圆盘。问应该如何操作?
三根柱子分别命名为A,B,C,把任意个圆盘从第一根柱子A上移动到最后一根柱子C的基本思路:

1.当n=1的时候,直接把圆盘从A盘移动到C盘
当n>1的时候
1. 将A柱上的n-1个盘子借助C柱移动到B柱上
2. 将A柱上面的第n个盘子移动到到C 柱上面。
3. 将B柱上面的n-1个盘子移动到C柱上面

简单分析:
首先递归随着递归的层次深入,问题越来越复杂,很抽象,到达一定的规模是人脑无法思考的,我们只能通过简单的递归找到规律,之后书写代码。一个两个三个盘子的情况是非常简单容易想象的,假设有三个盘子,我们首先把上面的2个盘子移经过C移动到B(有多个盘子的时候一定要借助其他的盘子移动,这样才能保证大的盘子在下面),**之后把第三个盘子移动到C柱,把A上面的2个盘子移动到B柱上面又是一次递归操作和原来的问题是不是一个问题呢?只是盘子变少了,每次递归的传过来的实际参数也变了,**传过来的实际参数就是每次递归时带进来的。那么如果移动n个盘子的思路是不是和上面的说的一样呢,这是不是体现了递归的的思想,“大事化小”,每次递归时都更加接近递归结束的条件,学习递归时慢慢来,首先从简单到复杂,理解简单的再到复杂的过程,复杂的过程不必太死扣,就是那样像简单的那样执行思路,如果你较真去死扣每一步,我想是很难理解递归的
一个盘子A->C,1=2^1-1
两个盘子的情况: A->B A->C B->C ,移动三次, 3= 2^2 -1
在这里插入图片描述
三个盘子的移动:
A->C A->B C->B A->C B->A B->C A->C , 7 =2^3 -1

有三个盘子,我们首先把上面的2个盘子移经过C移动到B就是蓝色那一坨是一次递归,使用第一个递归函数,之后把第三个盘子移动到C柱后,把A上面的2个盘子移动到B柱上面又是那一坨又是一次递归。
在这里插入图片描述
由上面的一个两个三个盘子的移动次数,我们可以归纳出,移动n个盘子的次数为2^n-1次,,回到最初的这个问题,当金刚石柱上有64个黄金圆盘时,如果移动需要移动的次数为2 ^ 64-1,如果一秒钟移动一次的化,那么需要2 ^64-1=1.845*10 ^19,约为5849亿年!可以想象问题的有多复杂了。

代码示例:

public class TestDemo {
    public static void move(char pos1,char pos2) {
        System.out.print(pos1+"->"+pos2+"  ");
    }


    /**
     *
     * @param n 当前的盘子个数
     * @param pos1 起始位置
     * @param pos2 中转位置
     * @param pos3 目的位置
     */
    public static void hanoiTower(int n,char pos1,char pos2,char pos3) {//传入的实参会发生变化
        if(n == 1) {
            move(pos1,pos3);
        }else {
            hanoiTower(n-1,pos1,pos3,pos2);
            move(pos1,pos3);
            hanoiTower(n-1,pos2,pos1,pos3);
        }
    }

    public static void main(String[] args) {
        hanoiTower(1,'A','B','C');
        System.out.println();
        hanoiTower(2,'A','B','C');
        System.out.println();
        hanoiTower(3,'A','B','C');
        System.out.println();

    }
}

🛣️过🉐小🧑🏽‍🤝‍🧑🏼如果9️⃣🉐🉑以🉐话记🉐点👍🏻🍹持👇🏻,🦀🦀

  • 42
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 27
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值