C 语言 第九讲:函数递归

目录

1.递归是什么?

[1.1 递归的思想:](#1.1 递归的思想:)

[1.2 递归的限制条件](#1.2 递归的限制条件)

[2. 递归举例](#2. 递归举例)

[2.1 举例1: 求n的阶乘](#2.1 举例1: 求n的阶乘)

[2.1.1 分析和代码实现](#2.1.1 分析和代码实现)

[2.1.2 画图推演](#2.1.2 画图推演)

[2.2 举例2:顺序打印一个整数的每一位](#2.2 举例2:顺序打印一个整数的每一位)

[2.2.1 分析和代码实现](#2.2.1 分析和代码实现)

[2.2.2 画图推演](#2.2.2 画图推演)

[3. 递归和迭代](#3. 递归和迭代)

4.举例3:求第n个斐波那契数

5.拓展学习

青蛙跳台阶问题

问题描述

核心逻辑

1.递归版本(易理解但效率低)

2.迭代版本(高效无重复计算)

总结

汉诺塔问题

问题描述

递归解题思路(大事化小)

递归实现

代码关键解释

总结


1.什么是递归

2.递归的限制条件

3.递归的举例

4.递归与迭代

1.递归是什么?

递归是学习 C 语言函数绕不开的一个话题,那什么是递归呢?

递归其实是一种解决问题的方法,在 C 语言中,递归就是函数自己调用自己

写一个史上最简单的 C 语言递归代码:

cs 复制代码
#include <stdio.h>

int main()
{
    printf("hehe\n");
    main();//main函数中又调用了main函数
    return 0;
}

1.1 递归的思想:

把一个大型复杂问题层层转化为一个与原问题相似,但规模较小的子问题来求解;直到子问题不能再被拆分,递归就结束了。所以递归的思考方式就是把大事化小的过程。

递归中的递就是递推 的意思,归就是回归的意思,接下来慢慢来体会。

1.2 递归的限制条件

提取的文字内容: 递归在书写的时候,有2个必要条件:

  • 递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。

  • 每次递归调用之后越来越接近这个限制条件。

    在下面的例子中,我们逐步体会这2个限制条件。

2. 递归举例

2.1 举例1: 求n的阶乘

一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1.

自然数n的阶乘写作n!

题目:计算n的阶乘(不考虑溢出),n的阶乘就是1~n的数字累计相乘。

2.1.1 分析和代码实现

n的阶乘的公式:n! = n * (n - 1)!

举例:

5! = 5*4*3*2*1

4! = 4*3*2*1

所以5!= 5*4!

从这个公式不难看出:如何把一个较大的问题,转换为一个与原问题相似,但规模较小的问题来求解。

n 的阶乘和 n-1 的阶乘是相似的问题,但是规模要少了 n。有一种有特殊情况是:当 n==0 的时候,n 的阶乘是 1,而其余 n 的阶乘都是可以通过上面的公式计算。

这样就能写出 n 的阶乘的递归公式如下:

cs 复制代码
// 定义递归函数Fact,用于计算整数n的阶乘
int Fact(int n)
{
    // 递归的限制条件:当n等于0时,0的阶乘结果为1,触发递归终止
    if(n==0)
        return 1;
    // 非终止条件时,执行递归逻辑
    else
        // 递归调用:将n的阶乘拆解为n × (n-1)的阶乘,逐步缩小问题规模
        return n*Fact(n-1);
}

测试:

cs 复制代码
// 引入标准输入输出头文件,用于后续的scanf(输入)和printf(输出)函数
#include <stdio.h>

// 定义递归函数Fact:用于计算整数n的阶乘
int Fact(int n)
{
    // 递归终止条件:0的阶乘是1,满足此条件时递归不再继续
    if(n==0)
        return 1;
    // 非终止条件时,执行递归逻辑
    else
        // 递归调用:将n的阶乘拆解为「n × (n-1)的阶乘」,逐步缩小问题规模
        return n*Fact(n-1);
}

// 程序的入口函数
int main()
{
    // 定义整型变量n,初始化为0,用于存储用户输入的数字
    int n = 0;
    // 通过scanf获取用户输入的整数,存入变量n中
    scanf("%d", &n);
    // 调用Fact函数计算n的阶乘,结果存入变量ret
    int ret = Fact(n);
    // 输出阶乘的计算结果
    printf("%d\n", ret);
    // 程序正常结束,返回0
    return 0;
}

运行结果(这里不太考虑n太大的情况,n太大存在溢出):

2.1.2 画图推演

2.2 举例2:顺序打印一个整数的每一位

输入一个整数m,按照顺序打印整数的每一位

比如:

输入:1234 输出: 1 2 3 4

输入: 520 输出: 5 2 0

2.2.1 分析和代码实现

这个题目,放在我们面前,首先想到的是,怎么得到这个数的每一位呢?

如果 n 是一位数,n 的每一位就是 n 自己n 是超过 1 位数的话,就得拆分每一位

1234%10 就能得到 4,然后 1234/10 得到 123,这就相当于去掉了 4

然后继续对 123%10,就得到了 3,再除 10 去掉 3,以此类推

不断的 %10 和 / 10 操作,直到 1234 的每一位都得到;

但是这里有个问题就是得到的数字顺序是倒着

但是我们有了灵感,我们发现其实一个数字的最低位是最容易得到的,通过 %10 就能得到那我们假设想写一个函数 Print 来打印 n 的每一位,如下表示:

// 定义用于打印数字n每一位的函数Print

1 Print(n)

// 以数字1234为例,演示函数的作用

2 如果n是1234,那表示为

// 调用Print函数,目标是打印1234的每一位

3 Print(1234) //打印1234的每一位

4

// 分析:1234的最低位4可通过%10运算得到,因此拆分Print(1234)的逻辑

5 其中1234中的4可以通过%10得到,那么

6 Print(1234)就可以拆分为两步:

// 步骤1:递归调用Print,处理去掉最低位后的数字123(即1234/10)

7 1. Print(1234/10) //打印123的每一位

// 步骤2:打印1234的最低位4(即1234%10)

8 2. printf(1234%10) //打印4

// 说明:完成上述两步后,即可实现1234每一位的打印

9 完成上述2步,那就完成了1234每一位的打印

10

// 延续递归逻辑:Print(123)同样拆分为处理12(123/10)+打印3(123%10)

11 那么Print(123)又可以拆分为Print(123/10) + printf(123%10)

以此类推下去,就有

// 初始调用Print函数,目标是打印数字1234的每一位

1 Print(1234)

// 递归拆分:将Print(1234)分解为「处理去掉最低位的123」+「打印最低位4」

2 ==>Print(123) + printf(4)

// 继续递归拆分Print(123):分解为「处理去掉最低位的12」+「打印当前最低位3」

3 ==>Print(12) + printf(3)

// 继续递归拆分Print(12):分解为「处理去掉最低位的1」+「打印当前最低位2」

4 ==>Print(1) + printf(2)

// 递归终止:Print(1)是一位数,直接打印最低位1

5 ==>printf(1)

直到被打印的数字变成一位数的时候,就不需要再拆分,递归结束。

那么代码完成也就比较清楚:

cs 复制代码
// 定义Print函数:递归实现打印整数n的每一位
void Print(int n)
{
    // 判断n是否为多位数(大于9则至少是两位数)
    if(n>9)
    {
        // 递归调用Print:处理"去掉n最后一位后的数",逐步缩小问题规模
        Print(n/10);
    }
    // 打印n的最后一位(通过%10取余),递归回溯时会按"高位→低位"顺序输出
    printf("%d ", n%10);
}

// 主函数:程序的入口
int main()
{
    // 定义整型变量m,初始化为0,用于存储用户输入的数字
    int m = 0;
    // 通过scanf获取用户输入的整数,存入变量m中
    scanf("%d", &m);
    // 调用Print函数,打印m的每一位
    Print(m);
    // 程序正常结束,返回0
    return 0;
}

输入输出结果:

在这个解题的过程中,我们就是使用了大事化小的思路把

Print (1234) 打印 1234 每一位,拆解为首先 Print (123) 打印 123 的每一位,再打印得到的 4

把 Print (123) 打印 123 每一位,拆解为首先 Print (12) 打印 12 的每一位,再打印得到的 3直到 Print 打印的是一位数,直接打印就行。

2.2.2 画图推演

3. 递归和迭代

递归是一种很好的编程技巧,但是和很多技巧一样,也是可能被误用的,就像举例 1 一样,看到推导的公式,很容易就被写成递归的形式:

cs 复制代码
// 定义递归函数Fact,用于计算整数n的阶乘
int Fact(int n)
{
    // 递归终止条件:0的阶乘结果为1,满足此条件时递归停止
    if(n==0)
        return 1;
    // 非终止条件时,执行递归逻辑
    else
        // 递归调用:将n的阶乘拆解为「n × (n-1)的阶乘」,逐步缩小问题规模
        return n*Fact(n-1);
}

Fact 函数是可以产生正确的结果,但是在递归函数调用的过程中涉及一些运行时的开销。

在 C 语言中每一次函数调用,都需要为本次函数调用在内存的栈区,申请一块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧。

函数不返回,函数对应的栈帧空间就一直占用,所以如果函数调用中存在递归调用的话,每一次递归函数调用都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。

所以如果采用函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stack overflow)的问题。

所以如果不想使用递归,就得想其他的办法,通常就是迭代的方式(通常就是循环的方式)。比如:计算 n 的阶乘,也是可以产生 1~n 的数字累计乘在一起的

cs 复制代码
// 定义计算n阶乘的迭代(循环)实现函数
int Fact(int n)
{
    // 定义循环计数器i
    int i = 0;
    // 定义阶乘结果的累加变量,初始化为1(乘法的单位元)
    int ret = 1;
    // 循环范围:从1遍历到n,逐步累积乘积
    for(i = 1; i <= n; i++)
    {
        // 每次迭代将当前数字i乘入结果ret
        ret *= i;
    }
    // 返回最终计算得到的阶乘结果
    return ret;
}

上述代码是能够完成任务,并且效率是比递归的方式更好的。

事实上,我们看到的许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更加清晰,但是这些问题的迭代实现往往比递归实现效率更高。

当一个问题非常复杂,难以使用迭代的方式实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销

4.举例3:求第n个斐波那契数

我们也能举出更加极端的例子,就像计算第 n 个斐波那契数,是不适合使用递归求解的,但是斐波那契数的问题通过是使用递归的形式描述的,如下:

看到这公式,很容易诱导我们将代码写成递归的形式,如下所示:

cs 复制代码
// 定义递归函数Fib,用于计算第n个斐波那契数
int Fib(int n)
{
    // 递归终止条件:斐波那契数的第1、2项结果均为1
    if(n <= 2)
        return 1;
    // 非终止条件时,执行递归逻辑
    else
        // 递归调用:第n个斐波那契数 = 第n-1项 + 第n-2项(斐波那契数的递归定义)
        return Fib(n-1) + Fib(n-2);
}

测试代码:

cs 复制代码
// 引入标准输入输出头文件,支持scanf(输入)和printf(输出)函数
#include <stdio.h>

// 程序的入口函数
int main()
{
    // 定义整型变量n,初始化为0,用于存储用户要计算的斐波那契数的项数
    int n = 0;
    // 通过scanf获取用户输入的项数,存入变量n中
    scanf("%d", &n);
    // 调用Fib函数计算第n个斐波那契数,结果存入变量ret
    int ret = Fib(n);
    // 输出计算得到的斐波那契数结果
    printf("%d\n", ret);
    // 程序正常结束,返回0
    return 0;
}

当我们 n 输入为 50 的时候,需要很长时间才能算出结果,这个计算所花费的时间,是我们很难接受的,这也说明递归的写法是非常低效的,那是为什么呢?

其实递归程序会不断的展开,在展开的过程中,我们很容易就能发现,在递归的过程中会有重复计算,而且递归层次越深,冗余计算就会越多。我们可以作个测试:

cs 复制代码
// 引入标准输入输出头文件,支持scanf(输入)和printf(输出)函数
#include <stdio.h>

// 定义全局变量count,用于统计第3个斐波那契数被计算的次数
int count = 0;

// 定义递归函数Fib,计算第n个斐波那契数,同时统计第3项的计算次数
int Fib(int n)
{
    // 当计算的是第3个斐波那契数时,count自增(统计其被计算的次数)
    if(n == 3)
        count++;//统计第3个斐波那契数被计算的次数
    
    // 递归终止条件:斐波那契数的第1、2项结果均为1
    if(n<=2)
        return 1;
    // 非终止条件时,执行递归逻辑:第n项 = 第n-1项 + 第n-2项
    else
        return Fib(n-1)+Fib(n-2);
}

// 程序的入口函数
int main()
{
    // 定义整型变量n,初始化为0,用于存储用户要计算的斐波那契数的项数
    int n = 0;
    // 通过scanf获取用户输入的项数,存入变量n中
    scanf("%d", &n);
    // 调用Fib函数计算第n个斐波那契数,结果存入变量ret
    int ret = Fib(n);
    // 打印计算得到的斐波那契数结果
    printf("%d\n", ret);
    // 打印第3个斐波那契数被计算的总次数
    printf("\ncount = %d\n", count);
    // 程序正常结束,返回0
    return 0;
}

测试结果:

这里我们看到了,在计算第 40 个斐波那契数的时候,使用递归方式,第 3 个斐波那契数就被重复计算了 39088169 次,这些计算是非常冗余的。所以斐波那契数的计算,使用递归是非常不明智的,我们就得想迭代的方式解决。

我们知道斐波那契数的前 2 个数都 1,然后前 2 个数相加就是第 3 个数,那么我们从前往后,从小到大计算就行了。这样就有下面的代码:

cs 复制代码
// 定义迭代函数Fib,高效计算第n个斐波那契数
int Fib(int n)
{
    // 初始化:a代表斐波那契数的第n-2项,初始为第1项(值为1)
    int a = 1;
    // 初始化:b代表斐波那契数的第n-1项,初始为第2项(值为1)
    int b = 1;
    // 初始化:c代表当前要计算的斐波那契数项,初始为1(覆盖前2项的情况)
    int c = 1;
    // 循环条件:当n大于2时(前2项已初始化,无需计算)
    while(n>2)
    {
        // 计算当前项c:第n项 = 第n-1项(b) + 第n-2项(a)
        c = a+b;
        // 更新a为原来的b(下一轮的第n-2项)
        a = b;
        // 更新b为原来的c(下一轮的第n-1项)
        b = c;
        // n减1,逐步逼近"前2项"的终止条件
        n--;
    }
    // 返回计算得到的第n个斐波那契数
    return c;
}

迭代的方法去实现这个代码,效率就要高出很多了。

有时候,递归虽好,但是也会引入一些问题,所以我们一定不要迷恋递归,适可而止就好。

5.拓展学习

青蛙跳台阶问题

问题描述

一只青蛙一次可以跳 1 级台阶,也可以跳 2 级台阶。求该青蛙跳上 n 级台阶总共有多少种跳法。

核心逻辑

问题描述:一只青蛙一次可以跳 1 级台阶,也可以跳 2 级台阶。求该青蛙跳上 n 级台阶总共有多少种跳法。核心推导:

  • n=1 时,只有 1 种跳法(直接跳 1 级);

  • n=2 时,有 2 种跳法(1+1 或 直接跳 2 级);

  • n>2 时,跳上 n 级的最后一步要么从 n-1 级跳 1 级,要么从 n-2 级跳 2 级 → `f(n) = f(n-1) + f(n-2)`(逻辑和斐波那契数一致,仅初始值不同)。

1.递归版本(易理解但效率低)

cs 复制代码
#include <stdio.h>

// 递归计算青蛙跳n级台阶的跳法数
int jumpFloor(int n)
{
    // 终止条件1:n=1时,只有1种跳法
    if (n == 1)
        return 1;
    // 终止条件2:n=2时,有2种跳法
    else if (n == 2)
        return 2;
    // 递归逻辑:n>2时,f(n) = f(n-1) + f(n-2)
    else
        return jumpFloor(n - 1) + jumpFloor(n - 2);
}

int main()
{
    int n; // 存储用户输入的台阶数
    printf("请输入台阶数n:");
    scanf("%d", &n);
    
    // 校验输入(台阶数需为正整数)
    if (n < 1)
    {
        printf("输入错误!台阶数必须是正整数。\n");
        return 1;
    }
    
    int result = jumpFloor(n);
    printf("跳上%d级台阶共有%d种跳法\n", n, result);
    return 0;
}

2.迭代版本(高效无重复计算)

cs 复制代码
#include <stdio.h>

// 迭代计算青蛙跳n级台阶的跳法数(无冗余计算,效率高)
int jumpFloor(int n)
{
    // 处理边界条件:n=1或n=2时直接返回结果
    if (n == 1) return 1;
    if (n == 2) return 2;
    
    // 初始化:a=第n-2级的跳法数(初始为n=1的情况),b=第n-1级的跳法数(初始为n=2的情况)
    int a = 1, b = 2;
    int c = 0; // c存储当前n级的跳法数
    
    // 从n=3开始迭代计算,直到目标n级
    while (n > 2)
    {
        c = a + b;  // 当前级跳法数 = 前两级跳法数之和
        a = b;      // 前两级指针后移:a变为原来的b(下一轮的n-2级)
        b = c;      // 前一级指针后移:b变为当前的c(下一轮的n-1级)
        n--;        // n减1,逐步逼近终止条件
    }
    return c;
}

int main()
{
    int n;
    printf("请输入台阶数n:");
    scanf("%d", &n);
    
    if (n < 1)
    {
        printf("输入错误!台阶数必须是正整数。\n");
        return 1;
    }
    
    int result = jumpFloor(n);
    printf("跳上%d级台阶共有%d种跳法\n", n, result);
    return 0;
}

总结

  1. 青蛙跳台阶问题的核心是递推公式f(n) = f(n-1) + f(n-2)(初始值 f (1)=1、f (2)=2);

  2. 递归版代码简洁易理解,但存在大量重复计算,仅适合小数值场景;

  3. 迭代版通过循环逐步计算,无冗余开销,是工程上的最优选择

汉诺塔问题

问题描述

有 3 根柱子(通常标记为 A、B、C),n 个大小不同的盘子从小到大叠在 A 柱上。要求把所有盘子从 A 柱移到 C 柱,规则是:

  1. 每次只能移动1 个盘子;

  2. 任何时候,大盘子不能压在小盘子上面

  3. 可以借助 B 柱作为中转

递归解题思路(大事化小)

汉诺塔的核心是把复杂问题拆解为简单子问题:

  • 终止条件:如果只有 1 个盘子(n=1),直接把它从 A 移到 C 即可;

  • 递归逻辑:

    1. 先把 A 柱上的n-1个盘子,借助 C 柱中转,移到 B 柱;

    2. 把 A 柱剩下的最后 1 个(最大的)盘子,直接移到 C 柱;

    3. 再把 B 柱上的n-1个盘子,借助 A 柱中转,移到 C 柱。

递归实现

cs 复制代码
#include <stdio.h>

// 汉诺塔递归函数
// 参数说明:
// n:要移动的盘子数量
// from:盘子当前所在的柱子(源柱)
// to:盘子要移动到的目标柱子(目标柱)
// temp:中转柱子
void hanoi(int n, char from, char to, char temp)
{
    // 递归终止条件:只有1个盘子时,直接从源柱移到目标柱
    if (n == 1)
    {
        printf("将盘子 %d 从 %c 移到 %c\n", n, from, to);
        return; // 终止递归
    }

    // 第一步:把n-1个盘子从from移到temp(借助to中转)
    hanoi(n - 1, from, temp, to);

    // 第二步:把第n个(最大的)盘子从from移到to
    printf("将盘子 %d 从 %c 移到 %c\n", n, from, to);

    // 第三步:把n-1个盘子从temp移到to(借助from中转)
    hanoi(n - 1, temp, to, from);
}

int main()
{
    int n; // 盘子的数量
    printf("请输入汉诺塔的盘子数量:");
    scanf("%d", &n);

    // 校验输入:盘子数量需为正整数
    if (n < 1)
    {
        printf("输入错误!盘子数量必须是正整数。\n");
        return 1;
    }

    // 调用汉诺塔函数:n个盘子从A柱移到C柱,借助B柱中转
    printf("\n汉诺塔移动步骤(共需要 %d 步):\n", (1 << n) - 1); // (2^n -1)是汉诺塔的最少移动步数
    hanoi(n, 'A', 'C', 'B');

    return 0;
}

代码关键解释

  1. 函数参数:
  • `n`:当前要移动的盘子总数;

  • `from`:源柱子(盘子当前所在的柱子);

  • `to`:目标柱子(盘子要移到的柱子);

  • `temp`:中转柱子(辅助移动的柱子)。

  1. 核心递归逻辑:
  • 先处理`n-1`个盘子的移动(拆解问题),再处理最大的盘子,最后处理剩下的`n-1`个盘子;

  • 递归的每一层都会把问题规模缩小 1,直到`n=1`时直接移动。

  1. **移动步数**:汉诺塔的最少移动步数是 `2^n - 1`(代码中用`1 << n`表示 2 的 n 次方,位运算更高效)。

测试用例(输入 n=3)

输入`3`后,程序输出的移动步骤:

请输入汉诺塔的盘子数量:3

汉诺塔移动步骤(共需要 7 步):

将盘子 1 从 A 移到 C

将盘子 2 从 A 移到 B

将盘子 1 从 C 移到 B

将盘子 3 从 A 移到 C

将盘子 1 从 B 移到 A

将盘子 2 从 B 移到 C

将盘子 1 从 A 移到 C

总结

  1. 汉诺塔的核心是递归拆解 :把n个盘子的问题拆解为n-1个盘子的子问题,直到规模缩小到 1(终止条件);

  2. 递归函数的关键是明确源柱、目标柱、中转柱的角色转换(每一层递归中,三个柱子的角色会变化);

  3. 汉诺塔的最少移动步数固定为 2^n - 1,n 越大,步数呈指数级增长(比如 n=10 时需要 1023 步)。

相关推荐
CodeCraft Studio2 小时前
国产化Word处理控件Spire.Doc教程:使用C# 编程方式批量转换Word为RTF
开发语言·c#·word·spire.doc·word文档转换·word开发组件·word api库
yaoh.wang2 小时前
力扣(LeetCode) 119: 杨辉三角 II - 解法思路
数据结构·python·算法·leetcode·面试·职场和发展·跳槽
客梦2 小时前
数据结构--最小生成树
数据结构·笔记
CSDN_RTKLIB2 小时前
【类定义系列一】C++ 头文件 / 源文件分离
开发语言·c++
invicinble2 小时前
arthas
开发语言·python
CoderCodingNo2 小时前
【GESP】C++五级真题(埃氏筛思想考点) luogu-B3929 [GESP202312 五级] 小杨的幸运数
数据结构·c++·算法
机器学习之心2 小时前
基于PSO-GA混合算法的施工进度计划多目标优化,以最小化总成本并实现资源均衡,满足工期约束和资源限制,MATLAB代码
算法·matlab·多目标优化·pso-ga混合算法
bbq粉刷匠2 小时前
Java--二叉树概念及其基础应用
java·数据结构·算法
CodeByV2 小时前
【算法题】前缀和
算法