热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

浅谈尾调用和尾递归(C语言)

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

什么是尾调用

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

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

比如有个函数叫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; //参数错误则返回0
else if (n == 0 || n == 1)
return 1;
else
return 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; //参数错误则返回0
else if (n == 0)
return 1;
else if (n == 1)
return product_from_n;
else
return 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


推荐阅读
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文介绍了九度OnlineJudge中的1002题目“Grading”的解决方法。该题目要求设计一个公平的评分过程,将每个考题分配给3个独立的专家,如果他们的评分不一致,则需要请一位裁判做出最终决定。文章详细描述了评分规则,并给出了解决该问题的程序。 ... [详细]
  • 原文地址:https:www.cnblogs.combaoyipSpringBoot_YML.html1.在springboot中,有两种配置文件,一种 ... [详细]
  • Nginx使用AWStats日志分析的步骤及注意事项
    本文介绍了在Centos7操作系统上使用Nginx和AWStats进行日志分析的步骤和注意事项。通过AWStats可以统计网站的访问量、IP地址、操作系统、浏览器等信息,并提供精确到每月、每日、每小时的数据。在部署AWStats之前需要确认服务器上已经安装了Perl环境,并进行DNS解析。 ... [详细]
  • 基于PgpoolII的PostgreSQL集群安装与配置教程
    本文介绍了基于PgpoolII的PostgreSQL集群的安装与配置教程。Pgpool-II是一个位于PostgreSQL服务器和PostgreSQL数据库客户端之间的中间件,提供了连接池、复制、负载均衡、缓存、看门狗、限制链接等功能,可以用于搭建高可用的PostgreSQL集群。文章详细介绍了通过yum安装Pgpool-II的步骤,并提供了相关的官方参考地址。 ... [详细]
  • Skywalking系列博客1安装单机版 Skywalking的快速安装方法
    本文介绍了如何快速安装单机版的Skywalking,包括下载、环境需求和端口检查等步骤。同时提供了百度盘下载地址和查询端口是否被占用的命令。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • 本文介绍了RPC框架Thrift的安装环境变量配置与第一个实例,讲解了RPC的概念以及如何解决跨语言、c++客户端、web服务端、远程调用等需求。Thrift开发方便上手快,性能和稳定性也不错,适合初学者学习和使用。 ... [详细]
author-avatar
勿忘
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有