【C语言篇】递归详细介绍(基础概念习题及汉诺塔等进阶问题)

简介: 要保持最小的步数,每一次汉诺塔问题(无论是最初还是递归过程中的),如果此时初始柱盘子数为偶数,我们第一步是把最上面的盘子移动到中转柱,如果为奇数,我们第一步则是将其移动到目标柱。

递归是什么


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

递归其实是⼀种解决问题的⽅法,在C语⾔中,递归就是函数⾃⼰调⽤⾃⼰。 写⼀个史上最简单的C语⾔递归代码:

#include <stdio.h>
int main()
{
    printf("hehe\n");
    main();//main函数中⼜调⽤了main函数 
    return 0;
}

上述就是⼀个简单的递归程序,只不过上⾯的递归只是为了演⽰递归的基本形式,不是为了解决问题,代码最终也会陷⼊死递归,导致栈溢出(Stack overflow)。



递归的思想


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


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


递归的限制条件

递归在书写的时候,有2个必要条件:


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


在下⾯的例⼦中,我们逐步体会这2个限制条件


递归举例


求n的阶乘


⼀个正整数的阶乘(factorial)是所有⼩于及等于该数的正整数的积,并且0的阶乘为1。 ⾃然数n的阶乘写作n!。


分析和代码实现


我们知道n的阶乘的公式:n! = n ∗ (n − 1)!


当 n==0 的时候,n的阶乘是1,其余n的阶乘都是可以通过公式计算。


那我们就可以写出函数Fact求n的阶乘,假设Fact(n)就是求n的阶乘,那么Fact(n-1)就是求n-1的阶乘,函数如下:

int Fact(int n)
{
    if(n==0)
        return 1;
    else
        return n*Fact(n-1);
}
  • 要求n的阶乘,那就是求n-1的阶乘再乘以n


这样的思路就是把⼀个较⼤的问题,转换为⼀个与原问题相似,但规模较⼩的问题来求解的。

画图推演


顺序打印一个整数的每一位


输⼊⼀个整数m,按照顺序打印整数的每⼀位。

⽐如: 输⼊:1234 输出:1234

输⼊:52 输出:52


分析和代码实现


在这之前学习循环的时候我们通过不断模10除10可以逆序打印整数的每一位


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


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


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


但现在要求我们顺序打印,该怎么实现呢?


发现其实⼀个数字的最低位是最容易得到的,通过%10就能得到


那我们假设想写⼀个函数Print来打印n的每⼀位,如下表⽰:


Print(n)
如果n是1234,那表⽰为
Print(1234) //打印1234的每⼀位 
其中1234中的4可以通过%10得到,那么
Print(1234)就可以拆分为两步:
1. Print(1234/10) //打印123的每⼀位 
2. printf(1234%10) //打印4 
完成上述2步,那就完成了1234每⼀位的打印
那么Print(123)⼜可以拆分为Print(123/10) + printf(123%10)

以此类推下去,就有:

Print(1234)
==>Print(123) +                       printf(4)
==>Print(12) +             printf(3)
==>Print(1) +   printf(2)
==>printf(1) 

直到被打印的数字变成⼀位数的时候,就不需要再拆分,递归结束。 那么代码完成也就⽐较清楚:

void Print(int n)
{
    if(n>9)
    {
        Print(n/10);
    }
    printf("%d ", n%10);
}

在这个解题的过程中,我们就是使⽤了⼤事化⼩的思路:


把Print(1234)打印1234每⼀位,拆解为⾸先Print(123)打印123的每⼀位,再打印得到的4


把Print(123)打印123每⼀位,拆解为⾸先Print(12)打印12的每⼀位,再打印得到的3


直到Print打印的是⼀位数,直接打印就⾏。


画图推演


递归与迭代


递归是⼀种很好的编程技巧,但是和很多技巧⼀样,也是可能被误⽤的,就像练习一求阶乘一样,看到推导的公式,很容易就被写成递归的形式:


但是,但是


在递归函数调⽤的过程中涉及⼀些运⾏时的开销。


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


函数不返回,函数对应的栈帧空间就⼀直占⽤,所以如果函数调⽤中存在递归调⽤的话,每⼀次递归 函数调⽤都会开辟属于⾃⼰的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。 所以如果采⽤函数递归的⽅式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stackoverflow)的问题。


