理解递归

date: 2016-08-18 9:10:00
title: 理解递归
categories: 数据结构

版权声明:本站采用开放的[知识共享署名-非商业性使用-相同方式共享 许可协议]进行许可

所有文章出现的代码,将会出现在我的github中,名字可以根据类全名来找,我在github中的文件夹也会加目录备注。


在接下来的更新列表中,会接触到树,图相关知识点,又因为树、图是非线性结构,并且树是递归定义的,很多关于树和图的问题,可以用递归很好的解决。所以想要学好树和图,先要对递归有所了解。

定义:函数对其本身直接或间接的调用叫做递归。

那么什么叫直接和间接?

public class RecursionDemo {

    // directly call
    public static void a(int n) {
        a(20);
    }

    // indirectly call
    public static void b(int m) {
        g(m);
    }

    public static void g(int k) {
        b(k);
    }
}

在上述的代码中,可以知道,在调用函数体中调用“自己”叫做直接调用,通过“第三方”函数来调用“自己”叫做间接调用。但是上面所示的代码,并没有给递归调用设置一个出口,也就是说,会抛出StackOverFlowException。

递归的实现

在C语言版数据结构(第二版)中,对于递归的实现是如下解释的:

一个递归函数,在函数的执行过程中,需要多次进行自我调用,那么,这个递归函数是如何执行的?先看任意两个函数之间进行调用的情形。

与汇编程序设计中主程序和子程序之间的链接及信息交换相类似,在高级语言编制的程序中,调用函数和被调用函数之间的链接及信息交换通过栈来进行,

通常,当在一个函数的运行期间调用另一个函数时,在运行被调用函数之前,系统需先完成3件事:

将所有的实参、返回地址等信息传递给被调用函数保存;

为被调用函数的局部变量分配存储区;

将控制转移到被调用函数入口。

而从被调用函数返回调用函数之前,系统也应完成3件工作:

保存被调函数的计算结果;

释放被调函数的数据区;

依照被调函数保存的返回地址将控制转移到调用函数

当有多个函数构成嵌套调用时,按照“后调用先返回”的原则,上述函数之间的信息传递和控制转移必须通过“栈”来实现,即系统将整个程序运行时所需的数据空间安排在一个栈中,每当调用一个函数时,就为它在栈顶分配一个存储区,每当从一个函数推出时,就释放它的存储区,则当前正运行的函数的数据区必在栈顶。

解读:在函数运行期间调用另外一个函数时,系统需要先完成3件事中,返回地址指的是调用其它函数时,当前代码执行完成后,下一句要执行的代码的地址,比如:

public class ComputeFactorial {
    // get n's factorial
    public static int count(int n) {
        if (n == 1)
            return 1;
        int val = count(n - 1);
        return val * n;
    }
}

上面的例子是求n的阶乘例子,在int val=count(n-1);这一行中进行了递归调用,在计算机中,我们调用自己的函数跟调用别的函数没有区别。而上面所说的地址信息指的是 return val*n; 这一行代码的地址信息,把这个地址信息传递给被调用函数的目的是为了保持调用的连续性,即当被调用函数执行完之后,可以直接返回下一步要执行的代码,使程序继续执行下去。

递归调用函数

想要了解怎样“自己”调用“自己”首先要知道怎样调用别的函数:

public class CallOthers {

    // call others
    public static void a() {
        System.out.println("aaaaaaaaa");
        b();
    }

    public static void b() {
        System.out.println("bbbbbbbbbb");
    }
}

上面的代码中,是a函数调用b的过程,运行的结果是输出a和b各一行。而调用“自己”就跟上面的直接调用中所示代码一样,直接在函数体中调用“自己”,但是递归调用要注意函数的出口,否则会抛异常。

下面通过几个例子来强化对递归的理解:

求阶乘、求和

在刚刚学习程序设计语言的时候,我们都会接触到循环,其中求阶乘和求和是最常见的例子,下面我们也用这两个例子

求n的阶乘:

思路:

1、在学习循环的时候,要求一个数的阶乘,就是把前面几个数的积乘以最后一个数,在递归中,也是这个道理,不过,递归强调的是把一个问题分解成好几个相类似的小问题,通过解决小问题从而解决大问题。

