`

递归替换:尾递归

阅读更多

来自HNUlanwei的CSDN:http://blog.csdn.net/u010911350

 

递归在很多时候被视为洪水猛兽。它的名声狼籍,好像永远和低效联系在一起。 
其实,对一些如树的递归结构,递归算法是又自然又好用。 
如果看看一些用来代替递归的技术,(汉诺塔的迭代算法不去说它,那是真正的算法的革命,除了佩服没啥好说的),一般来说只不过是自己模拟堆栈,编起来费劲,读起来费劲,维护起来更费劲。而模拟堆栈的效果,相比于简单的递归,好处在哪里呢? 
1。不使用进程堆栈,不会耗尽堆栈空间(虽然可能耗尽堆空间) 
2。可以有选择地把真正有用的东西压栈,而不用什么pc, sp, 所有的局部变量都压栈。这样节省了一些内存(不过,仍然在一个数量级,递归是O(N),模拟递归仍然是O(N))。 不过这也不绝对,编译器的优化可以识别一些不需要保存的局部变量的。(这叫变量生存期分析) 


那么这样做又没有坏处呢?除了上面的代码复杂度的问题(我想很多搞嵌入式系统,实时系统的C高手都对此不屑一顾),还有一个效率上的缺点: 
模拟的递归使用堆空间,它的new/delete都比直接在堆栈上分配空间慢得多。而且很容易产生内存碎片。 
所以说,模拟堆栈只不过是牺牲了一定的效率来换取了一部分空间而已。是否值得,嘿嘿,就得看具体应用了。 

好,闲话说完,下面言归正传。 

话说大家都知道函数调用要压栈。不过这是有几个例外的。 
1。函数是inline的。 
2。语言采用的函数调用方式是continuation, 而不是activation record. 这种模式中语言可以使用堆而不是栈来完成函数调用。 
3。尾调用。也就是说,函数调用出现在调用者函数的尾部,因为是尾部,所以根本没有必要去保存任何局部变量,sp, pc的信息。直接让被调用的函数返回时越过调用者,返回到调用者的调用者去。 
比如: 
f(){g();} g(){h();}


这里,h()函数返回时可以绕过g()函数,f()函数而直接返回到f()函数的调用者。 
注意,尾调用优化不是什么很复杂的优化,实际上几乎所有的现代的高级语言编译器都支持尾调用这个很基本的优化。 
实现层面上,只需要把汇编代码call改成jmp, 并放弃所有局部变量压栈处理,就可以了。所以,很简单。 
我们不考虑continuation这种情况,因为c/c++/java等流行的语言都不是这种模式。 
对于递归,inline也不能使用。因为你不知道你会递归调用多少次。 
于是,就剩下递三条:尾调用。而一个对自己本身的递归尾调用,就叫做尾递归。 
那么,当尾递归时,我们就没有前面分析的递归调用的占用堆栈的缺点,因为每次调用都是尾调用,所以堆栈根本就没有被占用,每次调用都是重新使用调用者的堆栈。 
有些看过ml, haskell这种functional language的程序员可能会奇怪为什么他们不支持循环。 
现在让我们看看他们会怎么实现循环的功能。 


考虑一个简单的例子,求从一加到一百的和。 

用递归, 我们也许可以这样做: 

//普通递归
int sum(int start,int end)
{
	if(start>end)
		return 0;
	else
		return start+sum(start+1,end);
}

 


这个递归函数是可以工作的,唯一的缺点是它会占用很多堆栈空间,也会有很大的压栈出栈的开销。 

于是,我们用尾递归: 

//尾递归
int tail_sum(int seed,int start,int end)
{
	if(start>end)
		return seed;
	else
		return tail_sum(seed+start,start+1,end);
}

 


小小的修改,sum变成了尾递归,没有了对堆栈空间的占用,也没有任何压栈开销。 

注意,这里尾调用的“尾”字,是指运行时需要执行的最后一个动作。不是简单的语法字面上的最后一个语句。 
比如说,c++中,如果我的sum返回的不是int, 而是一个带有有副作用的析构函数的类,那么它就不再是尾递归: 

X sum(X seed, X start, X end){ 
	if(start<end) 
		return seed; 
	else 
		return sum(seed+start, start+1, end); 
}

 


几乎同样的代码,只是不同的类型,但这里,它已经不再是尾递归了! 
即使这个X类的拷贝构造和析构函数里面通过所有权转移,move semantics减小了拷贝和析构的开销,但,副作用就是副作用,它不再是尾调用了。

分析了一大堆,那么尾递归到底有什么好处呢?呵呵,可读性好,代码强壮,易维护。 

另外,不是所有的递归都可以变成尾递归的。

 

最后附上Fibonacci数列求解的尾递归:

//普通递归
int Fibonacci(int n)
{
	if(n<2)
		return n;
	else
		return Fibonacci(n-2)+Fibonacci(n-1);
}

//尾递归
int tail_Fibonacci(int n,int a1,int a2)
{
	if(n==0)
		return a1;
	else
		return tail_Fibonacci(n-1,a1+a2,a1);
}

 

 

 

过程模拟请见:http://www.cnblogs.com/Anker/archive/2013/03/04/2943498.html

分享到:
评论

相关推荐

    C语言递归函数的学习与运用

    - 注意递归深度,避免栈溢出,必要时可以使用尾递归或者迭代方式替换。 - 使用递归时要考虑到性能影响,尽量避免在性能敏感的代码段中使用。 6. 尾递归优化: 在某些编译器或解释器中,如果递归调用是函数体内的...

    CRP.zip_CRP_CRP递归图_matlab 递归图_递归 MATLAB_递归分析

    3. **解析分析**:通过图形分析调用模式,找出可能的优化点,比如减少不必要的递归调用,或者通过尾递归优化来提高效率。 递归分析则更进一步,它不仅关注递归的结构,还涉及性能评估。这包括: - **时间复杂度分析...

    recur.rar_MATLAB 递归_matlab递归_recur_recur.m_递归 MATLAB

    - **尾递归**:如果递归调用是函数体的最后一个操作,且没有其他操作依赖于这个递归调用的结果,那么这被称为尾递归。MATLAB R2016b及以后版本支持尾递归优化,可以减少内存消耗。 - **迭代替换**:有时可以通过改写...

    .net递归算法优化源码案例

    通过理解递归的局限性,我们可以采取适当的优化策略,如尾递归优化、记忆化、迭代替换等,来提升代码的效率和可维护性。提供的源码案例展示了未优化的递归与优化后的迭代在解决相同问题时的不同之处,进一步说明了...

    关于尾递归的使用详解

    - **直接替换**:尾递归可以被编译器优化为循环,即每次递归调用都可以被替换为简单的跳转,从而避免了重复的栈帧分配与回收。 - **节省资源**:相比于普通的递归,尾递归极大地减少了栈空间的使用,降低了栈溢出的...

    递归有关程序

    - **尾递归**:在函数的最后一步调用自身,并且返回值仅依赖于这次递归的结果,编译器或解释器可以优化这种情况,使其具有与迭代相同的空间复杂度。 6. **递归的应用**: - **分治策略**:如快速排序、归并排序等...

    递归算法与非递归算法的转换文.pdf

    **直接转换法** 主要用于处理尾递归和单向递归,通过循环结构替换递归调用。尾递归指的是递归调用是函数的最后一步,这种情况下,可以通过将递归调用转换为循环来提高效率。对于单向递归,可以通过设置额外变量存储...

    js代码-递归修改字段

    为避免这种情况,可以考虑使用尾递归优化或迭代方法来代替。 总的来说,JavaScript中的递归是处理复杂数据结构的关键技术,尤其适用于遍历和修改字段。理解递归的概念和正确使用递归,能帮助开发者编写出更加灵活和...

    数据结构及答案

    - **规则6:** 尾递归调用不需要执行入栈操作,因为它是递归函数的最后一个操作。 4. **实例分析:图的深度优先搜索(DFS)的非递归实现:** - 图的深度优先搜索通常使用递归来实现。 - 使用上述转换规则,可以...

    蓝桥杯大赛 青少年创意编程 Python组 资料集-2022.01.21.pdf

    * Python 算法之递归与尾递归:该资源提供了 Python 算法之递归与尾递归的解析和解答,涵盖了递归和尾递归算法的基本概念和应用。 * Python 实现欧拉函数:该资源提供了 Python 实现欧拉函数的解析和解答,涵盖了...

    《C语言程序设计(C99版)》(改后课件)

    - 递归效率:递归可能导致栈溢出,应谨慎使用,并考虑尾递归优化。 3. **结构、联合、位运算和枚举类型** (ch08 结构、联合、位运算和枚举类型.ppt) - 结构体:C语言中的复合数据类型,可以包含多种不同类型的...

    剑指offer(java版67题)

    面试题 2:替换空格(考点: 字符串) 2 面试题 3:从尾到头打印链表(考点: 链表) 2 面试题 4:重建二叉树(考点: 树) 4 面试题 5:用两个栈实现队列(考点: 栈和队列) 5 面试题 6:旋转数组的最小数字(考点:...

    jvm-tail-recursion:Java字节码中的尾部递归调用的优化器库

    例子倒数至零一个简单的尾递归函数,倒数为零:前后static void count( int n) { if (n == 0 ) { return ; } count(n - 1 ); } static void count( int n) { while ( true ) { if (n == 0 ) { return ; } n = n - 1 ...

    复杂度递归

    - **尾递归优化**:在某些情况下,如果递归调用是函数体的最后一个操作,编译器或解释器可以进行尾递归优化,将递归调用替换为跳转,从而避免增加额外的栈空间。这可以将空间复杂度降低到O(1)。 在C++中,理解和...

    yuandaima.rar_RCR

    例如,它可能会介绍如何找到可以被递归替换的代码段,如何定义递归函数,以及如何确保递归的终止条件。递归通常涉及到函数或方法的自身调用,因此理解递归的工作原理是至关重要的,包括递归深度、堆栈溢出问题以及...

    浅论C语言在提高程序执行效率上的编程技巧.pdf

    考虑使用迭代替代递归,或使用尾递归优化(如果编译器支持)。 四、预编译宏与条件编译 4.1 预编译宏:宏定义可以在编译阶段进行文本替换,减少运行时计算。但过度使用可能导致代码难以理解和维护,应谨慎使用。 ...

    面试高频算法题总结-剑指Offer题解

    递归 动态规划 分治 记忆化搜索 贪心 回溯 位运算 数学 设计 其他 共66题 面试题3:数组中重复的数字 面试题4:二维数组的查找 面试题5:替换空格 面试题6:从尾到头打印链表 面试题7:重建二叉树 面试题8:二叉树的...

    深入理解JavaScript中的尾调用(Tail Call)

    在非尾递归版本中,调用栈随着递归深度增加,而尾递归版本则通过传递状态(`a`和`b`)来避免创建新的堆栈帧,从而保持调用栈的常量大小。 需要注意的是,虽然ES6标准中提出了尾调用优化的概念,但JavaScript引擎...

    C#中的递归APS和CPS模式详解

    尾递归优化允许编译器或解释器跳过保存当前函数状态,直接替换为下一次递归调用,从而避免堆栈溢出。CPS尾递归的特点是递归调用发生在函数体的最后,且没有其他操作,这样可以极大地提高程序效率。 总结来说,C#中...

    数据结构(java版)习题解答

    - **解答**:找到第一个链表的尾节点,将其指向第二个链表的头节点。 **习2.7:实验2.2复制单链表** - **题目**:复制一个单链表。 - **解答**:遍历原链表,依次创建新节点,并链接起来。 **习2.8:实验2.2单链表...

Global site tag (gtag.js) - Google Analytics