【C语言】函数递归

目录

前言

递归的思想:

写递归的必要条件:

举例说明

求n的阶乘:

打印整数的每一位:

分析:

补充知识:

尾递归:

递归的优缺点:

递归的缺点:

递归的优点:


前言

递归(recursion)的++概念++ 很简单:如果一个函数调用了自己 ,我们就可以说这个函数是递归的(recursive)。有些编程语言依赖递归,有的却根本不允许使用递归,C语言介于这两种情况之间,C语言允许递归,但其实不一定常用得上。

递归有时难以捉摸,有时却很方便。结束递归是递归的难点 ,当一个递归代码中没有终止递归的条件时,这个函数就会无限递归下去。而每一次函数调用都要为这一次调用分配内存空间,是从内存的栈区上分配的,如果无限递归,就会将栈区空间用完,这时就出现了我们所谓的栈溢出

可以使用循环的地方通常都可以使用递归。有时用循环解决问题比较好,但有时用递归会更好。递归的代码往往更简洁 ,但效率却没有循环高。

递归的思想:

其实本质上就是把一个复杂的问题层层转化为一个与原问题相似,但规模较小的问题来求解,直到问题不能再被拆分,递归就结束。

其实有点像剥一颗洋葱,一层层剥开,越来越接近最小的那层。

写递归的必要条件:

1.递归存在限制条件,满足时便不再继续。

2.每次递归之后,越来越接近这个限制条件。

注意,对于如何自己写出一个递归函数,最主要的抓手就是递归的必要条件和思想!

举例说明

光这样说肯定没有什么实感,那么现在就来通过例子感受一下到底什么是递归:

求n的阶乘:

递归最简单、常用的例子之一无疑是求n!,n!是指自然数n的阶乘,即:n!=1*2*3...(n-2)*(n-1)*n。在用递归求解这个问题之前,我们必须先知道公式n!=n×(n-1)!,解释一下就是5的阶乘为1*2*3*4*5,而4的阶乘为1*2*3*4,所以我们可以看出5的阶乘也可以表示为4的阶乘再乘一个5,也就是n。

那么我们可以写成求n的阶乘的代码,将其封装成一个递归的函数:

cpp 复制代码
//写一个函数实现以递归的方式求n!
int recursion(int n)
{
	if (n<=1)
		return 1;//归
	else 
		return n * recursion(n - 1);//递
}


int main()
{
	int ret = recursion(5);
	printf("%d\n", ret);//打印看看5的阶乘结果
}

vs运行效果:

在这个例子中,我们的限制条件就是n<=1,当我们的n越来越小直到<=1时,就不再继续" "下去,而是开始""了,返回时也是一层层返回的:

示意图:

打印整数的每一位:

现在,让我们再来看一个例子:输入一个整数,要求按照顺序打印整数的每一位。

加入我们输入整数1234,我们如何打印出:1 2 3 4呢?

分析:

首先,我们应该想到把这个打印的实现代码++封装++ 写成一个函数。不妨就叫Print(),因为这个函数只是要我们打印,++无需返回值++ 。又因为我们会向它传递我们要打印的整数,所以函数的++参数++为一个整数。

那么我们就可以得到初步的框架:

cpp 复制代码
void Print(int n)
{
    //
}

上面我们说过递归的思想:把一个复杂的问题层层转化为一个与原问题相似,但规模较小的问题来求解,直到问题不能再被拆分。所以我们应该着重去想,如何将打印整数转化为更"小"的相似问题呢?其实,我们逐个打印1234可以拆分为先逐个打印123再打印4,又可以继续拆分下去。

那么现在的问题就是,我们怎么把1234拆开呢?我们这时会想到%,1234%10,得到的是4,然后/10可以得到123,123再%10得到3......拿到一位,去掉一位。我们可以从低位到高位,一次拿到一位。这时你可能会疑惑,我们是要以1 2 3 4高位到低位的顺序打印,可现在我们是先拿低位,这怎么办呢?

这时你可别忘了递归的特性,是先递再归。什么意思呢?

