C语言笔记归纳8:函数递归

函数递归

目录

函数递归

一、什么是递归?(通俗理解:自己调用自己)

1.1 递归的本质:分而治之

1.2 错误示范:无限递归(栈溢出警告!)

1.3 栈溢出的底层原因(必懂!)

1.4 递归的两个核心条件(缺一不可!)

生动类比:俄罗斯套娃

二、递归经典示例(图解执行流程)

示例 1:求 n 的阶乘(递归入门必练)

问题分析

递归实现

执行流程图解(以Fact(4)为例)

核心流程总结

示例 2:顺序打印整数的每一位(理解 “先递后归”)

需求

问题拆解

递归实现(已优化)

执行流程图解(以Print(1234)为例)

核心亮点

三、递归与迭代:该怎么选?

1. 阶乘的迭代实现(修正原代码笔误)

2. 斐波那契数列:暴露递归的致命缺陷

朴素递归实现(低效!)

效率问题:重复计算的 “灾难”

高效迭代实现(修正返回值缺陷)

递归与迭代核心对比表

四、递归的适用场景与避坑指南

1. 何时优先用递归?

2. 何时避免用递归?

3. 递归避坑 3 大要点

五、拓展思考题(练手必备)

📝 总结


✨ 引言:

对于 C 语言学习者来说,递归就像 “编程界的魔术”—— 它能用几行简洁的代码解决复杂问题,却又因 “看不见的调用栈” 让新手望而却步。很多人能看懂递归代码,却想不通它的执行流程;能写出简单递归,却踩坑栈溢出或效率低下。

一、什么是递归?(通俗理解:自己调用自己)

1.1 递归的本质:分而治之

递归是一种 “大事化小” 的解决问题思想 :

将一个复杂的大问题,分解为一个或多个与原问题结构相同但规模更小的子问题,直到子问题小到能直接解决(基准情况),再通过子问题的解反向推导原问题的解。

在 C 语言中,递归的具体表现是:函数直接或间接调用自身

1.2 错误示范:无限递归(栈溢出警告!)

#include <stdio.h>

int main()
{
	printf("hehe\n");
	main(); // 函数调用自身,无终止条件
	return 0;
}

1.3 栈溢出的底层原因(必懂!)

每一次函数调用,系统都会在栈(Stack) 上分配一块 “函数栈帧”,用于存储:

  • 函数的参数;
  • 局部变量;
  • 返回地址(函数执行完后要回到的位置)。

栈的容量是有限的(通常几 MB),上面的代码中,main 函数无限调用自身,栈帧会像 “叠积木” 一样持续累积,直到栈空间被耗尽,触发 栈溢出 (Stack Overflow) 错误,程序直接崩溃。

1.4 递归的两个核心条件(缺一不可!)

✅ 存在基准情况(Base Case)

当问题规模缩小到基准情况时,递归停止,直接返回明确结果(比如 0! = 1);

✅ 递推逼近基准

每次递归调用必须让问题规模 “变小”,朝着基准情况推进(比如 n! 分解为 n * (n-1)!)。

生动类比:俄罗斯套娃

  • 递推:一层一层打开外层套娃,直到找到最里面不能再打开的小娃娃(基准情况);
  • 回归:找到小娃娃后,再一层一层合上套娃,最终回到最初的外层套娃(原问题的解)。

二、递归经典示例(图解执行流程)

示例 1:求 n 的阶乘(递归入门必练)

问题分析
  • 数学定义:n! = n * (n-1) * (n-2) * ... * 1
  • 递归拆解:n! = n * (n-1)!(大问题→小问题);
  • 基准情况:0! = 11! = 1(小问题直接解)。
递归实现
#include <stdio.h>

// 用unsigned int避免负数输入导致无限递归
int Fact(unsigned int n)
{
	if (n == 0) // 基准情况:0! = 1
	{
		return 1;
	}
	else// 递推:n! = n * (n-1)!
	{
		return n * Fact(n - 1);
	}
}

