目录
前言
递归(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个斐波那契数,我将在下一篇博客中进行讲解,敬请期待。