每周学点大数据 | No.9递归——以阶乘为例

时间:2022-05-07
本文章向大家介绍每周学点大数据 | No.9递归——以阶乘为例,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

No.9期

递归——以阶乘为例

Mr. 王:我们介绍一个在计算机算法设计和程序设计中都非常常见的概念——递归。

小可:什么是递归呢?

Mr. 王:从程序设计的角度来说,递归就是一个函数,在它的定义中调用了它本身。从算法的角度来说,递归就是一个算法对于一个输入的求解需要对这个算法在更小输入上求解的情况。

小可:这个说法听起来有点复杂啊。

Mr. 王:我们举个例子来说明吧。你一定听说过有一个数学概念叫作阶乘。

小可:我知道,阶乘就是把一个正整数一直乘以它的值减1,直到乘数为1,比如5!=5×4×3×2×1。推广到n的情况就是n!=n×(n−1)×(n−2)×…×3×2×1(特殊的,0!=1)。

Mr. 王:在计算机中求解一个数的阶乘,就可以利用递归。因为阶乘具有一个很有意思的特征,就是:n!=n×(n−1)!。假如我们把阶乘定义为f(n)的话(也就是f(n)=n!),就有f(n)=n×f(n−1)。

小可:哦,从阶乘的定义来看,就是我们想知道f(n),就要知道f(n−1)是多少,推广下去,想知道f(n−1)就要知道f(n−2),一直到f(1)。

Mr. 王:从递归的定义来看,求阶乘这个算法是不是正好符合求对于一个输入n的解,需要求取这个算法在一个更小的输入n−1上的解,而对于n−1的解需要知道去求取n−2的解。

小可:嗯,从这个角度来看,这种求递归的算法确实是一个递归算法。

Mr. 王:如果要设计一个程序,我们也可以书写一个求递归的函数的伪代码:

int f(int n)
{
  if (n==1 || n==0)
    {
      return 1;
    }
   else
return f(n-1);
}

小可:原来函数还可以这样定义啊。

Mr. 王:是的,C/C++语言是非常典型的支持递归的语言。一些早期的语言不支持递归,不过现在很多程序设计语言都支持递归算法的设计。虽然所有的递归算法都可以设计成非递归的版本,比如阶乘,我们可以用一个循环来实现:

int f(int n)
{
  if (n==1 || n==0)
    {
      return 1;
    }
  else
  {
     for (int i=1;i <= n;i++)
     {
        ans *=1;
     }
     return ans;
  }
}

但是递归往往可以更加直观地表达算法的思路,这是非常有利于算法实现和程序设计的。不过有一点需要注意,设计不好的递归算法是非常容易出现无限循环的,在设计递归算法时,一定要设计递归的终点。比如在阶乘中,我们必须指定递归最终会达到的结果f(1)=1 ;否则程序就会一直执行下去,直到内存溢出。

小可:嗯,我懂什么是递归了,但是这和栈有什么关系呢?在递归算法中也没有发现栈的存在啊?

Mr. 王:递归算法和栈的联系非常紧密,虽然在递归程序中我们并没有直接定义出一个栈,但程序运行的内部却会帮我们生成一个栈,这对于递归算法的运行是必要的。现在我们就以阶乘为例来剖析递归算法是如何运行的。

比如我们要求5的阶乘,也就是f(5)。这时程序内的一个栈空间会开始工作,这个空间叫作函数调用栈。程序会将f(5)压栈:

Call stack :[top= f(5)]

求解f(5)时,程序发现需要知道f(4),就把f(4)压栈:

Call stack :[top= f(4)][f(5)]

求解f(4)时,程序发现需要知道f(3),就把f(3)压栈:

Call stack :[top= f(3)][f(4)][f(5)]

依此类推,最后栈中会形成这样一种情况:

Call stack :[top= f(1)][f(2)][f(3)][f(4)][f(5)]

此时,程序发现f(1)的值我们知道了,f(1)=1。所以我们得到了f(1)的解,f(1)这个函数返回1,已经解决的问题或者说已经返回的函数就会弹出调用栈:

Call stack :[top=f(2)][f(3)][f(4)][f(5)]

然后,程序发现f(1)我们知道了,f(2)也就知道了,f(2)=2×f(1)。f(2)返回了值2,f(2)得到解决之后再将f(2)移出栈:

Call stack :[top=f(3)][f(4)][f(5)]

依此类推,程序发现f(2)我们知道了,f(3)也就知道了,f(3)=3×f(2)。f(3)返回了值6,相应地,f(3)得到解决之后再将f(3)移出栈:

Call stack :[top=f(4)][f(5)]

不断地执行下去,就能够得出f(5)的值为120,此时栈空,程序结束。

不难看出,在运行递归程序时,栈一直在工作。因为我们调用函数的嵌套关系恰好满足先到的问题后得到结果、先调用的函数最后返回这样的关系,所以语言的设计者们就利用这一点,用栈结构来表示函数的调用关系。

小可:原来是这样,虽然看不见,但栈一直存在于我们设计的递归和函数调用程序之中。

Mr. 王:是的,栈这种看似简单的数据结构,其实应用是非常广泛的。

这里再谈谈以递归实现算法的缺点。递归程序虽然能够非常有效地表达程序的思路,使得程序的书写变得非常简洁,易于理解,但它的运行速度和执行同样工作的非递归版本相比往往是比较慢的,如果对程序的执行效率有要求,则可以将递归版本重写为非递归的。另外,递归程序在实际的执行过程中执行了多少层递归是不容易预测的。我们知道,前面提到的调用栈也是在计算机的内存空间中,如果递归的层次非常深,就会导致调用栈占用的内存空间被占满,无法继续下一层递归的运行,这就是很多人说的栈溢出或者说“爆栈”,栈溢出会导致程序运行崩溃,所以递归也并不是十全十美的。还是一定要对程序的运行环境进行评估,选择设计递归或者非递归版本的程序。

内容来源:灯塔大数据