int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fact(n);
	printf("%d\n", ret);

	return 0;
}
执行流程图解(以Fact(4)为例)
Fact(4)
|
|-- 判断 n == 0? 4 != 0
|-- 执行 return 4 * Fact(3)
|   |
|   --> Fact(3)
|       |
|       |-- 判断 n == 0? 3 != 0
|       |-- 执行 return 3 * Fact(2)
|       |   |
|       |   --> Fact(2)
|       |       |
|       |       |-- 判断 n == 0? 2 != 0
|       |       |-- 执行 return 2 * Fact(1)
|       |       |   |
|       |       |   --> Fact(1)
|       |       |       |
|       |       |       |-- 判断 n == 0? 1 != 0
|       |       |       |-- 执行 return 1 * Fact(0)
|       |       |       |   |
|       |       |       |   --> Fact(0)
|       |       |       |       |
|       |       |       |       |-- 判断 n == 0? 0 == 0  <-- 触发基准情况
|       |       |       |       |-- 执行 return 1
|       |       |       |   |
|       |       |       |   <-- 返回 1
|       |       |       |-- 计算 1 * 1 = 1,执行 return 1
|       |       |   |
|       |       |   <-- 返回 1
|       |       |-- 计算 2 * 1 = 2,执行 return 2
|       |   |
|       |   <-- 返回 2
|       |-- 计算 3 * 2 = 6,执行 return 6
|   |
|   <-- 返回 6
|-- 计算 4 * 6 = 24,执行 return 24
核心流程总结
  • 递推阶段:从Fact(4)逐层分解到Fact(0),问题规模不断缩小;
  • 回归阶段:从Fact(0)的结果1逐层反向计算,最终得到Fact(4)=24

示例 2:顺序打印整数的每一位(理解 “先递后归”)

需求

输入一个整数(如1234),按高位到低位顺序打印每一位(输出1 2 3 4)。

问题拆解
  • 要打印1234:先打印123的每一位,再打印41234%10);
  • 要打印123:先打印12的每一位,再打印3123%10);
  • 基准情况:当数字为个位数(n<10)时,直接打印该数字。
递归实现(已优化)
#include <stdio.h>

void Print(int n)// 以1234为例
{
	if (n > 9) // 递推条件:不是个位数,先处理高位
	{
		Print(n / 10); // 1234/10=123,递归处理高位
	}
	// 基准情况+回归操作:打印当前最低位
	printf("%d ", n % 10);
}

int main()
{
	int n = 0;
	scanf("%d", &n);
	Print(n); // 打印n的每一位

	return 0;
}
执行流程图解(以Print(1234)为例)
Print(1234)
|
|-- 判断 n > 9? 1234 > 9 → 是
|-- 执行 Print(1234 / 10) = Print(123)
|   |
|   --> Print(123)
|       |
|       |-- 判断 n > 9? 123 > 9 → 是
|       |-- 执行 Print(123 / 10) = Print(12)
|       |   |
|       |   --> Print(12)
|       |       |
|       |       |-- 判断 n > 9? 12 > 9 → 是
|       |       |-- 执行 Print(12 / 10) = Print(1)
|       |       |   |
|       |       |   --> Print(1)
|       |       |       |
|       |       |       |-- 判断 n > 9? 1 > 9 → 否(触发基准情况)
|       |       |       |-- 执行 printf("%d ", 1 % 10) → 打印 "1 "
|       |       |   |
|       |       |   <-- 返回
|       |       |-- 执行 printf("%d ", 12 % 10) → 打印 "2 "
|       |   |
|       |   <-- 返回
|       |-- 执行 printf("%d ", 123 % 10) → 打印 "3 "
|   |
|   <-- 返回
|-- 执行 printf("%d ", 1234 % 10) → 打印 "4 "
核心亮点

打印操作发生在回归阶段:递推时只分解问题,不执行打印;回归时才逐层打印最低位,最终实现 “高位到低位” 的顺序输出 —— 这正是递归 “先递后归” 的精髓!

三、递归与迭代:该怎么选?

迭代是通过forwhile等循环语句重复执行代码,是与递归并列的 “重复执行” 逻辑。两者各有优劣,需根据场景灵活选择。

1. 阶乘的迭代实现(修正原代码笔误)

#include <stdio.h>

int Fact(int n)
{
	int i = 0;
	int ret = 1;
	for (i = 1; i <= n; i++) // 从1开始累积相乘
	{
		ret *= i;
	}
	return ret;
}

int main()
{
	int n = 0;
	// 原笔误:scanf("%d ,&n");(格式字符串与变量地址未分离)
	scanf("%d", &n); // 修正后:格式字符串与变量地址分开
	int ret = Fact(n);
	printf("%d\n", ret);

	return 0;
}

2. 斐波那契数列:暴露递归的致命缺陷

