函数递归加强理解&画图讲解详细理解透彻(适合初学者&重新温习)

前言😽😽💖🌻🌻🌻🌻

hello,各位小伙伴,大家来到函数递归这一部分内容,说明大家对知识的向往是非常热情的😁😁(因为这部分知识理解是有一丢丢难度的😊),所以接下来希望大家可以仔细阅读完本篇文章,同时也希望大家能够分享一些自己对于这部分知识的理解!

好啦,话费不多说,咋们直接上干货!😽😽😽😽


首先我们要知道什么是递归?

递归:是指程序调用自身的编程技巧,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

并且我们要知道,递归的主要思考方式是:把大事化小,小事化了

递归两个必要条件:

  • 存在限制条件,当满足这个限制条件时,递归不再继续;

  • 每次递归调用后,越来越接近这个限制条件;

递归的优缺点:

优点:利用递归大大减少了程序代码只需少量的程序就可描述出解题过程所需要的多次重复计算;😽

递归可以把大事化小;😽

对于一些复杂的题目利用递归可以更高效进行解决;还有就是为以后数据结构知识做一个铺垫.😽

缺点:如果函数递归使用不恰当,会导致栈溢出,因为每一次函数调用都会在栈区上申请内存空间,这样可能在一直开辟新的空间,最终导致栈空间耗尽;

每一次函数都会在函数栈帧上开辟一块空间,所谓的压栈。这样会大大降低我们代码的执行效率(这会在函数递归例题详解:斐波那系数中解释)😽😽😽


接下来我们通过实例给大家把递归这一章知识给讲解清楚

史上最简单的递归:😽

#include<stdio.h>

int main()
{
    printf("搁浅\n");
    main();
    return 0;
}

通过运行结果我们可以知道,这个递归结果是个死循环(死递归),所以这个最易递归代码是有问题的,但这同时也验证了我们上述所讲解的递归的缺点,就是栈溢出,因为我们在不断的给代码开辟新的空间,直到栈溢出。


  1. 画图讲解递归经典题,

接受一个整型值(无符号),按照顺序打印它的每一位,比如,输入1234,打印1 2 3 4 .😽😽

先给大家展示是代码:

#include<stdio.h>

void print(int n)
{
    if(n>9)
    {
        print(n / 10);
    }
    printf("%d ", n % 10);
}
int main()
{
    int n = 0;
    scanf("%d", &n);
    print(n);
    return 0;

}

👇👇👇画图解析:

在这个图中,我还写了一个比较重要的知识点,就是递归有两个含义,递归中的递是指递推,归是指回归

图中,红线是指每一次的递推绿线代表着回归,,我们结合这个图还可以分析到递归的两个必要条件,这个大家一定不要望,这个相当于是我们想到递归的一个重要思想,我们可以发现每次递归调用时,我们离我们设定的限制条件越来越近了,然后当我们满足这个限制条件时,我们的递推将不再进行了,程序开始回归了。

打印结果👉👉👉:


接着我们再来一个例题,可能一个例题感觉不过瘾,那么我们接着干😋😋😋。

2.eg:递归求字符串长度

编写函数不允许创建临时变量,求字符串的长度。这个题目有这个要求不允许创建临时变量,按道理我们常规思想应该就是下面这个代码:

#include<stdio.h>

int my_strlen(const char* arr)
{
    int count = 0;
    while(arr[count] != '\0')
    {
        count++;

    }
    return count;
    
}
int main()
{
    char arr[] = "abcde";
    int ret = my_strlen(arr);
    printf("%d\n", ret);
    return 0;
}

这个就是利用循环进行判断每一个字符是否为'\0',如果不是就count++,一直循环到'\0'为止,但是是我们不允许创建临时变量的,所以我们就不能定义一个int型变量count。

接下来我们利用递归思想好好解决这题😊😊😊

因为要求字符串长度,那么我们以'\0'为一个限制条件,不断的接近他,直到我们满足这个条件那么我们的递归调用将不再进行,下面给大家展示代码:

#include<stdio.h>

int my_strlen(const char* arr)
{
    
    if (*arr != '\0')
    {
        return 1 + my_strlen(arr + 1);
    }
    else
        return 0;
}
int main()
{
    char arr[] = "abcde";
    int ret = my_strlen(arr);
    printf("%d\n", ret);
    return 0;
}

画图进行分析:😽😽

在这个程序中我们没有利用到任何临时变量,在这里可能有同学不太懂*arr,这个*arr是指这个arr这个空间的值,这部分我在指针一节会在后续博客中详细讲解,我用一个图给大家展示,这样大家对于这份代码理解更加透彻:

目前大家只需要理解,arr是指这个位置的空间,而arr*是指arr这个空间里的值。

代码运行结果:


3. eg:求n的阶乘。

(不考虑溢出)😽😽😽😽

在高中和初中我们就接触过,关于n!的知识,1!=1,2!=1*2, 3!=1*2*3,4!=1*2*3*4,我们通过这些我们可以得到一个规律,就是3!=3*2!,4!=4*3!,所以我们n!=n * (n-1)!,知道这部分内容那么我们的题目就好解了

所以我们得代码如下:👇👇👇👇

