C语言-什么是尾递归

浅浅的花香味﹌ 2022-06-09 11:49 609阅读 0赞

文章目录

  • 1 尾递归简介
  • 2 总结
  • 3 参考

1 尾递归简介

想必大家都知道递归是什么,第一次接触尾递归,首先要从它的定义说起:

尾递归:当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作。

举一个简单的例子,用递归算阶乘:

  1. int factorial(int n)
  2. {
  3. if(n == 0 || n == 1) {
  4. return 1;
  5. } else {
  6. return n * factorial(n-1);
  7. }
  8. }

现在我们模拟一下程序的过程,例如当 n = 5 的时候:

  1. factorial(5)
  2. 5 * factorial(4)
  3. 5 * (4 * factorial(3))
  4. 5 * (4 * (3 * factorial(2)))
  5. 5 * (4 * (3 * (2 * factorial(1))))
  6. 5 * (4 * (3 * (2 * 1)))
  7. 5 * (4 * (3 * 2))
  8. 5 * (4 * 6)
  9. 5 * 24
  10. 120

上述的递归过程中,“最长”的部分,即代表着此刻占用的栈内存最大,所以在一般的递归中,它的内存占用,先是增大,后来到达一个峰值,然后缩小。

下面给出尾递归的实现方法,看到这段代码后,第一印象会发现函数参数多了一个 tmp,事实上 tmp 代表着 1!(1的阶乘的值)也就是 1 1 1 ,于是在使用函数的时候,第二个参数传递一个 1 1 1 。

  1. int factorial(int n, int tmp)
  2. {
  3. if(n == 0) {
  4. return 1;
  5. } else if (n == 1) {
  6. return tmp;
  7. } else {
  8. return factorial(n - 1, n * tmp);
  9. }
  10. }

我们同样模拟一下程序的过程,例如当 n = 5 的时候:

  1. factorial(5, 1)
  2. factorial(4, 5)
  3. factorial(3, 20)
  4. factorial(2, 60)
  5. factorial(1, 120)
  6. 120

通过这个过程,我们可以发现,它完全对应着尾递归的定义,即当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作

尾递归比线性递归(第一个例子称为线性递归)多一个参数,这个参数是上一次调用函数得到的结果,并且每次递归的时候都会保留着上次递归的结果,它不属于表达式的一部分,而且它需要回归的数据,本身已经通过参数携带,所以回归过程中不用做任何操作,所以,相比较线性递归,尾递归占用的栈内存是恒定的

2 总结

既然尾递归相比线性递归解决了线性递归致命的缺点(stack overflow风险),是不是更提倡使用尾递归呢?

答案不是的,参考数据结构与算法分析-C语言描述中的一个例子,打印一个链表:

  1. /* Bad use of recursion : Printing a linked list */
  2. /* No header */
  3. void PrintList(List L)
  4. {
  5. if (L != NULL) {
  6. PrintElement(L->Element);
  7. PrintList(L->Next);
  8. }
  9. }

在这里使用尾递归就是一个不好的例子,因为没有什么需要存储,在递归结束调用的时候,实际上并没有需要存储的值,因此,我们就可以带着在第一次递归调用中已经用过的那些值, goto 到函数的顶部,下面是改进后的程序(记住,应该更加自然的使用 while 循环结果去去除尾递归,这里使用 goto 只是为了说明编译器是如何自动地去除尾递归)。

  1. /* Printing a linked list non-recursively */
  2. /* Uses a mechanical translation */
  3. /* No header */
  4. void PrintList(list L)
  5. {
  6. top:
  7. if (L != NULL) {
  8. PrintElement(L->Element);
  9. L = L->Next;
  10. goto top;
  11. }
  12. }

不用递归去打印一个链表,事实上尾递归的去除是如此的简单,以至于某些编译器可以自动完成,所以这些工作不用程序员去完成,但是即使如此,最好还是在编写程序时避免出现尾递归。

3 参考

  1. What is tail recursion - StackOverflow?
  2. 数据结构与算法分析:C语言描述(原书第2版) - Mark Allen Weiss

发表评论

表情:
评论列表 (有 0 条评论,609人围观)

还没有评论,来说两句吧...

相关阅读

    相关 什么

    什么是递归 \\递归:\\如果一个函数在内部可以调用其本身,那么这个函数就是递归函数。简单理解:函数内部自己调用自己, 这个函数就是递归函数 \\注意:\\递归函数的作

    相关 C语言实现

    普通递归在执行时需要保存当前数据,并在内存中重新开辟栈。所以普通递归的空间复杂度比较高,效率比较低。所以,我们一般倾向于把递归方式转化成迭代的方式进行计算。但是如果递归语句是整

    相关

    尾递归 如果要说尾递归的话,那么首先应该先说一下递归函数。递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但是循环的逻辑不如递归清晰易理解

    相关

    什么是尾递归 什么是尾递归呢?(tail recursion), 顾名思议,就是一种“不一样的”递归,说到它的不一样,就得先说说一般的递归。对于一般的递归,比如下面

    相关

    尾递归就是说一个递归函数,在return语句中调用了这个递归函数本身,如图所示。从理论上来说,尾递归都可以用非递归的方法实现。 ![这里写图片描述][70] [70]

    相关

    1、递归 简单的来说递归就是一个函数直接或间接地调用自身,是为直接或间接递归。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满