递归算法详解

递归(英语:recursion)在电脑科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。

0、前言

递归其实和循环是非常像的,循环可以改写成递归,递归未必能改写成循环,这是一个充分不必要的条件。

  • 那么,有了循环,为什么还要用递归呢??在某些情况下(费波纳切数列,汉诺塔),使用递归会比循环简单很多很多
  • 话说多了也无益,让我们来感受一下递归吧。

我们初学编程的时候肯定会做过类似的练习:

  • 1+2+3+4+....+100(n)求和
  • 给出一个数组,求该数组内部的最大值
我们要记住的是,想要用递归必须知道  两个条件:
  • 递归出口(终止递归的条件)
  • 递归表达式(规律)

技巧:在递归中常常是将问题切割成两个部分(1和整体的思想),这能够让我们快速找到递归表达式(规律)

一、求和

如果我们使用for循环来进行求和1+2+3+4+....+100,那是很简单的:


  1. int sum = 0;

  2. for (int i = 1; i <= 100; i++) {

  3. sum = sum + i;

  4. }

  5. System.out.println("公众号:Java3y:" + sum);

前面我说了,for循环都可以使用递归来进行改写,而使用递归必须要知道两个条件:1、递归出口,2、递归表达式(规律)

首先,我们来找出它的规律:1+2+3+...+n,这是一个求和的运算,那么我们可以假设X=1+2+3+...+n,可以将1+2+3+...+(n-1)看成是一个整体。而这个整体做的事又和我们的初始目的(求和)相同。以我们的高中数学知识我们又可以将上面的式子看成X=sum(n-1)+n

好了,我们找到我们的递归表达式(规律),它就是sum(n-1)+n,那递归出口呢,这个题目的递归出口就有很多了,我列举一下:

  • 如果n=1时,那么就返回1
  • 如果n=2时,那么就返回3(1+2)
  • 如果n=3时,那么就返回6(1+2+3)

当然了,我肯定是使用一个最简单的递归出口了:if(n=1) return 1

递归表达式和递归出口我们都找到了,下面就代码演示:

递归出口为1:


  1. public static void main(String[] args) {

  2. System.out.println("公众号:Java3y:" + sum(100));

  3. }

  4. /**

  5. *

  6. * @param n 要加到的数字,比如题目的100

  7. * @return

  8. */

  9. public static int sum(int n) {

  10. if (n == 1) {

  11. return 1;

  12. } else {

  13. return sum(n - 1) + n;

  14. }

  15. }

递归出口为4:


  1. public static void main(String[] args) {

  2. System.out.println("公众号:Java3y:" + sum(100));

  3. }

  4. /**

  5. *

  6. * @param n 要加到的数字,比如题目的100

  7. * @return

  8. */

  9. public static int sum(int n) {

  10. //如果递归出口为4,(1+2+3+4)

  11. if (n == 4) {

  12. return 10;

  13. } else {

  14. return sum(n - 1) + n;

  15. }

  16. }

结果都是一样的。

二、数组内部的最大值

如果使用的是循环,那么我们通常这样实现:


  1. int[] arrays = {2, 3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 2};

  2. //将数组的第一个假设是最大值

  3. int max = arrays[0];

  4. for (int i = 1; i < arrays.length; i++) {

  5. if (arrays[i] > max) {

  6. max = arrays[i];

  7. }

  8. }

  9. System.out.println("公众号:Java3y:" + max);