所以如果不想使⽤递归,就得想其他的办法,通常就是迭代的⽅式(通常就是循环的⽅式)。


⽐如:计算n的阶乘,也是可以产⽣1~n的数字累计乘在⼀起的。


int Fact(int n)
{
    int i = 0;
    int ret = 1;
    for(i=1; i<=n; i++)
    {
        ret *= i;
    }
    return ret;
}

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


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


但如果当⼀个问题⾮常复杂,难以使⽤迭代的⽅式实现时,此时递归实现的简洁性便可以补偿它所带来的运⾏时开销。


递归求第n个斐波那契数



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

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

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



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

#include <stdio.h>

int count = 0;
int Fib(int n)
{
    if(n == 3)
        count++;//统计第3个斐波那契数被计算的次数 
    if(n<=2)
        return 1;
    else
        return Fib(n-1)+Fib(n-2);
}

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

这⾥我们看到了,在计算第40个斐波那契数的时候,使⽤递归⽅式,第3个斐波那契数就被重复计算了 39088169次,这些计算是⾮常冗余的。


所以斐波那契数的计算,使⽤递归是⾮常不明智的,我们就得 想迭代的⽅式解决


迭代求第n个斐波那契数


我们知道斐波那契数的前2个数都1,然后前2个数相加就是第3个数,那么我们从前往后,从⼩到⼤计算就⾏了。


这样就有下⾯的代码:


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;
}

迭代的⽅式去实现这个代码,效率就要⾼出很多了。


有时候,递归虽好,但是也会引⼊⼀些问题,所以我们⼀定不要迷恋递归,适可⽽⽌就好。


拓展练习


青蛙跳台阶问题


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


递归求解


和斐波那契数列很相似,要求跳上第n级的台阶(n>3),无非就是从n-2跳两级或者n-1的台阶跳1级,那总方法就是f(n)=f(n-2)+f(n-1)


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

显然递归求解也涉及到很多的重复计算问题,效率十分之低

迭代求解

斐波那契数列是:1 1 2 3 5 8 13······

青蛙跳台阶数列是:1 2 3 5 8 13······

只需做适当更改即可:

int Fn(int n) {
    int a = 1;
    int b = 2;
    int c = 0;
    if (n == 1) {
        return 1;
    }
    else if (n == 2) {
        return 2;
    }
    else{
        while(n>2){
            c = a + b;
            a = b;
            b = c;
            n--;
        }
    }
    return c;
}

汉诺塔问题


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


注意:求得是最少移动次数



  • 当n=1时,直接将a移到c即可
  • 当n=2时,a->b,a->c,b->c
  • 当n=3时,a->c,a->b,c->b,a->c,b->a,b->c,a->c


当n越来越大时,如果我们要一次一次想再把它每一步写出来,是没有意义的了


所以我们先不关注每一步细节,思考一下移动的核心逻辑是什么


核心逻辑就是每一次移动都是把最底下的盘子移动到目标盘,此时只关注剩下的n-1个盘子,又变为了新的汉诺塔问题


例如当n=3的时候,(从下到上依次设定为盘子1,2,3)我们前四步进行的就是把盘子3移动到c,此时初始柱为a,中转柱为b,目标柱为c。


此时3已经移动到最后的正确位置了,直接忽略,而接下来要做的就是把b柱上的盘子1和2移到c,这不就是n==2的汉诺塔问题吗,此时初始柱变成了b,中转柱变成了a,目标柱就是c,我们在第5-7所做的事就是把盘子2移动到c


最后一步,即变为了只有一个盘子的汉诺塔问题,直接将盘子1移动到c即可。


上述分析仍然太过复杂,不妨这样考虑:

  • 第一步,n-1个盘子移动到中转柱,这其实何尝不是一个汉诺塔问题呢
  • 第二步,最底下的盘子移动到目标柱
  • 第三步,中转柱的n-1个盘子移动到目标柱,这又何尝不是一个汉诺塔问题呢


并且,这种思维还能帮我们推导出n个盘子移动所需要的最少步数

手写推导如下:



就是高中数学很简单的数列题

如果想用数学归纳法求解也是可以的,也很简单,这里就不过多赘述

