函数递归的思想与应用|C语言

1.函数递归的定义:


程序在执行时自己调用自己的编程技巧称为递归(recursion)。

函数递归是一种解决问题的方法,其中问题的解决方案依赖于相同问题的较小实例。在函数递归中,问题被分解为更小的、相似的子问题,直到达到一种基本情况,然后逐步将结果组合起来,得到原始问题的解。

递归函数通常包含两个部分:

  1. 基本情况: 这是递归的结束条件。当递归到达基本情况时,函数将不再调用自身,而是返回一个确定的值。

  2. 递归情况: 在这一部分,函数调用自身,但通常是通过解决一个规模更小的子问题。递归情况将问题划分为更小的子问题,然后通过逐步解决这些子问题来解决原始问题。

2.函数递归的优缺点:


递归在编程中有一些优点和缺点,这些特性使得递归在某些情况下非常有用,而在其他情况下可能不太适合。

 2.1优点:

  1. 问题分解: 递归能够将大问题分解成小问题,使得程序更易于理解和维护。

  2. 代码重用: 递归可以促使代码的重用,因为函数可以在自身调用,处理相似的问题。

2.2缺点:

  1. 性能开销: 递归可能会导致性能开销较大。每次递归调用都需要在内存栈上分配空间,而递归调用层次较深时可能会导致栈溢出

  2. 难以调试: 递归可能会增加代码的复杂性,使得调试过程变得更加困难,特别是对于递归深度较大的情况。

  3. 潜在的无限循环: 如果递归没有正确的终止条件,可能会导致无限递归,最终耗尽系统资源。

  4. 空间复杂度: 递归可能占用大量的内存空间,特别是在递归深度较大的情况下。非递归解决方案通常可以更节省内存。

3.函数递归的常见应用:


3.1用递归法求阶乘:

3.1.1题目描述:

请使用递归的方法求n的阶乘
输入样例:
4

输出样例:

24

3.1.2解题思路:

我们可以参照递归的思想:将大问题分解成小问题。要求n的阶乘,我们只需要知道(n-1)的阶乘,再用 n * (n-1)的阶乘 即可得到n的阶乘,要求(n-1)的阶乘,我们只需要知道(n-2)的阶乘,再用(n-1) * (n-2)的阶乘即可得到(n-1)的阶乘... ...最后,要求2的阶乘,我们只需要求1的阶乘,再用 2 * 1的阶乘得到2的阶乘,而我们显然知道1的阶乘 = 1,所以我们以n = 1 为基本情况,这是递归结束的条件。从而实现了大事化小的思想。

这里以n=3为例,画一个图帮助理解: 

3.1.3代码实现:
#include<stdio.h>

int factorial(int n);

int main()
{
	int n;
	scanf("%d", &n);
	int result = factorial(n);
	printf("%d\n", result);
	
	return 0;
}

int factorial(int n)
{
	if(n == 1)
	{
		return 1;
	}
	else
	{
		return n * factorial(n - 1);
	}
}

3.2用递归法解决数学问题:

3.2.1题目描述:

已知f(n)的函数满足递推公式:

f(1) = k

f(2) = f(1) + 1

f(3) = f(2) + 1 + 2

......

f(n) = f(n-1) + (1 + 2 + ... + n-1)

输入格式:

第一行输出一个整数T,表示样例数。(1 <= T <= 100)

每个样例占一行,输入两个整数n,k。(0 < n, k <= 1000) 。

输出格式:

每个样例输出一个整数表示f(n)。

输入样例:

2
1 1
2 3

输出样例:

1
4
3.2.2解题思路:

 本题思路与求阶乘类似,要求f(n),我们只需要知道f(n-1)的值以及1到n-1的和,1到n-1的和我们可以用等差数列前N项公式算出,要求f(n-1)我们只需要知道f(n-2)的值以及1到n-2的和......最后,要求f(2)我们只需要知道f(1)的值,及题目给的k(基本情况)。

同样以n=3为例,画一个图帮助理解: 

3.2.3代码实现:
#include<stdio.h>
int figure(int x, int y);
int main()
{
    int n, a, b;
    scanf("%d", &n);

    for(int i = 0; i < n; i ++)
    {
        scanf("%d %d", &a, &b);
        int rst = figure(a, b);
        printf("%d\n", rst);
    }
    
    return 0;
}

int figure(int x, int y)
{
    if (x == 1)
    {
        return y;
    }
    else 
    {
        return figure(x - 1, y) + (x * (x - 1) / 2);
    }
}

3.3递归解决汉诺塔问题(Hanoi)

3.3.1问题引入:

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

