一文学会递归递推

递归算法和递推算法无论是在ACM竞赛还是项目工程上都有着极为广泛的应用,但想要完全掌握两者的思想并不容易,对于刚刚接触编程的人来说更是这样,我在初次接触递归递推时就吃了很多的苦头,除了当时对编程语言不太熟悉之外,最大的原因就是难以理解其中的思想,本文将二者结合代码分别讲解,力求以“理论+实践”的方式使读者明白两种算法。一箭双雕,一文双递。

一.递归和递推的区别

学习递归递推的一个容易遇到的问题就是混淆二者的概念。所以学习时首先就要明白二者的区别。

二者的区别也可以看做二者的概念。

  • 递归:一个方法/函数的自我嵌套,从结果出发一直向前回溯到初始状态(值),是一种程序自身调用自身的技巧,类似于套娃。举个栗子。
从前有座山,山里有座庙,庙里有个老和尚,在给小和尚讲故事。讲的什么故事呢?从前有座山,山里有座庙,庙里有个老和尚,在给小和尚讲故事。讲什么故事呢?从前有座山,山里有座庙,庙里有个老和尚,在给小和尚讲故事。讲的什么故事呢?从前有座山,山里有座庙,庙里有个老和尚,在给小和尚讲故事

这个大家耳熟能详的“老和尚讲故事”就是典型的递归。

  • 递推:与递归的从结果找初始状态(值)相反,递推是已知初始状态(值),一步步计算得到最后的结果。高中数学中的数列经常会使用递推式。如:
S(n) = S(n - 1) + n  ;  D(n)=D(n-1)+D(n-2)+3

从上述样例可以看出,一个数列的某一项是通过它的前一项或前几项通过某种计算得到的。

二.递归–自我调用,递尽而归

1.递归条件

(1)原问题可以变成更简单,规模更小的子问题,且原问题和子问题相似。

(2)一定要存在递归出口,即递归的结束条件。比如设置某种情况,在这种情况下会使递归函数返回,做到递尽而归。否则函数会一直递归下去,造成死循环。

2.递归图解

“一图胜千言”,用图解的方式可以更好的理解递归。

给出一个正整数n,计算出1+2+...+n的结果

这道题最快的方法是用求和公式,但是为了体现出递归的概念,这里采用递归的思想。

import java.util.Scanner;

public class ADD {
    public static void main(String[] args) {
        Scanner scanner=new Scanner(System.in);
        int n=scanner.nextInt();
        System.out.printf("sum=%d\n",sum(n));
    }
    private static int sum(int n){
        if(n==1)return 1;//到1开始返回
        return sum(n-1)+n;//否则继续递归,当前值和递归返回值相加
    }
}

在这里插入图片描述

从图中可以看出,函数先是不断的调用自身,函数每调用一次,参数就减一,当参数减到一的时候,函数开始返回1给上一层,就这样一层层返回。

3.典型题的代码实现

既然是"理论+实践",那习题是必不可少的了。学习算法需要刷题,学好算法需要大量的习题作为支持。

  • 递归乘法

    递归乘法。 写一个递归函数,不使用 * 运算符, 实现两个正整数的相乘。可以使用加号、减号、位移,但要吝啬一些。
    

    思路:首先根据题干要求,‘*’是无法使用的,且一定要用递归来完成,所以我们很容易想到用"加法+递归"的方式实现。

    代码:

    class Solution {
        public int multiply(int A, int B) {
            return add(A,B);
        }
        public int add(int A,int B){
            if(A==0)return 0;
            if(A>B)return add(B,A);//选择较小的数来作为控制调用次数的参数,可以节省时间和避免栈溢出
            return add(A-1,B)+B;
        }
    }
    
    注:此题来自leetcode。链接:https://leetcode-cn.com/problems/recursive-mulitply-lcci/
    
  • 汉诺塔

    法国数学家爱德华·卢卡斯曾编写过一个印度的古老传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片:一次只移动一片,不管在哪根针上,小片必须在大片上面。僧侣们预言,当所有的金片都从梵天穿好的那根针上移到另外一根针上时,世界就将在一声霹雳中消灭,而梵塔、庙宇和众生也都将同归于尽。

    这个传说是否真实我们不得而知,但是汉诺塔问题已经成为了学习递归时的一个再经典不过的问题了,在学习递归的过程中,十之八九都会遇到汉诺塔问题。

    在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制:
    (1) 每次只能移动一个盘子;
    (2) 盘子只能从柱子顶端滑出移到下一根柱子;
    (3) 盘子只能叠在比它大的盘子上。
    请编写程序,用栈将所有盘子从第一根柱子移到最后一根柱子。    
    

    思路:我们把三根柱子分别命名为A,B,C,开始时所有盘子都叠在A柱子上,要通过若干步全部转移到C上。假设N=1,则可以直接把盘子从A放到C上。当N=2时,就无法直接从A搬到C上了,这时B的作用就体现出来了。当N>1时,B就起到了一个"中转站"的作用,盘子可以先放到B上寄存,等可以放到C上时再放。可以把N个盘子分成上面的N-1个和最底下的1个,先把上面的N-1个通过C放到B上,再把底下的1个放到C上,最后再把B上的N-1个通过A放到C上。说着容易,N-1个盘子怎么移动呢,这就是递归的用武之地了,我们已经把N的问题变成N-1的问题了,所以可以进一步变成N-2的问题,这样下去一直到1个盘子的问题

    class Solution {
        public void hanota(List<Integer> A, List<Integer> B, List<Integer> C) {
            int n=A.size();//得出盘子的数量
            move(n,A,B,C);//通过B把盘子从A移动到C
        }
        public void move(int n,List<Integer> A, List<Integer> B, List<Integer> C){
            if(n==1){//只有一个盘子时
                C.add(A.get(A.size()-1));
                A.remove(A.size()-1);//直接从A移到C
                return ;
            }
            move(n-1,A,C,B);//把n-1个盘子从A通过C转移到B
            C.add(A.get(A.size()-1));//最后一个直接放到C上
            A.remove(A.size()-1);
            move(n-1,B,A,C);//再把n-1个盘子从B通过A转移到C
        }
    }
    
    注:此题来自leetcode。链接:https://leetcode-cn.com/problems/hanota-lcci
    

