什么是尾调用

在计算机科学里,尾调用是指一个函数里的最后一个动作是一个函数调用的情形,即这个调用的返回值直接被当前函数返回的情形。这种情形下称该调用位置称为“尾位置”。

说得通俗点,尾调用就是指某个函数的最后一步是调用另一个函数。这个调用位置称为“尾位置”。

比如有个函数叫fun,其实现是:

int fun(void)
{foo();
}

上面代码中,函数fun的最后一步是调用函数foo,这就叫尾调用。

以下三种情况,哪种属于尾调用?

// 情况一
int fun(void)
{int a = foo();return a;
}// 情况二
int g(void)
{return 3+foo();
}// 情况三
int s(int x)
{if (x > 0)return m(x);return r(x);
}

情况一是调用函数foo之后,还有别的操作,所以不属于尾调用,即使语义一样。

情况二在调用后也有别的操作,所以不属于尾调用,即使写在同一行。

情况三中,不管x取什么值,最后一步操作都是函数调用,所以属于尾调用。

尾调用优化

函数调用会在栈上形成一个”栈帧”(frame stack),用来保存函数参数、返回地址、局部变量等信息。如果函数A调用函数B,那么在A的栈帧下方(假设栈从高地址向低地址生长),还会形成B的栈帧。等到B函数返回,B的栈帧才会消失。如果函数B又调用了函数C,那么B的栈帧下方又形成C的栈帧。以此类推,所有的栈帧堆叠起来,就形成了一个”调用栈”(call stack)。如下图所示:

由于尾调用是外层函数的最后一步操作,尾调用返回后,外层函数也就返回了。执行尾调用的时候,外层函数栈帧中保存的调用位置、内部变量等信息都不会再用到了,所以,可以用内层函数(即尾调用函数)的栈帧覆盖外层函数的栈帧(而不是在外层函数栈帧下面再新开一个),这样可以节省栈空间。这就叫做”尾调用优化”(Tail call optimization).

如果你觉得抽象,可以举个例子:

int f(void)
{int m = 1;int n = 2;return g(m + n);
}

对于以上代码,f();等同于g(3);调用g(3)之后,函数f()就结束了。所以执行到g(3)的时候,完全可以用g(3)的栈帧覆盖f()的栈帧。

尾递归

若一个函数在尾位置调用自身,则称这种情况为尾递归。尾递归是递归的一种特殊情形。

尾递归优化

当编译器检测到尾递归的时候,它就覆盖当前的栈帧而不是在栈中去创建一个新的。无论调用多少次,只要每次都将栈空间覆盖(或重用),其空间占用就是一个常数,即O(1).所以,尾递归优化使原本O(n)的调用栈空间只需要O(1).

递归和尾递归的比较

我们以求阶乘为例,比较一下递归和尾递归的不同。

递归写法

int factorial(int n)
{if(n < 0)return 0;  //参数错误则返回0else if (n == 0 || n == 1)return 1;elsereturn n * fact(n - 1);
}

假设计算factorial(5),那么栈空间的变化如下:

factorial(5)
5 * factorial(4)
5 * (4 * factorial(3))
5 * (4 * (3 * factorial(2)))
5 * (4 * (3 * (2 * factorial(1))))
5 * (4 * (3 * (2 * 1)))
5 * (4 * (3 * 2))
5 * (4 * 6)
5 * 24
120

可观察,栈从左到右,增加到一个峰值,然后从右到左缩小。

尾递归写法