这是一个经典的递归问题,用递归的思想来想这个问题,该问题可以简化为:我一个人完成64个盘子的移动很难,如果有人能帮我把第一根柱子上的前63个圆盘移动到第二根柱子上,我只用将第一根柱子上的最后一个盘子移动到第三根柱子上,最后在叫那个人帮我把第二根柱子上的63个盘子移动到第三根柱子上,问题就解决了。

第二个人拿到这个问题时也觉得很难,他也想要是有人能帮我把第一根柱子上的前62个圆盘移动到第二根柱子上,我只用将第一根柱子上的倒数第二个盘子移动到第三根柱子上,最后在叫那个人帮我把第二根柱子上的62个盘子移动到第三根柱子上,我的任务就完成了。

......

到第64个人,此时他的任务已经很简单了:

把第一根柱子上的第1个盘子移动到第二根柱子上。等第63个人把第2个盘子从第二柱子移动到第三根柱子后,第63个人把第二根柱子上的盘子移动到第三根柱子(基本情况)。

由此可以分析,要把n个盘子从第一根柱子移动到第三根柱子只需要:

  (1)把第一根柱子上(n-1)个盘子借助第三根柱子移到第二根柱子。

  (2)把第一根柱子上第n个盘子移动到第三根柱子。

  (3)把第二根柱子上(n-1)个盘子借助第一根柱子移动到第三根柱子。

3.3.2题目描述:

输入格式:

输入为一个整数后面跟三个单字符字符串。
整数为盘子的数目,后三个字符表示三个杆子的编号。

输出格式:

输出每一步移动盘子的记录。一次移动一行。
每次移动的记录为例如3:a->b 的形式,即把编号为3的盘子从a杆移至b杆。
我们约定圆盘从小到大编号为1, 2, ...n。即最上面那个最小的圆盘编号为1,最下面最大的圆盘编号为n。

输入样例:

3 a b c

输出样例:

1:a->c
2:a->b
1:c->b
3:a->c
1:b->a
2:b->c
1:a->c
3.3.4代码实现:
#include <stdio.h>

// 函数声明
void hanoi(int n, char source, char auxiliary, char target);

int main() {
    int n;

    // 输入要移动的盘子数
	char a, b, c; 
    scanf("%d %c %c %c", &n, &a, &b, &c);

    // 调用汉诺塔函数
    hanoi(n, a, b, c);

    return 0;
}

// 汉诺塔函数的实现
void hanoi(int n, char source, char auxiliary, char target) {
    if (n == 1) {
        // 如果只有一个盘子,直接从源柱子移动到目标柱子
        printf("%d:%c->%c\n", n, source, target);
    } else {
        // 将 n-1 个盘子从源柱子移动到辅助柱子
        hanoi(n - 1, source, target, auxiliary);

        // 将第 n 个盘子从源柱子移动到目标柱子
        printf("%d:%c->%c\n", n, source, target);

        // 将 n-1 个盘子从辅助柱子移动到目标柱子
        hanoi(n - 1, auxiliary, source, target);
    }
}
3.3.5代码原理: 

hanoi函数接收四个参数:盘子的个数(n),起始柱(source),辅助柱(auxiliary),目标柱(target)

(1)当source上只有1一个盘时,我们只需要将source上的1个盘直接移动到target盘上。

