递归与尾递归
关于递归操作,相信大家都已经不陌生。简单地说,一个函数直接或间接地调用自身,是为直接或间接递归。例如,我们可以使用递归来计算一个单向链表的长度:
package com.jiaozg.algorithm.tailrecursively;
public class Node {
private int value;
private Node next;
public Node(int value, Node next) {
this.value = value;
this.next = next;
}
public int getValue() {
return value;
}
public Node getNext() {
return next;
}
}
编写一个递归的GetLength方法:
public static int GetLengthRecursively(Node head)
{
if (head == null) return 0;
return GetLengthRecursively(head.getNext()) + 1;
}
在调用时,GetLengthRecursively方法会不断调用自身,直至满足递归出口。对递归有些了解的朋友一定猜得到,如果单项链表十分长,那么上面这个方法就可能会遇到栈溢出,也就是抛出StackOverflowException。这是由于每个线程在执行代码时,都会分配一定尺寸的栈空间(Windows系统中为1M),每次方法调用时都会在栈里储存一定信息(如参数、局部变量、返回地址等等),这些信息再少也会占用一定空间,成千上万个此类空间累积起来,自然就超过线程的栈空间了。不过这个问题并非无解,我们只需把递归改成如下形式即可(在这篇文章里我们不考虑非递归的解法):
public static int GetLengthTailRecursively(Node head, int acc)
{
if (head == null) return acc;
return GetLengthTailRecursively(head.getNext(), acc + 1);
}
GetLengthTailRecursively方法多了一个acc参数,acc的为accumulator(累加器)的缩写,它的功能是在递归调用时“积累”之前调用的结果,并将其传入下一次递归调用中——这就是GetLengthTailRecursively方法与GetLengthRecursively方法相比在递归方式上最大的区别:GetLengthRecursive方法在递归调用后还需要进行一次“+1”,而GetLengthTailRecursively的递归调用属于方法的最后一个操作。这就是所谓的“尾递归”。与普通递归相比,由于尾递归的调用处于方法的最后,因此方法之前所积累下的各种状态对于递归调用结果已经没有任何意义,因此完全可以把本次方法中留在堆栈中的数据完全清除,把空间让给最后的递归调用。这样的优化1便使得递归不会在调用堆栈上产生堆积,意味着即时是“无限”递归也不会让堆栈溢出。这便是尾递归的优势。
有些朋友可能已经想到了,尾递归的本质,其实是将递归方法中的需要的“所有状态”通过方法的参数传入下一次调用中。对于GetLengthTailRecursively方法,我们在调用时需要给出acc参数的初始值:
GetLengthTailRecursively(head, 0)
为了进一步熟悉尾递归的使用方式,我们再用著名的“菲波纳锲”数列作为一个例子。传统的递归方式如下:
如果对“菲波纳锲”不熟的话,可以参考我以前写的一篇博文
http://jiaozhiguang-126-com.iteye.com/blog/1678806
public static int FibonacciRecursively(int n)
{
if (n < 2) return n;
return FibonacciRecursively(n - 1) + FibonacciRecursively(n - 2);
}
而改造成尾递归,我们则需要提供两个累加器:
public static int FibonacciTailRecursively(int n, int acc1, int acc2)
{
if (n == 0) return acc1;
return FibonacciTailRecursively(n - 1, acc2, acc1 + acc2);
}
参考:http://www.cnblogs.com/JeffreyZhao/archive/2009/03/26/tail-recursion-and-continuation.html
分享到:
相关推荐
Java8 使用 lambda 实现 Java 的尾递归 Java8 使用 lambda 实现 Java 的尾递归是 Java8 中一个重要的知识点。本篇文章主要介绍了 Java8 使用 lambda 实现 Java 的尾递归的相关资料,需要的朋友可以参考下。 什么是...
6. **尾递归优化**:某些Java编译器支持尾递归优化,这允许在不增加额外堆栈空间的情况下进行递归调用。不过,Java标准版目前并不默认开启这一优化,但在Java 14及以上版本的JDK中,可以使用`@jdk.internal.vm....
遗憾的是,标准的Java并不支持尾递归优化。 6. **调试递归**:由于递归涉及多个函数调用,调试递归函数可能会比较复杂。使用调试器的步进功能可以帮助理解每个递归层级的执行过程。 举例来说,计算阶乘可以使用...
因此,应尽可能优化递归深度,例如通过尾递归或迭代实现。 3. **记忆化**:对于重复计算的递归问题,可以使用缓存(也称为记忆化)存储已计算的结果,避免重复计算,提高效率。 总之,Java递归的排序和查找是算法...
但并不是所有递归都比迭代慢,比如在尾递归优化的情况下,递归性能可以接近迭代。 3. **可读性**:递归代码往往更简洁,易于理解,但过度使用或不恰当使用可能导致“递归地狱”,使代码难以理解和调试。 4. **问题...
清单1展示了在Java中使用尾递归计算整数集合迭代器元素乘积时的一个例子,但由于初始积累值设置错误,会导致程序抛出异常。这个问题的修复并不改变尾递归的本质,即递归调用在函数末尾,没有其他操作。 清单2中,...
对于非尾递归的子线程,父线程在启动递归子线程后,可以通过调用子线程的`join()`方法来等待子线程的完成,这有助于同步线程之间的操作。然而,在并行递归算法中,我们往往希望避免这种同步,以最大化并行度。为此,...
因此,有时候通过“尾递归”优化或是将递归改写为迭代来提高效率是必要的。 递归与迭代在Java中不仅可以应用于基础算法的实现,还可以应用于更复杂的数据结构和算法,例如树的遍历、图的搜索等。在处理这类问题时,...
9. **优化递归**:讲解如何通过记忆化(memoization)或尾递归优化来减少重复计算和提高性能。 10. **递归的边界条件**:强调设置正确的边界条件以避免无限递归和堆栈溢出错误的重要性。 通过这些视频,学习者将...
Java 1.8及以上版本支持尾递归优化。 6. **实际应用**: - 文件系统遍历:递归地遍历目录结构。 - 图的深度优先搜索(DFS):使用递归访问节点及其相邻节点。 - 数据结构操作,如树、图、队列、堆等。 7. **...
在Java中,如果没有尾递归优化(如Haskell等语言支持),大量递归可能导致性能问题,甚至引发`StackOverflowError`异常。 相比之下,迭代通常使用循环结构(如`while`、`for`或`for-each`)来解决问题。迭代通常比...
在开发过程中,一定要注意递归深度限制,防止栈溢出错误,可以适时使用尾递归优化或者分步处理大数据量的JSON字符串。 总之,JSON字符串递归解析是处理复杂JSON数据结构的关键技术。通过自定义递归函数,我们可以...
- **递归效率**:对于一些问题,递归可能不是最优解,尤其是在没有考虑缓存中间结果的情况下(这被称为尾递归优化)。在这些情况下,使用迭代或其他算法可能更加高效。 - **调试递归程序**:调试递归程序可能较为...
- **尾递归优化**:某些编译器或解释器支持尾递归优化,可以避免因递归调用产生的额外开销,但Java标准版(JVM)目前不支持尾递归优化。 总的来说,递归是Java编程中的重要概念,熟练掌握递归算法可以帮助解决许多...
标题“所有BOM列表_VBA连接BOM表的递归展开至最后一阶”表明这是一个使用VBA脚本创建的Excel工作簿,其功能是通过连接到Oracle数据库来获取BOM数据,并递归地展开所有层级,直到达到最底层的组件。 这个过程涉及到...
14. **尾递归优化**:虽然Java目前没有原生支持尾递归优化,但理解这个概念对于编写高效递归函数很重要。尾递归是递归函数的最后一步是调用自身的情况,理论上可以优化成循环以避免栈溢出。 以上知识点只是Java函数...
Java 递归是一种编程技术,它允许函数在执行过程中调用自身来解决复杂问题。这种方法基于一个问题的解决方案可以通过解决规模更小的...了解何时使用递归以及如何优化递归(如尾递归)是每个Java开发者应该掌握的技能。
Java不支持尾递归优化,所以在此场景下不推荐使用。 6. **闭式公式**: 虽然斐波那契数列本身没有简单的闭式公式,但是可以利用矩阵方法得到一个公式F(n) = (1/sqrt(5)) * ((1 + sqrt(5))/2)^n - (1/sqrt(5)) * (...
(5的阶乘)不会遇到性能问题,但当n变得非常大时,应考虑使用迭代方法或者优化的递归算法,如尾递归,以避免不必要的性能损失。 总结,递归在Java中是一个重要的编程技巧,可以用来解决各种问题,包括计算阶乘。...
但需要注意,递归可能导致栈溢出,因此需谨慎使用,并考虑优化,如尾递归或者迭代。 3. **迭代算法**: 相比递归,迭代通常更节省资源,因为它避免了重复的函数调用。迭代常用于循环结构,如for、while。在Java中...