当我写出了下面的代码:

cpp 复制代码
void Print(int n)
{
	if (n > 9)
		Print(n / 10);
	printf("%d ", n % 10);//在找到结束条件之前,一直无法往下执行到这一步
						  //在逐层"归"的过程中,恰好是从高位到低位打印
}


int main()
{
	Print(1234);
	return 0;
}

这种写法巧妙地利用了递归的特性,把printf("%d ", n % 10);写在判断条件后面,只有当n<=9,也就是达到我们递归的结束条件时我们才不会进入if语句,也就是不再"递",终于能执行下面的printf语句,然后因为是从更深的层次往回递归,所以恰好从高位到低位,打印成1 2 3 4。

vs运行效果:

补充知识:

在这里补充一些递归相关的知识:

尾递归:

尾递归是指函数在最后一步调用自身,或者说将递归调用置于函数的末尾。且这个调用语句不依赖于任何额外的计算。

尾递归是最简单的递归形式,因为它相当于循环。上面提到的阶乘就是一个尾递归的例子。

尾递归具有优化性质 ,因为递归调用是当前活跃期内最后一条待执行的语句,没有表达式等待它的返回结果,因此当这个调用返回时栈帧中并没有其他事情可做,也就没有保存栈帧的必要了。编译器检测到尾递归时,会覆盖当前的活动记录而不是在栈中创建新的记录,从而大大缩减了所使用的栈空间,提高了运行效率,避免了++栈溢出++ 的问题。这使得在需要进行++大量递归++ 的场景下使用尾递归可以提高程序的++性能和效率++。

递归的优缺点:

其实,上面的求n!也可以用循环的方式来写:

cpp 复制代码
int recur(int n)
{
    int ret;
    for(ret=1;n>1;n--)
        ret*=n;//ret=ret*n;
    return ret;
}


int main()
{
    int r = recur(5);
    printf("5! = %d\n",r);
    return 0;
}

vs运行效果:

而第二个例子,按顺序打印整数每一位也可以以循环(或者说迭代)的方式来写,只要将每一位存入一个整型数组,最后逆序输出就行。这里就不具体展示了。

递归的缺点:

所以我们递归和循环到底用哪一个呢?一般而言,循环更好。因为每次递归都会创建一组变量,如求n!中每次函数调用都有自己的变量,虽然变量名都是n但其实值不相同。创建的新变量放在栈中,直到递归不再继续,开始回归,才逐层释放空间。所以递归使用的内存比循环多。

若递归的层次太深,会浪费过多空间,可能引起栈溢出

此外,每次函数调用需要一定的时间,所以递归的执行速度更慢。

递归的优点:

递归能够为某些编程问题提供最简单的解决方案。有些问题一看就很适合递归,比如求n!,用循环去做反而没有那么好想。

其实关于递归的优缺点,有一个很经典例子可以体现,就是求第n个斐波那契数,我将在下一篇博客中进行讲解,敬请期待。

相关推荐
从以前14 分钟前
【算法题解】Bindian 山丘信号问题(E. Bindian Signaling)
开发语言·python·算法
不白兰17 分钟前
[代码随想录23回溯]回溯的组合问题+分割子串
算法
持续更新中~39 分钟前
Visual Studio 2022 && opencv 环境配置
ide·visual studio
御风@户外1 小时前
质数生成函数、质数判断备份
算法·acm
闻缺陷则喜何志丹1 小时前
【C++动态规划】1105. 填充书架|2104
c++·算法·动态规划·力扣·高度·最小·书架
Dong雨1 小时前
六大排序算法:插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序
数据结构·算法·排序算法
析木不会编程2 小时前
【C语言】动态内存管理:详解malloc和free函数
c语言·开发语言
达帮主2 小时前
7.C语言 宏(Macro) 宏定义,宏函数
linux·c语言·算法
是十一月末2 小时前
机器学习之KNN算法预测数据和数据可视化
人工智能·python·算法·机器学习·信息可视化
chenziang12 小时前
leetcode hot100 路径总和
算法