目录
[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. 递归和迭代)
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;
}
总结
-
青蛙跳台阶问题的核心是递推公式 :
f(n) = f(n-1) + f(n-2)(初始值 f (1)=1、f (2)=2); -
递归版代码简洁易理解,但存在大量重复计算,仅适合小数值场景;
-
迭代版通过循环逐步计算,无冗余开销,是工程上的最优选择
汉诺塔问题
问题描述
有 3 根柱子(通常标记为 A、B、C),n 个大小不同的盘子从小到大叠在 A 柱上。要求把所有盘子从 A 柱移到 C 柱,规则是:
-
每次只能移动1 个盘子;
-
任何时候,大盘子不能压在小盘子上面;
-
可以借助 B 柱作为中转
递归解题思路(大事化小)
汉诺塔的核心是把复杂问题拆解为简单子问题:
-
终止条件:如果只有 1 个盘子(n=1),直接把它从 A 移到 C 即可;
-
递归逻辑:
-
先把 A 柱上的
n-1个盘子,借助 C 柱中转,移到 B 柱; -
把 A 柱剩下的最后 1 个(最大的)盘子,直接移到 C 柱;
-
再把 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;
}
代码关键解释
- 函数参数:
-
`n`:当前要移动的盘子总数;
-
`from`:源柱子(盘子当前所在的柱子);
-
`to`:目标柱子(盘子要移到的柱子);
-
`temp`:中转柱子(辅助移动的柱子)。
- 核心递归逻辑:
-
先处理`n-1`个盘子的移动(拆解问题),再处理最大的盘子,最后处理剩下的`n-1`个盘子;
-
递归的每一层都会把问题规模缩小 1,直到`n=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
总结
-
汉诺塔的核心是递归拆解 :把
n个盘子的问题拆解为n-1个盘子的子问题,直到规模缩小到 1(终止条件); -
递归函数的关键是明确源柱、目标柱、中转柱的角色转换(每一层递归中,三个柱子的角色会变化);
-
汉诺塔的最少移动步数固定为
2^n - 1,n 越大,步数呈指数级增长(比如 n=10 时需要 1023 步)。