10. 函数的递归调用

一、什么是递归

  C 允许函数调用自己,这种调用过程称为递归(recursion)。也就是说,每个函数都可以直接或间接调用自己。所谓的间接调用,是指在递归函数调用的下层函数中再调用自己。

  递归之所以能实现,是因为函数的每次执行过程在栈中都有自己的形式参数和局部变量的副本,这些副本和该函数的其它执行过程不发生关系。假定某个调用函数调用了一个被调用函数,再假定被调用函数又反过来调用了调用函数,那么第二个调用就称为调用函数的递归,因为它发生再调用函数的当前执行过程运行完毕之前。而且,因为原先的调用函数、现在的被调函数在栈中较低的位置有它独立一组参数和自变量,原先的参数和变量将不受任何影响,所以递归调用能正常工作。

二、演示递归

  下面的程序中的 main() 函数调用 up_and_down() 函数,这种调用称为 “第 1 级递归”。然后 up_and_down() 调用自己,这次调用称为 “第 2 级递归”。接着第 2 级调用第 3 级递归,依次类推。该程序示例共有 4 级递归。

#include <stdio.h>

void up_and_down(int n);

int main(void)
{
    up_and_down(1);

    return 0;
}

void up_and_down(int n)
{
    printf("Level %d n location %p\n", n, &n);

    if(n < 4)
        up_and_down(n + 1);
  
    printf("LEVEL %d: n location %p\n", n, &n);
}

  首先,main() 调用了带参数 1 的 up_and_down() 函数,执行结果是 up_and_down() 中的形式参数 n 的值是 1,所以打印 Level 1。 然后,由于 n 小于 4,up_and_down()(第 1 级)调用实际参数为 n + 1(或 2)的 up_and_down()(第 2 级)。于是第 2 级调用的 n 的值是 2,打印 Level 2。于此类推,下面两次调用打印分别是 Level 3 和 Level 4。

  当执行到第 4 级时,n 的值是 4,所以 if 测试条件为假。up_and_down() 函数不再调用自己。第 4 级调用语句接着执行打印 LEVEL 4。因为 n 的值是 4。此时,第 4 级调用结束,控制被传回它的主调函数(即第 3 级调用)。在第 3 级调用中,执行的最后一条语句是调用 if 语句中的第 4 级调用。被调函数(第 4 级调用)把控制返回在这个位置。因此,第 3 级调用继续执行后面的代码,打印 LEVEL 3。然后第 3 级调用结束,控制被传回第 2 级调用,接着打印 LEVEL 2,依次类推。

注意:每次递归的变量 n 都属于本级递归私有。

三、递归的必要条件

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

五、递归的基本原理

  1. 每级函数调用都有自己的变量;
  2. 每次函数调用都会返回一次。当函数执行完毕后,控制权将被传回上一级递归。程序必须按顺序逐级返回递归;
  3. 递归函数中位于递归调用之前的语句,均按被调函数的顺序执行;
  4. 递归函数中位于递归调用之后的语句,均按被调回函数相反的顺序执行;
  5. 虽然每级递归都有自己的变量,但是并没有拷贝函数的代码。程序按顺序执行函数中的代码,而递归调用就是相当于又从头开始执行函数的代码。除了为每次递归调用创建变量外,递归调用非常类似于一个循环语句。
  6. 最后,递归函数必须包含让递归调用停止的语句。

六、尾递归

  最简单的递归形式是把递归调用置于函数的末尾,即正好在 return 语句之前。这种形式的递归被称为 尾递归(tail recursion)。因为递归调用在函数的末尾,因此它相当于循环。

  以下的程序是使用递归的方式实现计算一个数的阶乘:

#include <stdio.h>

int factorial(int num);

int main(void)
{
    int num = 5;
    int result = 0;

    result = factorial(5);
    printf("%d的阶乘为%d\n",num,result);

    return 0;
}

int factorial(int num)
{
    if(num == 1)
    {
        return 1;
    } else {
        return num * factorial(num-1);
    }
}

  以下的程序是使用循环的方式实现一个数的阶乘:

#include <stdio.h>

int factorial(int num);

int main(void)
{
    int num = 5;
    int result = 0;

    result = factorial(5);
    printf("%d的阶乘为%d\n",num,result);

    return 0;
}

int factorial(int num)
{
    int result = 1;
    int i = 1;

    for(i = 1; i <= num; i++)
    {
        result *= i;
    }

    return result;
}

  既然用递归和循环解决问题都没有问题,那么到底应该使用那个?

  一般而言,选择循环比较好。首先,每次递归都会创建一组变量,所以递归的使用内存更多,而且每次递归调用都会把创建的一组新变量放在栈中。递归调用的数量受限于内存空间。其次,由于每次函数调用要花费一定的时间,所以递归的执行速度较慢。

七、递归和倒序计算

  递归在处理倒序时十分方便。假设我们要解决一个问题:编写一个函数,打印一个整数的二进制数。二进制表示法根据 2 的幂来表示数字。例如,十进制数 234 实际上是 \(%\)\(2*10^{2} + 3 * 10 ^{1} + 4 * 10^{0}\),所以二进制数 101 是 \(1 * 2^{2} + 0 * 2^{1} + 1 * 2^{0}\)。二进制数由 0 和 1 表示。

  那我们如何用二进制表示十进制数呢?在二进制中,奇数的末尾一定是 1 ,偶数的末尾一定是 0。所以我们可以通过取余 2 的方法确定对应二进制的数的最后一位是 1 还是 0。一般而言,对于数字 n,其二进制的最后一位 n % 2。要想获得下一位数字,必须把原数除以 2。这种计算方式相当于在十进制下把小数点左移一位,如果计算结果是偶数,那么二进制的下一位就是 0;如果是 奇数,就是 1;当与 2 相除的结果小于 2 时停止计算,因为只要结果大于或等于 2,就说明还有二进制位。每次除 2 就相当于去掉一位二进制,直到计算出最后一位为止。

#include <stdio.h>

void binary(int n);

int main(void)
{
    int num = 32;
  
    binary(num);
  
    return 0;
}

void binary(int num)
{
    int n;

    n = num % 2;
    if( num >= 2)
        binary(num / 2);
    putchar(n ? '1' : '0');
}

八、递归的优缺点

  递归既有优点也有缺点。优点是递归为某些编程问题提供了最简单的解决方案。缺点是一些递归算法会快速消耗计算机的内存资源。另外,递归不方便阅读和维护。

posted @ 2023-03-03 17:35  星光樱梦  阅读(244)  评论(0编辑  收藏  举报