代码如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
/*
pos1:初始柱
pos2::中转柱
pos3:目标柱
*/
void move(char x,char y)
{
    printf("%c->%c ", x, y);
}
void hanoi(int n, char pos1, char pos2, char pos3)
{
    if (n == 1)
        move(pos1, pos3);
    else
    {
        hanoi(n - 1, pos1, pos3, pos2);//把n-1个盘子移动到中转柱pos2上,
        //是一个汉诺塔问题,对于这n-1个盘子来说,
        //初始柱为pos1,中转柱为pos3,目标柱为pos2

        move(pos1,pos3);//把初始柱最下面盘子移动到目标柱

        //此时n-1个盘子都在pos2上,
        //这也是一个汉诺塔问题
        //初始柱为pos2,中转柱为pos1,目标柱为pos3
        hanoi(n - 1, pos2, pos1, pos3);
    }
}

int main()
{
    int n = 0;
    scanf("%d", &n);
    hanoi(n, 'a', 'b', 'c');
    return 0;
}

如果我们更多关注每一次移动的细节会发现:


要保持最小的步数,每一次汉诺塔问题(无论是最初还是递归过程中的),如果此时初始柱盘子数为偶数,我们第一步是把最上面的盘子移动到中转柱,如果为奇数,我们第一步则是将其移动到目标柱。(当做一个归纳结论了解一下就行)

