【C语言初阶】函数递归

本文详细讲解了C语言中的函数递归概念,包括递归的定义、两个必要条件,以及通过递归和迭代方法求解阶乘和斐波那契数的实例。还探讨了递归在汉诺塔和青蛙跳台阶问题中的应用,同时提醒读者注意递归可能导致的性能问题。
摘要由CSDN通过智能技术生成

【C语言初阶】函数递归

八、函数递归

直接或者间接地调用自身的算法称为递归算法。

1 什么是函数递归?

函数递归就是一个函数在其函数体内调用自身。

  • 一个函数在其定义或说明中有直接或间接调用自身的一种方式。

  • 通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解

  • 递归策略只需要少量的程序就可以描述出解题过程所需要的多次重复计算,大大减少了程序的代码量


2 函数递归的两个必要条件

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


3 递归练习

3.1 练习1

将一个无符号整型数值,按照从大到小位的顺序打印每一位。

例如:输入:1234,输出:1 2 3 4

示例代码

#include<stdio.h>

void print(unsigned int n)
{
	if (n > 9)
	{
		print(n / 10);
	}
	printf("%d ", n % 10);
}
int main()
{
	//将一个无符号整型数值,按照从大到小位的顺序打印每一位。
	//例如:输入:1234,输出:1 2 3 4

	unsigned int num = 1234;
	print(num);

	return 0;
}

解释说明

1 num 的值当做实参传入 print 函数后,print 函数会不断地将 1234 除10的数充当实参继续调用,直到传入的实参是原数最高位时,就打印该位数,然后返回打印最高位的下一位数,直到打印完所有位数

2 每一次函数调用,都要在栈区上分配一块内存空间,用来保存函数在调用过程中的上下文信息


3.2 练习2

求字符串的长度,要求不能使用临时变量。

3.2.1 迭代实现

示例代码

#include<stdio.h>

int my_strlen(const char* str)
{
	int count = 0;//计数器

	//方法一
	//while (*str != '\0')
	//{
	//	count++;
	//	str++;
	//}

	//方法二
	while (*str++ != '\0')
		count++;
	
	return count;
}
int main()
{
	//求字符串的长度,要求不能使用临时变量。

	char* p = "abcdef";

	int len = my_strlen(p);
	printf("len = %d\n", len);

	return 0;
}

解释说明

1 while (*str++ != '\0') count++;

  求字符串长度,统计从当前位置 str 开始向后访问不是 \0 的字符个数,直到遇到 \0


3.2.2 递归实现

示例代码

#include<stdio.h>

int my_strlen(const char* str)
{
	if (*str == '\0')
		return 0;
	else
		return 1 + my_strlen(++str);
}
int main()
{
	//求字符串的长度,要求不能使用临时变量。

	char* p = "abcdef";

	int len = my_strlen(p);
	printf("len = %d\n", len);

	return 0;
}

解释说明

1 return 1 + my_strlen(str + 1);

  递归调用寻找 \0 , 每次字符数+1,直到遇到 \0 就向外返回上一层,直到返回到第一次调用的地方

  最终得出字符串的长度(也就是 \0 之前所有的字符总数)


4 递归与迭代

4.1 递归示例1

n 的阶乘。(不考虑溢出)

示例代码

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int fac(int n)
{
	if (n > 1)
		return n * fac(n - 1);
	else
		return 1;
}
int main()
{
	//求n的阶乘(不考虑溢出)

	int n = 0;
	//输入
	printf("请输入n的值:>");
	scanf("%d", &n);
	//计算并输出
	printf("%d! = %d\n", n, fac(n));

	return 0;
}

解释说明