三.递推–初值传递,层层推进

1.一般方法

递推常常是某种规律用公式的形式表达出来,因此该算法一般都会先找出递推式,然后由递推式算出结果。

2.斐波那契数列

斐波那契数列是数学家莱昂纳多·斐波那契在研究兔子繁殖时引入的,也称为"兔子序列"。如下:

1 , 1 , 2 , 3 , 5 , 8 , 13 , 21 , 34 , · · ·

这个序列解释起来很简单,序列前两项为1,从第三项开始每一项都是前两项之和。递推式可以看成

Fib[n]=1,n=0||n=1

Fib[n]=Fib[n-1]+Fib[n-2],n>=3

现在我们知道了数列的初值(前两项),也知道了递推式,求关于斐波那契数列的问题就很容易了。

public class Fibonacci {
    public static void main(String[] args) {
        int[] Fib=new int[10];
        Fib[0]=1;
        Fib[1]=1;;
        for(int i=2;i<9;i++){
            Fib[i]=Fib[i-1]+Fib[i-2];
        }
    }
}
3.典型题的代码实现
  • 杨辉三角

    杨辉三角是是二项式系数在三角形中的一种几何排列,最初是被用来探究(a+b)^n的展开问题,在计算机中也是组合数学和递推算法的常用模型。

    在这里插入图片描述

通过上图可以看出,杨辉三角中每行的最左边和最右边的数都是1,且第n行就会有n个数字,一个数字是上面的两个数字之和。可求出的递推表达式为

 tri[i][j] = tri[i-1][j-1] + tri[i-1][j]
public class Triangle {
    public static void main(String[] args) {
        int[][] tri=new int[33][33];//杨辉三角用数组存储
        for(int i=0;i<=10;i++){
            tri[i][0]=tri[i][i]=1;//每行的最左边和最右边的数都是1,所以先把两边的数都设置成1
            for(int j=1;j<i;j++){//处理中间的内容
                tri[i][j]=tri[i-1][j]+tri[i-1][j-1];//递推式,把上面两数相加得到下面的数
            }
        }//打印三角
        for(int i=0;i<=10;i++){
            for(int j=0;j<=10;j++){
                System.out.print(tri[i][j]+" ");
            }
            System.out.println();
        }
    }
}

输出结果数组中的值为0的项删除后,得到的结果如下

1 
1 1 
1 2 1  
1 3 3 1 
1 4 6 4 1  
1 5 10 10 5 1 
1 6 15 20 15 6 1 
1 7 21 35 35 21 7 1 
1 8 28 56 70 56 28 8 1 
1 9 36 84 126 126 84 36 9 1  
1 10 45 120 210 252 210 120 45 10 1 
  • 伯努利-欧拉错排问题

    问题的最初形式是错装信封的问题。假设有n个信封,n封信,每封信都有对应的信封。求每封信都被装错的可能有多少种。后来就慢慢演化为有n个元素,在某种方式的排列以后,每个元素都不在原来位置上的可能有多少种。

    分析:设错装信封的可能数为D(n),首先拿出第n封信,按照规则可以放在除第n个信封之外的另外n-1个信封中,假设第n封信被放到了第k个信封中(k!=n),这时信和信封各减一,此时有两种可能。

    • 第k封信被放在了第n个信封中,这时信和信封又各减一,就有n-2个信封,n-2封信,可能数为D(n-2)

    • 第k封信没有被放在第n个信封中,就有n-1个信封,n-1封信,第k封信既然一定不会被放到第n个信封中,那么第k封信就可以认为是第n封信,这样就相当于只是把第k封信和第k个信封拿走,而其它的不变,可能数为D(n-1)

      两种可能相加就是一共的可能数,经分析得到递推式

      D(n) = (D(n-2) + D(n-1)) * (n-1)
      

      因为信k和信封k是从n-1个信和n-1个信封中任意抽取的,所以要在最外面乘以n-1

      代码:

      import java.util.Scanner;
      
      public class cuopai {
          public static void main(String[] args) {
              Scanner scanner=new Scanner(System.in);
              int n=scanner.nextInt();
              int[] D=new int[n+1];
              if(n==1){
                  System.out.println(0);
              }else if(n==2){
                  System.out.println(1);
              }else if(n>=3){
                  D[1]=0;
                  D[2]=1;
                  for(int i=3;i<=n;i++){
                      D[i]=(i-1)*(D[i-1]+D[i-2]);
                  }
              }
              System.out.println(D[n]);
          }
      }
      

如果您觉得这篇文章对您有所帮助,欢迎关注我的微信公众号–【悬浮流星】,阅读更多的类似文章。如果您发现该文章有错误或不足之处,也欢迎批评指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值