2、要求n的阶乘,首先要把前面n-1的阶乘的结果得到,然后再跟n相乘,就是n的阶乘,要得到n-1的阶乘,就要先得到(n-1)-1的阶乘,同理,要求(n-1)-1的阶乘,就要先得到(n-2)-1的阶乘。知道要求的阶乘为1时,暂停嵌套调用。

3、因为1的阶乘就是1,这时候,最先要得到的数的阶乘已经得到了(1的阶乘是1),然后再往后面推回来,即1的阶乘乘以2,再用2的阶乘乘以3,一直到n-1的阶乘,然后再跟n相乘,得到n的阶乘。

代码实现:

public class ComputeFactorial {
    // get n's factorial
    public static int count(int n) {
        if (n == 1)
            return 1;
        // else get the n-1's factorial
        int val = count(n - 1);

        return val * n;
    }
}

求前n项之和也是同样的道理,只不过把乘号变成加号即可。

汉诺塔问题

问题描述:

汉诺塔(港台:河内塔)是根据一个传说形成的数学问题:

有三根杆子A,B,C。A杆上有N个(N>1)穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至C杆:

每次只能移动一个圆盘;大盘不能叠在小盘上面。`

思路:

  • 由于每次只能移动一个盘子,并且保证大的盘子在小的盘子下面,所以不能把第二根柱子当作栈来使用。

  • 这个问题的解决,可以把它分解成两部分(把三根柱子分别称为a,b,c,默认所有盘子开始在a柱子中,并且从上到下编号1……n,并且下面盘子比上面大):

    • 只有1个盘子,直接把这个盘子从a直接移动到c即可。

    • 有n(n>1)个盘子,最重要的是得到最大的盘子,即把前面n-1个盘子从a柱子借助柱子c移动到柱子b。

    • 把a中剩下的最大的盘子直接移动到柱子c中

    • 把柱子b中n-1个盘子借助柱子a移动到柱子c中.

    • 其中在解决前面的n-1个盘子时,依赖于前面n-2个盘子的解决。

图解:用3个盘子做例子

实现代码:

public class HanNuoTa {
    public static void hnt(int n, char a, char b, char c) {
        //在这里先不考虑n<=0的情况
        if (n == 1) {
            System.out.println("直接把" + n + "从" + a + "移动到" + c);
        } else {
            // 首先把n-1个盘子从a借助c移动到b
            hnt((n - 1), a, c, b);
            // 把a中最后一个盘子移动到c
            System.out.println("直接把" + n + "从" + a + "移动到" + c);
            // 然后把b中n-1个盘子从b借助a移动到c
            hnt((n - 1), b, a, c);
        }
    }

}

测试代码:

package com.xinpaninjava.recursion;

public class HanNuoTaTest {

    public static void main(String[] args) {
        char a = 'A';
        char b = 'B';
        char c = 'C';

        HanNuoTa.hnt(3, a, b, c);
    }

}

运行结果:

总结

使用递归的条件

1、为递归调用设置一个出口,避免无限递归而导致的栈溢出

2、使用递归时,确保问题的规模是逐渐变小,比如以上求和,求阶乘,汉诺塔问题,都是通过解决前面的n-1的问题来解决后面的n的问题

递归与循环的比较

  • 递归:

    • 缺点:递归由栈实现,并且递归涉及到调用函数,如上面函数调用函数要完成的几件事中可以看到,递归调用需要消耗空间,即函数调用与调用完成的压栈弹栈,为函数分配存储空间,由于上面步骤而导致的程序运行速度变慢

    • 优点:使用递归写的代码比较容易理解,能用循环实现的都可以用递归实现

  • 循环:

    • 缺点:能用递归实现的,用循环实现可能很复杂,比如树和图的遍历及相关的运算

    • 优点:消耗存储空间小,速度快

【全文完】

参考资料:

汉诺塔-维基百科

C语言版数据结构(第二版)p57 3.3.2 递归过程与递归工作栈

郝斌数据结构-第50-58个视频,提取密码:hvzr


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值