1 求 n 的阶乘:
f a c ( n ) = { n < = 1 , 1 n > = 2 , n ∗ f a c ( n − 1 ) fac(n) = \begin{cases} n <= 1, \qquad \quad 1 \\ n >= 2, \quad n * fac(n-1) \end{cases} fac(n)={n<=1,1n>=2,nfac(n1)


4.2 递归示例2

求第 n 个斐波那契数。(不考虑溢出)

示例代码

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int fib(int n)
{
	if (n > 2)
		return fib(n - 1) + fib(n - 2);
	else if (n > 0)
		return 1;
}
int main()
{
	//求第n个斐波那契数(不考虑溢出)
	//1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 ...

	int n = 0;
	//输入
	printf("请输入要计算的位数:>");
	scanf("%d", &n);
	printf("第%d个斐波那契数:%d\n", n, fib(n));

	return 0;
}

解释说明

1 求第 n 个斐波那契数:
f i b ( n ) = { n < = 2 , 1 n > 2 , f i b ( n − 1 ) + f i b ( n − 2 ) fib(n) = \begin{cases} n <= 2, \qquad \qquad \qquad 1 \\ n > 2, \qquad fib(n-1) + fib(n-2) \end{cases} fib(n)={n<=2,1n>2,fib(n1)+fib(n2)


4.3 使用递归函数发现的问题

  • 使用 fib 这个递归函数计算第50个斐波那契数时,会发现特别耗费时间

  • 使用 fac 这个递归函数计算10000的阶乘时程序会崩溃

4.3.1 递归求斐波那契数的问题

在使用递归求解斐波那契数时,在函数递归调用的过程中出现了大量的重复计算

a. 代码示例

修改代码,统计求第40个斐波那契数时,第3个斐波那契数被计算了多少次。

示例代码

#include<stdio.h>

int count = 0;
int fib(int n)
{
	if (3 == n)
		count++;//统计求第40个斐波那契数时,第3个斐波那契数被计算了多少次

	if (n > 2)
		return fib(n - 1) + fib(n - 2);
	else
		return 1;
}
int main()
{
	//求第n个斐波那契数

	int n = 40;
	printf("第%d个斐波那契数:>%d\n", n, fib(n));	//102334155
	printf("count(3) = %d\n", count);			// 39088169

	return 0;
}

解释说明

我们看到,计算第40个斐波那契数时第3个斐波那契数被重复计算了 39088169,这是一个很大的值。


b. 改进方法

在调试 fib 函数的时候,如果传入的实参比较大,就会出现 Stack Overflow(栈溢出) 的错误信息。

  • 系统分配给程序的栈空间是有限的,如果出现了死循环或者死递归,就会一直开辟新的栈空间,最终耗尽栈空间,这样的现象称之为栈溢出

解决方法:

  1. 将递归实现修改成非递归实现。

  2. 使用 static 静态变量代替 nonstatic 非静态变量。

  在递归函数设计中,可以使用 static 对象替代 nonstatic 局部对象(栈对象),这样做不仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保存递归调用的中间状态值,并且可为各个调用层访问(使用和修改)。


4.4 非递归实现(循环迭代)

4.4.1 求 n 的阶乘

代码示例

#include<stdio.h>

int fac(int n)
{
	int mul = 1;
	while (n > 0)
	{
		mul *= n;
		n--;
	}
	return mul;
}
int main()
{
	//求n的阶乘

	int n = 0;
	scanf("%d", &n);
	printf("%d的阶乘为:>%d\n", n, fac(n));

	return 0;
}

解释说明

1 非递归函数实现求 n 的阶乘,从 n 开始相乘,每乘一次 n 减1一次,直到 n 变成了1。


4.4.2 求第 n 个斐波那契数

代码示例

#include<stdio.h>

int fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 1;

	while (n > 2)
	{
		c = a + b;
		a = b;
		b = c;
		n--;
	}
	return c;
}
int main()
{
	//求第n个斐波那契数
	//1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 ...

	int n = 0;
	scanf("%d", &n);
	printf("第%d个斐波那契数:>%d\n", n, fib(n));

	return 0;
}

解释说明

1 使用三个临时变量 a、b、c ,初始值都为1;

  根据斐波那契数列的特点,可知第1、2个斐波那契数都是1,从第三个斐波那契数开始,每一项的值都为它前两项之和

  那么当所求第 n 个斐波那契位数大于2时,就c 表示前两项数之和,之后将 b、c 两项前移至 a、b 两项

  n 的自减表示 c 为前两项之和的计算次数,也就是说只有当计算位数 n 大于2,才开始 c 的计算。

  函数最终返回 c 的值(第 n 个斐波那契数)到主调函数处。


4.5 递归与迭代的提示

许多问题是以递归的形式解释说明的,这是因为它比非递归的形式更为清晰

但是这些问题的迭代实现往往比递归实现的效率更高,可能只是代码的可读性稍差些

当一个问题相当复杂,且难以使用迭代实现时,此时递归实现的代码简洁性可以弥补其运行开销


5 函数递归的经典题目

1 汉诺塔问题

1.1 问题描述

汉诺塔(Tower of Hanoi),又称河内塔,是一个源于印度古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

1.2 示例代码

代码示例

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int count = 0;
static void move(int x, int y)
{
	count++;//移动次数计数器
	printf("第%d次移动:>\t%c --> %c\n", count, x, y);
}
void Hanoi(int n, char a, char b, char c)
{
	if (n > 0)
	{
		Hanoi(n - 1, a, c, b);	//第一步
		move(a, c);				//第二步
		Hanoi(n - 1, b, a, c);	//第三步
	}
}
int main()
{
	//汉诺塔问题
    
	int n = 0;
	//输入
	printf("汉诺塔层数:>");
	scanf("%d", &n);
	//处理
	Hanoi(n, 'A', 'B', 'C');
    //输出
	printf("汉诺塔需移动:>  %d次\n", count);

	return 0;
}

解释说明

1 假设总共有 n 层(个)圆盘,那么总共需要移动 2^n -1 次,就能将放在 A 柱子上的圆盘全部移动到 C 柱子上。

2 步骤:

第一步:将 A 柱上的 n-1 个盘子通过 C 柱移动到 B 柱上;

第二步:将 A 柱上剩余的最大的 1 个盘子移动到 C 柱上;

第三步:将 B 柱上剩余的 n-1 个盘子通过 A 柱移动到 C 柱上。


2 青蛙跳台阶问题

2.1 问题描述

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

2.2 示例代码

代码示例

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int count = 0;///统计跳法数目
void frog(int n)
{
	if (0 == n)//最后正好跳到第n级台阶而不是跳过第n级台阶的情况
		count++;
	else if (n > 0)
	{
		frog(n - 1);
		frog(n - 2);
	}
}
int main()
{
	//青蛙跳台阶问题
	//台阶数:1 2 3 4 5  6  7  8  9 10  11  12  13  14
	//跳法数:1 2 3 5 8 13 21 34 55 89 144 233 377 610

    int n = 0;
	//输入
	printf("台阶个数:>");
	scanf("%d", &n);
	//计算
	frog(n);
	//输出
	printf("一共有%d种跳法\n", count);

	return 0;
}

解释说明

1 假设跳上 n 级台阶有 f(n) 种跳法,而在所有跳法中,最后一步跳到第 n 级台阶只有两种情况:跳1级就达到或者跳2级就达到。

  当为1级台阶时: 剩余 n-1 个台阶,此情况共有 f(n-1) 种跳法;

  当为2级台阶时: 剩余 n-2 个台阶,此情况共有 f(n-2) 种跳法。
f(n) 为以上两种情况之和,即 f(n)=f(n-1)+f(n-2),以上递推性质为斐波那契数列

2 本题可转化为求斐波那契数列第 n 项的值 ,与斐波那契数列等价

f i b ( n ) = { n < = 2 , 1 n > 2 , f i b ( n − 1 ) + f i b ( n − 2 ) fib(n) = \begin{cases} n <= 2, \qquad \qquad \qquad 1 \\ n > 2, \qquad fib(n-1) + fib(n-2) \end{cases} fib(n)={n<=2,1n>2,fib(n1)+fib(n2)


总结:

  本节介绍了什么是函数递归以及函数递归的两个必要条件;给出了同一问题的递归实现和迭代实现;讲述了函数递归的几个经典题目。


感谢您的阅读!如有任何错误,欢迎您的批评指正!

  • 29
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值