#include<stdio.h>

int fac(int n)
{
    if (n < 2)
    {
        return n;
    }
    else
        return fac(n - 1) * n;
}
int main()
{
    int n = 0;
    scanf("%d", &n);
    int ret = fac(n);
    printf("%d\n", ret);
    return 0;
}

运行结果:👇👇👇


在上述递归缺点中我们讲解到了,关于斐波那契数,这个可以将我们递归得一些缺点表示出来,在这道题中我们可以看到,当我们要求一个比较大的数值的时候我们代码得执行效率就比较低下了😰😰😰😰

为什么这样说呢,接下来我们来用代码解释这是为什么。

4.eg:斐波那契数

在讲解斐波那契数时,我们首先要知道什么是斐波那契数:🌻🌻🌻

像这样由前面两个数相加为第三个数的值就是斐波那契数,说到这里我相信很多同学肯定想到了,这不就是像上面那个n!,没错这两题的思路都是差不多的💐💐💐💐💐

#include<stdio.h>

int fib(int n)
{
    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);
    return 0;
}

但是我们通运行代码时我们会发现,当我们想知道第50个斐波那契数时运行结果是这样的:(如下)

相信大家和我一样有这样的疑问,为什么这个结果一直没有运行出来,这就是因为递归的一个弊端,效率在这个程序中效率太低了,但是这个并不是运行不出来,我们观察的过程中这个代码是一直在跑的,只是这个数太大太大了,所以它可能需要很久才能将结果运行出来。

下面我给大家画一个图给大家理解:

在这个图中我们可以知道,第50个斐波那契数,那是需要相加多少个数?并且有些数需要重复使用到多少次,我们知道2的10次方就是1028,所以可想而知,这个工作量是需要多大,所以其效率很低,当然我们也可以看看任意一个数需要使用到多少次,比如我们计算3这个斐波那契数,代码如下🌟🌟🌟:

🌟

#include<stdio.h>

int count = 0;//全局变量
int fib(int n)
{
    if (n == 3)//统计3使用了多少次
    {
        count++;
    }
    if (n <= 2)
    {
        return 1;
    }
    else
        return fib(n - 1) + fib(n - 2);
    return count;
}
int main()
{
    int n = 0;
    scanf("%d", &n);
    int ret = fib(n);
    printf("%d\n", ret);
    printf("%d\n", count);
    return 0;
}

运行结果🌸🌸🌸🌸:

可以看出光3这个数我们就要使用317811次,所以这个代码效率是很低下的。

所以我们要怎么解决这个问题呢?

迭代求解:

递归解决不了我们肯定要想非递归的方式进行解决,也称为迭代求解

那具体思路是怎样的呢?接下来我给大家画图讲解:

🌻🌻🌻🌻代码如下:

int main()
{
    int n = 0;
    int a = 1;
    int b = 1;
    int c = 0;
    scanf("%d", &n);
    if (n == 1|| n == 2)
    {
        printf("%d", 1);
    }
    while (n > 2)
    {
        c = a + b;  //三者之间进行相互交换,注意交换顺序不要反了
        a = b;
        b = c;
        n--;  交换完后进行n--,直到退出循环
    }
    printf("%d\n", c);

    return 0;

}

🌸🌸🌸🌸🌸运行结果:

我们运行代码中就可以看到,我们使用非递归的时候运行速度和运行效率都是比使用递归时更高。

所以在这题斐波那契数中我们发现递归的一些缺陷:

  • 我们在上述讲了每一次函数都会在函数栈帧上开辟一块空间,所谓的压栈。这样会大大降低我们代码的执行效率因此在我们这题中我们需要开辟很多空间去计算我们每一次的分支后的数,我们在上述中讲过光3这个数我们就需要利用317811次,所以可想而知我们所有数是需要开辟非常大的空间,所以我们代码运行效率是十分低下的。

  • 在这题中我们发现了递归中的一些缺点,但同时是我们也知道在非递归的一些优势,比如在这题中我们利用非递归我们就可以不需要每一个数重复使用那么多次,这大大的提升了我们代码运行效率。😽😽😽😽

递归和非递归(迭代)的比较

  • 我们在递归中,我们可以发现使用递归时,思路更为清晰,代码也更为简单,可能只需要几行代码就可以出结果,但同时也有一个问题,就是在一些习题中,我们的运行效率可能会很低下(斐波那契数)

  • 但是这些问题(斐波那契数)的迭代实现往往比递归实现效率更高,虽然迭代代码的可读性稍微差些


下面给大家两个例题大家自己去实现:

  1. 汉诺塔问题

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

其实我们可以简单类比于这个问题就是,我们只能利用1 2 3这三根木棍,将1中的几个方块,移放到2上,这个问题思路也是挺好想的,大家自己实现。

  1. 青蛙跳台阶问题(有点类比于斐波那契数,青蛙可以一次性跳一阶也可以调两阶,也是分情况,这个大家去实现)

好啦各位小伙伴们,今日分享就到这,后续我们还会继续更新汉诺塔和青蛙跳台阶问题,大家可以点点小赞和关注😁😁😁😁😁😁😁

小火车继续前进~~~~

  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值