if (n == 1) {
        // 如果只有一个盘子,直接从源柱子移动到目标柱子
        printf("%d:%c->%c\n", n, source, target);

(2) 如果source上的盘子数大于1,我们则将除最下方以外的盘子(n-1个)看作一个整体,并将这个整体(n-1个)从source柱开始,借助target柱,移动到auxiliary柱

else {
        // 将 n-1 个盘子从源柱子移动到辅助柱子
        hanoi(n - 1, source, target, auxiliary);

        // 将第 n 个盘子从源柱子移动到目标柱子
        printf("%d:%c->%c\n", n, source, target);

        // 将 n-1 个盘子从辅助柱子移动到目标柱子
        hanoi(n - 1, auxiliary, source, target);
    }

我们无需关心(n-1)个圆盘是具体怎么完成移动的,剩下的操作交给程序执行即可。

3.3.6初识汉诺塔递归代码可能会产生的疑问:

Q:为什么(n-1)个盘子的移动一定能正确的实现?

A:这与数学中的数学归纳法有关,他们关系如下:

数学归纳法                              递归

  归纳奠基 ——————> 基础情况处理

  归纳假设 ——————>   递归调用

  归纳递推 ——————> 递推到当前层

在数学归纳法中,在归纳奠基阶段,我们先证明了n=1时结论成立,然后在归纳假设阶段,假设n=k时结论也成立,最后在归纳递推阶段,我们需要由归纳假设推导出n=k+1时结论也成立。

如果这三个步骤全部满足,我们就可以说该结论成立,也就是对于任何的n,结论都适用。

在汉诺塔的递归代码中也是同理,当我们正确的处理了基础情况,设置好递归调用后,只用递推到当前层,整个递归就是正确的了。

3.4整数分解为若干项之和:

3.4.1题目描述:

将一个正整数N分解成几个正整数相加,可以有多种分解方法,例如7=6+1,7=5+2,7=5+1+1,…。编程求出正整数N的所有整数分解式子。

输入格式:

每个输入包含一个测试用例,即正整数N (0<N≤30)。

输出格式:

按递增顺序输出N的所有整数分解式子。递增顺序是指:对于两个分解序列N1​={n1​,n2​,⋯}和N2​={m1​,m2​,⋯},若存在i使得n1​=m1​,⋯,ni​=mi​,但是ni+1​<mi+1​,则N1​序列必定在N2​序列之前输出。每个式子由小到大相加,式子间用分号隔开,且每输出4个式子后换行。

输入样例:

7

输出样例:

7=1+1+1+1+1+1+1;7=1+1+1+1+1+2;7=1+1+1+1+3;7=1+1+1+2+2
7=1+1+1+4;7=1+1+2+3;7=1+1+5;7=1+2+2+2
7=1+2+4;7=1+3+3;7=1+6;7=2+2+3
7=2+5;7=3+4;7=7
3.4.2解题思路:

我们同样用递归的思想来看这个问题,该问题的基本情况是被分解的数为0,不能再被分解,递归结束。子问题是将待分解的整数拆分为更小的整数。具体而言,对于整数n,我们可以选择一个小于等于n的正整数作为分解的一部分,然后递归地对剩余的部分进行分解。

3.4.3代码实现:
#include<stdio.h>
int n;
int a[40];
int count = 0;

void decomposeANDprint(int index, int start, int num);

int main()
{
	scanf("%d", &n);
	decomposeANDprint(0, 1, n);
	
	return 0;
}

void decomposeANDprint(int index, int start, int num)
{
	int i = 0;
	if(num != 0)
	{
		for(i = start; i <= num; i ++)
		{
			a[index] = i;
			start = i;
			decomposeANDprint(index + 1, start, num - i);
		}
	}
	else 
	{
		printf("%d=%d", n, a[0]);
		for(i = 1; i < index; i ++)
		{
			printf("+%d", a[i]);
		}
		count ++;
		if(count != 4 && a[index - 1] != n)
		{
			printf(";");
		}
		if(count == 4)
		{
			printf("\n");
			count = 0;
		}
	}
}
3.4.4代码原理:

参数含义:

index是填入数组a的位置,每次调用decomposeANDprint函数时,通过`index + 1`来使index指向下次分解要填入的位置。start是这次分解开始的位置。num是待分解的部分。

在函数decomposeANDprint中,通过递归调用`decomposeANDprint(index + 1, start, num - 1)`,循环尝试每一种可能的分解项,递归调用自身并更新start和num的值,通过不断解决相同的子问题来解决原问题。需要注意每次调用函数后,编译器会为程序开辟一个新的栈,所以每次进入decomposeANDprint函数后都会有一个新的局部变量 i ,而之前的递归调用中的 i 不会对当前调用的i造成影响。这确保了每一层递归都有自己独立的 i 值。

void decomposeANDprint(int index, int start, int num)
{

    // 每次递归调用后从这里开始执行:
    int i = 0;
    if (num != 0)
    {
        for (i = start; i <= num; i++)
        {
            a[index] = i;
            start = i;
            decomposeANDprint(index + 1, start, num - i);
        }
    }

当num变为0时,表示整数分解完成,进入基本情况的代码块位函数的else分支内。即以下这段代码:

else
{
    printf("%d=%d", n, a[0]);
    for (i = 1; i < index; i++)
    {
        printf("+%d", a[i]);
    }
    count++;
    if (count != 4 && a[index - 1] != n)
    {
        printf(";");
    }
    if (count == 4)
    {
        printf("\n");
        count = 0;
    }
}

需要注意:在这个区块中,`count`是全局变量,不会因为每一层递归的层数不同而存在多个count的值,这确保了打印区块能正常按照题目要求:式子间用分号隔开,且每输出4个式子后换行。

4.参考:

C语言:函数递归详解(建议收藏)_c语言递归_小超想发财的博客-CSDN博客

递归实现汉诺塔问题_递归汉诺塔你-CSDN博客

汉诺塔(益智玩具)_百度百科 (baidu.com)

[Manim动画]4分钟看懂汉诺塔递归思路及函数内部运作过程_哔哩哔哩_bilibili

【递归2】如何治疗晕递归?_哔哩哔哩_bilibili

  • 34
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值