目录
相关文章
|
6月前
|
存储 编译器 程序员
C语言常见概念
C语言是一门基础的编程语言,通过编译器将源代码转换为计算机可执行的二进制程序。本文介绍了C语言的基本概念,包括其作为人与计算机交流的工具、编译与链接的过程、常用编译器的选择(如VS2022)、main函数的作用、库函数与关键字、字符与ASCII编码、字符串与转义字符等内容。同时,还讲解了如何在VS2022中创建C语言项目、编写第一个程序,以及常见的语法错误和调试方法。适合初学者了解C语言核心概念与开发环境搭建。
428 1
|
存储 C语言
【C语言程序设计——函数】递归求斐波那契数列的前n项(头歌实践教学平台习题)【合集】
本关任务是编写递归函数求斐波那契数列的前n项。主要内容包括: 1. **递归的概念**:递归是一种函数直接或间接调用自身的编程技巧,通过“俄罗斯套娃”的方式解决问题。 2. **边界条件的确定**:边界条件是递归停止的条件,确保递归不会无限进行。例如,计算阶乘时,当n为0或1时返回1。 3. **循环控制与跳转语句**:介绍`for`、`while`循环及`break`、`continue`语句的使用方法。 编程要求是在右侧编辑器Begin--End之间补充代码,测试输入分别为3和5,预期输出为斐波那契数列的前几项。通关代码已给出,需确保正确实现递归逻辑并处理好边界条件,以避免栈溢出或结果
702 16
|
算法 C语言
【C语言程序设计——循环程序设计】求解最大公约数(头歌实践教学平台习题)【合集】
采用欧几里得算法(EuclideanAlgorithm)求解两个正整数的最大公约数。的最大公约数,然后检查最大公约数是否大于1。如果是,就返回1,表示。根据提示,在右侧编辑器Begin--End之间的区域内补充必要的代码。作为新的参数传递进去。这个递归过程会不断进行,直到。有除1以外的公约数;变为0,此时就找到了最大公约数。开始你的任务吧,祝你成功!是否为0,如果是,那么。就是最大公约数,直接返回。
351 18
|
Serverless C语言
【C语言程序设计——循环程序设计】利用循环求数值 x 的平方根(头歌实践教学平台习题)【合集】
根据提示在右侧编辑器Begin--End之间的区域内补充必要的代码,求解出数值x的平方根;运用迭代公式,编写一个循环程序,求解出数值x的平方根。注意:不能直接用平方根公式/函数求解本题!开始你的任务吧,祝你成功!​ 相关知识 求平方根的迭代公式 绝对值函数fabs() 循环语句 一、求平方根的迭代公式 1.原理 在C语言中,求一个数的平方根可以使用牛顿迭代法。对于方程(为要求平方根的数),设是的第n次近似值,牛顿迭代公式为。 其基本思想是从一个初始近似值开始,通过不断迭代这个公式,使得越来越接近。
355 18
|
C语言
【C语言程序设计——循环程序设计】统计海军鸣放礼炮声数量(头歌实践教学平台习题)【合集】
有A、B、C三艘军舰同时开始鸣放礼炮各21响。已知A舰每隔5秒1次,B舰每隔6秒放1次,C舰每隔7秒放1次。编程计算观众总共听到几次礼炮声。根据提示,在右侧编辑器Begin--End之间的区域内补充必要的代码。开始你的任务吧,祝你成功!
284 13
|
存储 安全 C语言
【C语言程序设计——选择结构程序设计】预测你的身高(头歌实践教学平台习题)【合集】
分支的语句,这可能不是预期的行为,这种现象被称为“case穿透”,在某些特定情况下可以利用这一特性来简化代码,但在大多数情况下,需要谨慎使用。编写一个程序,该程序需输入个人数据,进而预测其成年后的身高。根据提示,在右侧编辑器补充代码,计算并输出最终预测的身高。分支下的语句,提示用户输入无效。常量的值必须是唯一的,且在同一个。语句的作用至关重要,如果遗漏。开始你的任务吧,祝你成功!,程序将会继续执行下一个。常量都不匹配,就会执行。来确保程序的正确性。
460 10
|
小程序 C语言
【C语言程序设计——基础】顺序结构程序设计(头歌实践教学平台习题)【合集】
目录 任务描述 相关知识 编程要求 测试说明 我的通关代码: 测试结果: 任务描述 相关知识 编程编写一个程序,从键盘输入3个变量的值,例如a=5,b=6,c=7,然后将3个变量的值进行交换,使得a=6,b=7,c=5。面积=sqrt(s(s−a)(s−b)(s−c)),s=(a+b+c)/2。使用输入函数获取半径,格式指示符与数据类型一致,实验一下,不一致会如何。根据提示,在右侧编辑器补充代码,计算并输出圆的周长和面积。
337 10
|
存储 编译器 C语言
【C语言程序设计——函数】分数数列求和2(头歌实践教学平台习题)【合集】
函数首部:按照 C 语言语法,函数的定义首部表明这是一个自定义函数,函数名为fun,它接收一个整型参数n,用于指定要求阶乘的那个数,并且函数的返回值类型为float(在实际中如果阶乘结果数值较大,用float可能会有精度损失,也可以考虑使用double等更合适的数据类型,这里以float为例)。例如:// 函数体代码将放在这里函数体内部变量定义:在函数体中,首先需要定义一些变量来辅助完成阶乘的计算。比如需要定义一个变量(通常为float或double类型,这里假设用float。
588 3
|
存储 C语言
【C语言程序设计——循环程序设计】利用数列的累加和求 sinx(头歌实践教学平台习题)【合集】
项的累加和,一般会使用循环结构,在每次循环中计算出当前项的值(可能基于通项公式或者递推关系),然后累加到一个用于存储累加和的变量中。在C语言中推导数列中的某一项,通常需要依据数列给定的通项公式或者前后项之间的递推关系来实现。例如,对于一个简单的等差数列,其通项公式为。的级数,其每一项之间存在特定的递推关系(后项的分子是其前项的分子乘上。,计算sinx的值,直到最后一项的绝对值小于。为项数),就可以通过代码来计算出指定项的值。对于更复杂的数列,像题目中涉及的用于近似计算。开始你的任务吧,祝你成功!
338 6
|
C语言
【C语言程序设计——循环程序设计】鸡兔同笼问题(头歌实践教学平台习题)【合集】
本教程介绍了循环控制和跳转语句的使用,包括 `for`、`while` 和 `do-while` 循环,以及 `break` 和 `continue` 语句。通过示例代码详细讲解了这些语句的应用场景,并展示了如何使用循环嵌套解决复杂问题,如计算最大公因数和模拟游戏关卡选择。最后,通过鸡兔同笼问题演示了穷举法编程的实际应用。文中还提供了编程要求、测试说明及通关代码,帮助读者掌握相关知识并完成任务。 任务描述:根据给定条件,编写程序计算鸡和兔的数量。鸡有1个头2只脚,兔子有1个头4只脚。
697 5