那如果我们用递归的话,那怎么用弄呢?首先还是先要找到递归表达式(规律)和递归出口

  • 我们又可以运用1和整体的思想来找到规律
    • 将数组第一个数->2与数组后面的数->{3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 2}进行切割,将数组后面的数看成是一个整体X={3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 2},那么我们就可以看成是第一个数和一个整体进行比较if(2>X) return 2 else(2
    • 而我们要做的就是找出这个整体的最大值与2进行比较。找出整体的最大值又是和我们的初始目的(找出最大值)是一样的
    • 也就可以写成if( 2>findMax() )return 2 else return findMax()
  • 递归出口,如果数组只有1个元素时,那么这个数组最大值就是它了。

使用到数组的时候,我们通常为数组设定左边界和右边界,这样比较好地进行切割

  • L表示左边界,往往表示的是数组第一个元素,也就会赋值为0(角标为0是数组的第一个元素)
  • R表示右边界,往往表示的是数组的长度,也就会赋值为arrays.length-1(长度-1在角标中才是代表最后一个元素)

那么可以看看我们递归的写法了:


  1. public static void main(String[] args) {

  2. int[] arrays = {2, 3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 1};

  3. System.out.println("公众号:Java3y:" + findMax(arrays, 0, arrays.length - 1));

  4. }

  5. /**

  6. * 递归,找出数组最大的值

  7. * @param arrays 数组

  8. * @param L 左边界,第一个数

  9. * @param R 右边界,数组的长度

  10. * @return

  11. */

  12. public static int findMax(int[] arrays, int L, int R) {

  13. //如果该数组只有一个数,那么最大的就是该数组第一个值了

  14. if (L == R) {

  15. return arrays[L];

  16. } else {

  17. int a = arrays[L];

  18. int b = findMax(arrays, L + 1, R);//找出整体的最大值

  19. if (a > b) {

  20. return a;

  21. } else {

  22. return b;

  23. }

  24. }

  25. }

三、冒泡排序递归写法

在冒泡排序章节中给出了C语言的递归实现冒泡排序,那么现在我们已经使用递归的基本思路了,我们使用Java来重写一下看看:

冒泡排序:俩俩交换,在第一趟排序中能够将最大值排到最后面,外层循环控制排序趟数,内层循环控制比较次数

以递归的思想来进行改造:

  • 当第一趟排序后,我们可以将数组最后一位(R)和数组前面的数(L,R-1)进行切割,数组前面的数(L,R-1)看成是一个整体,这个整体又是和我们的初始目的(找出最大值,与当前趟数的末尾处交换)是一样的
  • 递归出口:当只有一个元素时,即不用比较了:L==R

  1. public static void main(String[] args) {

  2. int[] arrays = {2, 3, 4, 5, 1, 5, 2, 9, 5, 6, 8, 3, 1};

  3. bubbleSort(arrays, 0, arrays.length - 1);

  4. System.out.println("公众号:Java3y:" + arrays);

  5. }

  6. public static void bubbleSort(int[] arrays, int L, int R) {

  7. int temp;

  8. //如果只有一个元素了,那什么都不用干

  9. if (L == R) ;

  10. else {

  11. for (int i = L; i < R; i++) {

  12. if (arrays[i] > arrays[i + 1]) {

  13. temp = arrays[i];

  14. arrays[i] = arrays[i + 1];

  15. arrays[i + 1] = temp;

  16. }

  17. }

  18. //第一趟排序后已经将最大值放到数组最后面了

  19. //接下来是排序"整体"的数据了

  20. bubbleSort(arrays, L, R - 1);

  21. }

  22. }

递归算法详解_第1张图片

四、斐波那契数列

接触过C语言的同学很可能就知道什么是费波纳切数列了,因为往往做练习题的时候它就会出现,它也是递归的典型应用。

菲波那切数列长这个样子:{1 1 2 3 5 8 13 21 34 55..... n }

数学好的同学可能很容易就找到规律了:前两项之和等于第三项

例如:


  1. 1 + 1 = 2

  2. 2 + 3 = 5

  3. 13 + 21 = 34

如果让我们求出第n项是多少,那么我们就可以很简单写出对应的递归表达式了:Z = (n-2) + (n-1)

递归出口在本题目是需要有两个的,因为它是前两项加起来才得出第三项的值

同样地,那么我们的递归出口可以写成这样:if(n==1) retrun 1 if(n==2) return 2

下面就来看一下完整的代码吧:


  1. public static void main(String[] args) {

  2. int[] arrays = {1, 1, 2, 3, 5, 8, 13, 21};

  3. //bubbleSort(arrays, 0, arrays.length - 1);

  4. int fibonacci = fibonacci(10);

  5. System.out.println("公众号:Java3y:" + fibonacci);

  6. }

  7. public static int fibonacci(int n) {

  8. if (n == 1) {

  9. return 1;

  10. } else if (n == 2) {

  11. return 1;

  12. } else {

  13. return (fibonacci(n - 1) + fibonacci(n - 2));

  14. }

  15. }

五、汉诺塔算法

图片来源百度百科:

递归算法详解_第2张图片

玩汉诺塔的规则很简单:

  • 有三根柱子,原始装满大小不一的盘子的柱子我们称为A,还有两根空的柱子,我们分别称为B和C(任选)
  • 最终的目的就是将A柱子的盘子全部移到C柱子中
    • 移动的时候有个规则:一次只能移动一个盘子,小的盘子不能在大的盘子上面(反过来:大的盘子不能在小的盘子上面)

我们下面就来玩一下:

  • 只有一个盘子:
    • A柱子的盘子直接移动到C柱子中
    • 完成游戏
  • 只有两个盘子:
    • 将A柱子上的盘子移动到B柱子中
    • 将A柱子上的盘子移动到C柱子中
    • 最后将在B柱子的盘子移动到C柱子盘子中
    • 完成游戏
  • 只有三个盘子:
    • 将A柱子的盘子移动到C柱子中
    • 将A柱子上的盘子移动到B柱子中
    • 将C柱子盘子移动到B柱子盘子中
    • 将A柱子的盘子移动到C柱子中
    • 将B柱子的盘子移动到A柱子中
    • 将B柱子的盘子移动到C柱子中
    • 最后将A柱子的盘子移动到C柱子中
    • 完成游戏

递归算法详解_第3张图片


.......................

从前三次玩法中我们就可以发现的规律:

  • 想要将最大的盘子移动到C柱子,就必须将其余的盘子移到B柱子处(借助B柱子将最大盘子移动到C柱子中[除了最大盘子,将所有盘子移动到B柱子中])[递归表达式]
  • 当C柱子有了最大盘子时,所有的盘子在B柱子。现在的目的就是借助A柱子将B柱子的盘子都放到C柱子中(和上面是一样的,已经发生递归了)
  • 当只有一个盘子时,就可以直接移动到C柱子了(递归出口)
    • A柱子称之为起始柱子,B柱子称之为中转柱子,C柱子称之为目标柱子
    • 从上面的描述我们可以发现,起始柱子、中转柱子它们的角色是会变的(A柱子开始是起始柱子,第二轮后就变成了中转柱子了。B柱子开始是目标柱子,第二轮后就变成了起始柱子。总之,起始柱子、中转柱子的角色是不停切换的)

简单来说就分成三步:

  1. 把 n-1 号盘子移动到中转柱子
  2. 把最大盘子从起点移到目标柱子
  3. 把中转柱子的n-1号盘子也移到目标柱子

那么就可以写代码测试一下了(回看上面玩的过程):


  1. public static void main(String[] args) {

  2. int[] arrays = {1, 1, 2, 3, 5, 8, 13, 21};

  3. //bubbleSort(arrays, 0, arrays.length - 1);

  4. //int fibonacci = fibonacci(10);

  5. hanoi(3, 'A', 'B', 'C');

  6. System.out.println("公众号:Java3y" );

  7. }

  8. /**

  9. * 汉诺塔

  10. * @param n n个盘子

  11. * @param start 起始柱子

  12. * @param transfer 中转柱子

  13. * @param target 目标柱子

  14. */

  15. public static void hanoi(int n, char start, char transfer, char target) {

  16. //只有一个盘子,直接搬到目标柱子

  17. if (n == 1) {

  18. System.out.println(start + "---->" + target);

  19. } else {

  20. //起始柱子借助目标柱子将盘子都移动到中转柱子中(除了最大的盘子)

  21. hanoi(n - 1, start, target, transfer);

  22. System.out.println(start + "---->" + target);

  23. //中转柱子借助起始柱子将盘子都移动到目标柱子中

  24. hanoi(n - 1, transfer, start, target);

  25. }

  26. }

我们来测试一下看写得对不对:

递归算法详解_第4张图片

参考资料:

  • 如何理解汉诺塔的递归? - 知乎

六、总结

递归的确是一个比较难理解的东西,好几次都把我绕进去了....

要使用递归首先要知道两件事:

  • 递归出口(终止递归的条件)
  • 递归表达式(规律)

在递归中常常用”整体“的思想,在汉诺塔例子中也不例外:将最大盘的盘子看成1,上面的盘子看成一个整体。那么我们在演算的时候就很清晰了:将”整体“搬到B柱子,将最大的盘子搬到C柱子,最后将B柱子的盘子搬到C柱子中

因为我们人脑无法演算那么多的步骤,递归是用计算机来干的,只要我们找到了递归表达式和递归出口就要相信计算机能帮我们搞掂。

在编程语言中,递归的本质是方法自己调用自己,只是参数不一样罢了。

最后,我们来看一下如果是5个盘子,要运多少次才能运完:

递归算法详解_第5张图片



 计算阶乘

  计算阶乘是递归程序设计的一个经典示例。计算某个数的阶乘就是用那个数去乘包括 1 在内的所有比它小的数。例如,factorial(5) 等价于 5*4*3*2*1,而 factorial(3) 等价于 3*2*1。

  阶乘的一个有趣特性是,某个数的阶乘等于起始数(starting number)乘以比它小一的数的阶乘。例如,factorial(5) 与 5 * factorial(4) 相同。您很可能会像这样编写阶乘函数:

不过,这个函数的问题是,它会永远运行下去,因为它没有终止的地方。函数会连续不断地调用 factorial。 当计算到零时,没有条件来停止它,所以它会继续调用零和负数的阶乘。因此,我们的函数需要一个条件,告诉它何时停止。

  由于小于 1 的数的阶乘没有任何意义,所以我们在计算到数字 1 的时候停止,并返回 1 的阶乘(即 1)。因此,真正的递归函数类似于:

递归算法详解_第6张图片

可见,只要初始值大于零,这个函数就能够终止。停止的位置称为 基线条件(base case)。基线条件是递归程序的 最底层位置,在此位置时没有必要再进行操作,可以直接返回一个结果。所有递归程序都必须至少拥有一个基线条件,而且 必须确保它们最终会达到某个基线条件;否则,程序将永远运行下去,直到程序缺少内存或者栈空间。

下面这个求 n! 的例子中,递归出口(确定递归什么时候结束)是fun(1)=1,递归体(确定递归求解时的递归关系)是fun(n)=n*fun(n-1),n>1。

求n! 的递归思路就是把 fun(n)=n! 转化成 fun(n-1) ,再把 fun(n-1) 分解为 f(n-2) 来解决,依此类推直到每个问题都可以直接解决(分解到 fun(1) )。

递归算法详解_第7张图片

递归算法详解_第8张图片

递归算法详解_第9张图片

你可能感兴趣的:(C++学习,算法学习,算法,递归)