C语言--9.函数的递归

在C语言中,对于一个函数而言,最大的特征就是可以调用别人,或者被别人调用,从而实现函数,那么我们来思考一个问题,函数可以被自己调用吗?

答案是可以的,一个函数对于自己的调用,这就被称为函数的递归,是一种算法的思想,那么就让我们一起来认识一下函数的递归吧!

函数的递归

什么是递归?
程序调用自身的编程技巧称为递归( recursion )。 递归做为一种算法在程序设计语言中广泛应
用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复
杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可
描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。 递归的主要思考方式在
于:把大事化小
 
递归的两个必要条件
存在限制条件,当满足这个限制条件的时候,递归便不再继续。
每次递归调用之后越来越接近这个限制条件。

 

递归是有限的:所有递归问题的子问题是可以用相同逻辑去解决;且递归的问题规模会不断缩小,直到缩小成一个很简单的问题,不需要递归便可实现

那么为什么递归一定要是有限的呢?

下面我们来通过几个例子来认识一下递归

1.函数的阶乘

int factorial(int n) {//递归实现
 if(n = 1){
 return 1;
}
 return n* factorial(n-1);
}
***********************
int factorial2(iny n) {//迭代实现
    int result = 1;
    while(n){
      result *=n;
        n--;
    }
}

我们可以发现,事实上,递归的代码写起来非常简洁,但是却不容易想得到,那么我们如何去深入理解递归呢?

现在,我们根据这个函数的阶乘这个简单的例子来认识一下递归,在这里我们需要进入到内存的角度去剖析递归到底是如何实现的。

这时候我们需要介绍很多的概念,最左边的就是我们所看到的内存(进程地址空间),大致可以化为这个样子,从下往上(地址依次增加)可以分为,代码区,静态常量区,已初始化全局数据区,未初始化全局数据区,堆区以及栈区,而在我们这章,主要来认识下栈区,这里简单介绍下栈,栈具有的最大的特点便是先进后出,以其后进先出,就像羽毛球桶,先被压进去的羽毛球永远最后才能被拿出来,而最后进去的羽毛球永远先被拿出来,我们可以看到,在我们的栈空间中它是可以不断向下拓展的,这是因为我们在实现函数调用的时候,都是在栈上开辟的空间。

在这个图中,我们先调用了main函数,形成了main函数的栈帧,而在main函数中,我们又定义了result变量,所以resule变量便在main函数的栈帧上开辟了。那么当我们调用factorial(递归函数)时,因为栈是从上往下拓展的,所以我们的factorial函数栈帧便在main函数下开辟了,而fact(简写)函数中又定义了临时变量n,这个n便被创建在了main与fact两个栈桢中间(而栈帧大小则是由函数中创建的临时变量个数以及类型而进行估算)此时我们需要明确的概念便是:当函数被调用之时,在栈上依次开辟空间,当调用完毕时,依次退栈(被释放),退回主程序,在这过程中,临时变量在函数被调用时被开辟空间定义,而在函数调用完毕时随着函数一同退栈(被释放),这也就是为什么临时变量具有临时性的原因

那么在函数被调用时底层是如何实现的呢,我们可以通过阅读汇编代码来观察

可以看到,函数的调用就是在进栈移栈出栈的过程,更加印证了函数时在栈中开辟并调用的这一结论

补充:在函数调用,栈帧被创建时,需要开辟空间,这时候便会有时间与空间的耗费,而退出函数,销毁栈帧时同样也需要时间成本,总结起来就是,函数的调用是有成本的,所以不可以无限制调用

有了我们以上的基础概念,那么我们再继续回到我们的递归函数factorial,看看它具体是如何实现的呢?

在我们的fact函数中,当我们输入形参5时,第一个调用的时fact(5),而在我们fact(5)中,却又有fact(4),我们又开始调用fact(4),以此类推,直到调用到fact(1),进入到if中,return了1;此时,我们开始了函数的返回,当fact(1)=1被返回时,进入了fact(2)中,return 了2*1;而此时fact(2)也被返回,返回到了fact(3)中,return 了3*2*1,以此类推,直到返回到fact(5)返回最后的结果.最后回到main函数的调用,保存在result中总结起来就是:调用时从上往下调用(不断形成栈帧),返回时再依次从下往上返回(不断释放栈帧)并完成计算,从规模最小的问题开始返回,直到返回到最大的问题得到结果

这时候我们在思考一下我们最初的那个问题

为什么函数的递归只能是有限递归?答:栈结构是向下增长的,若无限制的递归,就会无限制的形成栈帧,而又因为栈空间有限(最大不能向下拓展占用到堆),就会形成栈溢出报错,所以不能进行无限制的递归

2.求字符串长度

#include <stdio.h>
#include<Windows.h>
int MyStrlen1(const char *s){//利用指针与循环依次判断并计数
	int len = 0;
	while (*s!='\0'){//迭代实现
		s++;
		len++;
	}
	return len;
}
int MyStrlen2(const char *s){//递归实现
    if('\0'==*s){
      return 0;
    }
    return 1+MyStrlen(s+1);
int main()
{
	char *str = "hello world!";
	int len1 = MyStrlen1(str);
    int len2 = MyStrlen2(abcd);
	printf("%d\n", len1);//12
    printf("%d\n", len2);//4
	system("pause");
		return 0;
}

这便是这道题的递归实现过程,通过将问题不断缩小,最后依次返回

3.无符号整型,打印每一位

#include <stdio.h>
void ShowUint(int n) {
 if(n>9)
 {
 ShowUint(n/10);
 }
 printf("%d ", n%10);
}
int main()
{
 int num = 1234;
 ShowUint(num);//1 2 3 4
 return 0; }

此题仅用了三行代码便完成了需求,这大大的体现了递归的代码简洁性

4.斐波那契数列(求第n个斐波那契数的值)

int fib(int n) {
 if (n == 2||n==1)         
 return 1;
    else
    return fib(n - 1) + fib(n - 2);
}
但是我们发现 有问题
在使用 fib 这个函数的时候如果我们要计算第 50 个斐波那契数字的时候特别耗费时间。
使用 factorial 函数求 10000 的阶乘(不考虑结果的正确性),程序会崩溃。

 

当我们对这个程序进行分析时会发现,在递归调用时他是一个二叉树的结构,中间会有许许多多的重复调用,这大量的浪费了计算机时间与空间内存成本

总的来说:递归本身因天然需要函数调用,势必引起效率低下(时间),甚至栈溢出问题(空间)

递归使用要有度!如果其他的解决方案可以解决,尽量使用其他成本更小的方案

那如何解决这个问题呢,此时我们便可以使用迭代的方法去实现这个斐波那契数列

int Fib(int n){
	int first = 1;
	int second = 2;
	int third = 1;
	while (n>2){
		third = second + first;
		first = second;
		second = third;
		n--;
	}
	return third;
}

相对于递归方式而言,迭代版本的斐波那契数组没有重复计算,也没有多次的函数调用,仅在一个栈帧中进行的,所以效率高

通过上面的几个递归例子,相信我们对递归已经有一个初始的认识了吧,但是如果想要深刻理解递归,这就需要我们将语言,操作系统,数据结构预算法等都学完,再加上很多项目的经验,才可以真正的从深层理解递归,递归地学习学无止境,一起加油吧!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值