张银峰的编程课堂

递归函数

小时候我与小伙伴没有故事可讲时会讲一个故事:从前有座山,山里有座庙,庙里住着一个老和尚和一个小和尚,老和尚给小老和尚讲了个故事...,从前有座山,山里有座庙,... 老和尚给小老和尚讲了个故事... 这种故事套故事的手法,在编程语言中称为递归,在C中表现为函数调用其自身。如果没有终止递归调用的条件,函数会持续递归下去,直到栈空间溢出。

认识递归

递归可以用迭代实现,但递归有时可能很简单的表达语义。阶乘运算是理解递归一个很好的例子:它可以用递归很好的表达语义;也很容易翻译成迭代版本。

#include <stdio.h>

int factorial(int n)
{
    // 终止条件
    if (n == 0 || n == 1)
    {
        return 1;
    }

    // 递归语义表达清晰:n!=n*(n-1)!
    return n * factorial(n - 1);
}

int main()
{
    printf("factorial(5): %d\n", factorial(5));
    return 0;
}

这里我们用图例描述一下factorial(3)的过程,其中红色表示调用过程及实参,绿色部分表示返回过程与值。

glimix.com

斐波那契数列

使用递归实现的斐波那契数列是一个双重递归,该数列的一个特点是,从第3项开始,当前项等于前两项之和。如:

0   1   1   2   3   5   8 ...

下面的程序输出了斐波那契数列的前10项。

#include <stdio.h>

int fibonacci(int i)
{
    if (i == 0)
        return 0;

    if (i == 1)
        return 1;

    return fibonacci(i - 1) + fibonacci(i - 2);
}

int main()
{
    printf("fibonacci(10): ");

    for (int i = 0; i < 10; i++)
    {
        printf("%d ", fibonacci2(i));
    }
}

下面是迭代方式实现的版本,语义上不如递归表达直接。

int fibonacci(int i)
{
    if (i == 0)
        return 0;

    if (i == 1)
        return 1;

    int i0 = 0;
    int i1 = 1;
    int i2 = 0;

    for (int k = 2; k <= i; k++)
    {
        i2 = i0 + i1;   // 当前项 = 前前项 + 前一项
        i0 = i1;        // 前前项 移动到 前一项
        i1 = i2;        // 前一项 现在是 当前项
    }

    return i2;
}

正如开头的故事那样,如果没有结束条件,故事将永远下去。在程序中,这显然不是我们所希望的。无限的函数嵌套调用,类似于程序限入死循环模式,最终因为嵌套太深,程序堆栈出错。因此,递归需要一个明确的结束条件,在斐波那契数列中,就是前两项不会再调用自身,而是返回一个明确值。

处理语句前后影响

在编写递归程序时,必须小心的处理将要执行的语句顺序,对于层次性结构,有广度优先与深度优先两种方式。广度优先就是:先处理本层数据,再递归处理下一层。

#include <stdio.h>

void print_info(int count, const char *info)
{
    if (count > 0)
    {
        printf("#%d %s\n", count, info);
        print_info(count - 1, info);
    }

    printf("#%d\n", count);
}

int main()
{
    print_info(3, "glimix.com");
    return 0;
}

glimix.com

而深度优先则是抵达底层并处理该层数据,然后回退处理上层。

#include <stdio.h>

void print_info(int count, const char *info)
{
    if (count > 0)
    {
        print_info(count - 1, info);
        printf("#%d %s\n", count, info);
    }

    printf("#%d\n", count);
}

int main()
{
    print_info(3, "cs.glimix.com");
    return 0;
}

glimix.com

递归与函数参数

函数被递归调用时,每一次的递归调用,都有属于本次函数堆栈的变量拷贝。

#include <stdio.h>

void n_tabs(int n)
{
    for (int i = 1; i < n; i++)
        printf("\t");
}

void up_down(int n)
{
    n_tabs(n);
    printf("Layer %d: n location %p\n", n, &n);

    if (n < 4)
        up_down(n + 1);

    n_tabs(n);
    printf("LAYER %d: n location %p\n", n, &n);
}

int main()
{
    up_down(1);
    return 0;
}

glimix.com

例子:通过递归求数组元素的最大值

#include <stdio.h>

int maximum(int *head, int *tail)
{
    if (head == tail)
    {
        return *head;
    }
    else
    {
        if (*head > *tail)
            return maximum(head, tail - 1);
        else
            return maximum(head + 1, tail);
    }
}

int main()
{
    int a[] = {  2, 4, 9, 6, 3, 5, 8 };
    int b[] = { 12, 4, 9, 6, 3, 5, 8 };
    int c[] = {  2, 4, 9, 6, 3, 5, 18};

    printf("%d\n", maximum(a, a + 6));
    printf("%d\n", maximum(b, b + 6));
    printf("%d\n", maximum(c, c + 6));

    return 0;
}

初始时,函数maximum()中的head指向数组的第一个元素,tail则指向最后一个,程序利用两两比较的方式if (head > tail)来选择下次的比较元素,最终选出最大值。当然,这不是求最大值的好方法,只是用于教学性质的递归演示。

glimix.com