斐波那契数列定义:F(1)=1F(2)=1n>2F(n)=F(n-1)+F(n-2)。这是递归的经典案例,但朴素递归存在严重的效率问题。

朴素递归实现(低效!)
#include <stdio.h>

int Fib(int n)
{
	if (n <= 2)
	{
		return 1;
	}
	else
	{
		return Fib(n - 1) + Fib(n - 2);
	}
}

// 带计数的版本:展示重复计算问题
int count = 0;
int Fib_count(int n)
{
	if (n == 3) // 统计Fib(3)的调用次数
	{
		count++;
	}
	if (n <= 2)
	{
		return 1;
	}
	else
	{
		return Fib_count(n - 1) + Fib_count(n - 2);
	}
}

int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d\n", ret);

	// 演示重复计算:n=40时,Fib(3)调用次数极多
	count = 0;
	Fib_count(40);
	printf("计算Fib(40)时,Fib(3)被调用了 %d 次\n", count);
	return 0;
}
效率问题:重复计算的 “灾难”

Fib(5)为例,递归调用树如下:

              Fib(5)
             /      \
          Fib(4)     Fib(3)  // Fib(3)第1次调用
         /    \       /    \
      Fib(3) Fib(2)  Fib(2) Fib(1) // Fib(3)第2次调用
     /    \
  Fib(2) Fib(1)
  • 计算Fib(5)时,Fib(3)被重复调用 2 次,Fib(2)被重复调用 3 次;
  • n=40时,Fib(3)被调用的次数会达到数百万次 —— 重复计算导致时间复杂度呈指数级增长(O(2ⁿ)),程序运行极慢。
高效迭代实现(修正返回值缺陷)
#include <stdio.h>

int Fib(int n)
{
	if (n <= 2) // 基准情况:n=1或2时返回1
		return 1;

	int a = 1; // 存储F(n-2)
	int b = 1; // 存储F(n-1)
	int c = 0; // 存储F(n) = F(n-1)+F(n-2)

	while (n > 2)
	{
		c = a + b;
		a = b;   // 更新F(n-2)为上一轮的F(n-1)
		b = c;   // 更新F(n-1)为上一轮的F(n)
		n--;
	}
	return c; // 原代码缺少返回值,修正后返回F(n)
}

int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d\n", ret);
	return 0;
}
递归与迭代核心对比表
特性递归迭代(循环)
代码简洁性简洁优雅,逻辑直观相对繁琐,逻辑需手动梳理
执行效率低(函数调用开销 + 重复计算)高(无调用开销,无重复计算)
内存占用高(栈帧累积,有栈溢出风险)低(固定变量,内存稳定)
调试难度高(调用栈嵌套,流程复杂)低(循环流程清晰,易跟踪)

四、递归的适用场景与避坑指南

1. 何时优先用递归?

✅ 问题是递归定义的(如阶乘、斐波那契数列);

✅ 问题可自然分解为相似子问题(如归并排序、快速排序);

✅ 操作对象是递归数据结构(如链表、树、图的深度优先搜索)。

2. 何时避免用递归?

❌ 问题规模极大(可能导致栈溢出);

❌ 朴素递归存在严重重复计算(如斐波那契数列);

❌ 对性能要求极高,且迭代解法同样简洁。

3. 递归避坑 3 大要点

⚠️ 牢记基准条件:缺少基准条件会导致无限递归和栈溢出;

⚠️ 确保递推逼近基准:比如Fact(n)不能写成Fact(n+1),否则永远达不到基准;

⚠️ 警惕重复计算:遇到重复计算问题,优先用迭代或 “记忆化搜索” 优化。

五、拓展思考题(练手必备)

  1. 青蛙跳台阶问题:一只青蛙一次可以跳 1 级或 2 级台阶,求跳上 n 级台阶的总方法数?(提示:本质是斐波那契数列)
  2. 汉诺塔问题:将 A 杆上的 N 个圆盘(上小下大)通过 B 杆移到 C 杆,每次只能移一个圆盘,且大盘不能叠在小盘上。请打印移动步骤?(提示:递归拆解为 “移 N-1 个盘→移第 N 个盘→移 N-1 个盘”)

📝 总结

递归的核心是 “找准基准条件 + 正确递推分解”:递推是 “分解问题”,回归是 “计算结果”。它不是 “炫技工具”,而是解决特定问题的高效思路 —— 用好了简洁优雅,用不好则效率低下、栈溢出。

如果这篇博客帮你理清了递归逻辑,欢迎点赞收藏!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值