int factorial_tail(int n, int product_from_n)
{if (n < 0)return 0; //参数错误则返回0else if (n == 0)return 1;else if (n == 1)return product_from_n;elsereturn factorial_tail(n - 1, n * product_from_n);}

初次接触这种写法一定会觉得很别扭,求n!为什么要传入2个参数呢?先不着急回答,我们先模拟计算机算一遍。

如果求n!,则需要传入参数n和1(为什么?后文会说明).所以5!=factorial_tail(5,1);
计算过程如下:

factorial_tail(5,1)
factorial_tail(4,5*1) = factorial_tail(4,5)
factorial_tail(3,4*5*1) = factorial_tail(3,20)
factorial_tail(2,3*4*5*1) = factorial_tail(2,60)
factorial_tail(1,2*3*4*5*1) = factorial_tail(1,120)
120

factorial_tail(x,y)这个函数的作用是求((x!)*y).
显而易见,当x==1时,结果就是y,所以有

else if (n == 1)return product_from_n;

当x!=1的时候,将(x!)*y恒等变形,变为(x-1)!*(x*y),所以调用factorial_tail(x,y)就变成了调用factorial_tail(x-1,x*y)

于是有代码中的

 return factorial_tail(n - 1, n * product_from_n);

欲求n的阶乘,当然要使y=1,所以,5!=factorial_tail(5,1);

不难看出,这种思路的本质是:将单次计算的结果缓存起来,作为参数传递给下一次调用,每一次调用都离最终结果近了一步,相当于是迭代。

本来还想从汇编代码的角度分析一下尾递归优化,囿于篇幅,只能下次谈了。

参考资料
[1]https://zh.wikipedia.org/wiki/%E5%B0%BE%E8%B0%83%E7%94%A8
[2]http://www.voidcn.com/article/p-qdsabmbw-xk.html
[3]http://www.ruanyifeng.com/blog/2015/04/tail-call.html

浅谈尾调用和尾递归(C语言)相关推荐

  1. python打开方式错误_浅谈python 调用open()打开文件时路径出错的原因

    昨晚搞鼓了一下python的open()打开文件 代码如下 def main(): infile =open("C:\Users\Spirit\Desktop\bc.txt",'r ...

  2. python 调用 .netcore api_浅谈Python调用XBee的API来进行通讯

    浅谈Python调用XBee的API来进行通讯 用python编程来控制串口(COM口),来让一对XBee进行通讯.不需要借助终端来发送和接收数据,增大了XBee使用的灵活性.这才是使用XBee模块的 ...

  3. python open找不到文件的原因_浅谈python 调用open()打开文件时路径出错的原因

    昨晚搞鼓了一下python的open()打开文件 代码如下 def main(): infile =open("C:\Users\Spirit\Desktop\bc.txt",'r ...

  4. ondestroy什么时候调用_尾调用和尾递归

    尾调用 1. 定义 尾调用是函数式编程中一个很重要的概念,当一个函数执行时的最后一个步骤是返回另一个函数的调用,这就叫做尾调用. 注意这里函数的调用方式是无所谓的,以下方式均可: 函数调用: func ...

  5. 浅谈 TypeScript【下】-- TypeScript 语言规范与基本应用

    文章内容输出来源:拉勾教育 大前端高薪训练营 前言 在 [浅谈 TypeScript[上]]中,简单讲述了关于JavaScript静态类型检查工具Flow的用法等.可以看到,我们接下来讲述的TypeS ...

  6. ES6 尾调用和尾递归优化

    尾调用 尾调用(Tail Call)是函数式编程的一个重要概念,就是指某个函数的最后一步是调用另一个函数. function fun(x){return a(x); } 上面代码中,函数fun的最后一 ...

  7. 『软件工程13』浅谈面向对象方法,统一建模语言UML

    浅谈面向对象方法UML 一.UML的含义 二.UML的主要内容 1.UML的概念模型 2.UML概念模型图例 三.UML的基本构造块 1.UML中的事物 (1)UML中的四种事物 (2)UML中各种事 ...

  8. java:浅谈axis调用webservice接口

     [申明:此代码已经经过测试可以正确使用:但内容解释为个人见解,如有不准确之处,请指教.              阅读前请先仔细阅读"[]"中的说明文字,以免与您的需求不符而 ...

  9. 浅谈普通递归和尾递归

    问题: 以求解阶乘为例, 普通递归函数和尾递归函数有何不同? [从递归调用过程来看] 普通递归函数在递归前进阶段中, 不断将规模较大的问题分解为规模较小的同类子问题, 例如计算3!, 我们要先计算 2 ...

  10. 浅谈面向对象思想下的 C 语言

    如何使用OO思维方式 面向对象(object Oriented,简称:OO)在于用"找对象"的方式去规划和描述问题. 一.怎样"找对象" (思维过程) &quo ...

最新文章

  1. txt或者csv数据文件的格式是有要求的,如下shell代码中说明。
  2. [转][自勉]程序员困境:底层编码能力正逐步丧失
  3. 发送意图到浏览器以打开特定的URL [重复]
  4. tableau必知必会之用 Fixed 函数实现客户回购分析
  5. 顺序表应用3:元素位置互换之移位算法
  6. C语言再学习 -- dmesg 命令
  7. map集合的常用方法和遍历
  8. C语言-函数的指针/函数指针/回调函数
  9. 录屏软件|录屏软件下载|录屏软件哪个好用电脑无水印版
  10. 等额本金和等额本息还款方式的差异分析
  11. ArcEngine修改像素值与像元值
  12. HMC_Hamiltonian Monte Carlo 推导,代码
  13. 摸鱼刷题||听说打工和摸鱼更配
  14. setuptools-scm was unable to detect version for‘…/…/某git包‘
  15. tomcat的startup.bat启动成功了,但是页面加载不了
  16. 信息差副业小项目,高利润,新手日入500+
  17. spring boot mybatis 日志打印配置
  18. “晴耕 · 白话”栏目上线
  19. 根据mask绘制contour ,bounding box。批量展示图片 等工具函数
  20. 【面试题】深复制与浅复制的区别

热门文章

  1. 停车场web项目(内含有数据库)
  2. 如何“谨慎”使用“数据驱动”的风控模型(三)——监控篇
  3. JVM进阶(六):鲜为人知的二次标记
  4. JVM垃圾回收的二次标记
  5. 本周(12.23-12.29)半价电子书
  6. java 拼图_Java 9:“拼图计划终于给了我们急需的Java安全带”
  7. 笔记本和利用服务器算力直连,使用闲置服务器的CPU算力挖掘Monero—Windows篇
  8. 浅析RTB和RTA(一)
  9. 书籍推荐-游戏程序员的学习之路
  10. CSS Cascading Style Sheets 层叠样式表:CSS了解 (一)