当我开始学习递归时,不同的问题正在我脑海中浮现。递归为堆栈使用更多内存,并且由于维护堆栈通常会更慢。
如果我仍然可以使用for
循环,使用递归有什么好处?我们描述了在条件为真之前反复重复的操作,我们可以使用递归或for
循环。
为什么我会选择递归版本,而我可以选择更快的控制结构?
答案 0 :(得分:5)
递归为堆栈使用更多内存,并且由于维护堆栈通常会更慢
这种说法远非普遍存在。它适用于您不需要将状态保存超过固定数量级别的情况,但这不包括许多可以递归解决的重要任务。例如,如果要在图形上实现depth-first search,则需要创建自己的数据结构来存储否则将进入堆栈的状态。
如果我仍然可以使用for循环,那么使用Recursion有什么好处?
当您将递归算法应用于通过递归最佳理解的任务(例如处理递归定义的结构)时,您会更清晰。在这种情况下,一个循环本身就不够了:你需要一个数据结构来配合你的循环。
为什么在我有更快的控制结构时会选择递归版本?
当您可以使用易于理解的更快控制结构实现相同的算法时,您不一定会选择递归。但是,在某些情况下,您可能希望编写递归来提高可读性,即使您知道可以使用循环对算法进行编码,而无需其他数据结构。 Modern compilers can detect situations like that,并“重写”场景后面的递归代码,使其使用迭代。这使您可以充分利用这两个方面 - 一个符合读者期望的递归程序,以及不会浪费堆栈空间的迭代实现。
不幸的是,演示递归给你带来明显优势的情况的例子需要高级主题的知识,因此许多教育工作者通过使用错误的例子(例如阶乘和斐波纳契数)证明递归来获取快捷方式。一个相对简单的示例是为带括号的表达式实现解析器。你可以用许多不同的方式做到这一点,有或没有递归,但the recursive way of parsing expressions为你提供了一个易于理解的非常简洁的解决方案。
答案 1 :(得分:2)
递归解决方案优于迭代解决方案的最佳示例是tower of Hanoi。考虑以下两种解决方案 -
递归(来自this问题):
public class Hanoi {
public static void main(String[] args) {
playHanoi (2,"A","B","C");
}
//move n disks from position "from" to "to" via "other"
private static void playHanoi(int n, String from , String other, String to) {
if (n == 0)
return;
if (n > 0)
playHanoi(n-1, from, to, other);
System.out.printf("Move one disk from pole %s to pole %s \n ", from, to);
playHanoi(n-1, other, from, to);
}
}
迭代(从RIT复制):
import java.io.*;
import java.lang.*;
public class HanoiIterative{
// -------------------------------------------------------------------------
// All integers needed for program calculations.
public static int n;
public static int numMoves;
public static int second = 0;
public static int third;
public static int pos2;
public static int pos3;
public static int j;
public static int i;
public static void main(String args[]) {
try{
if( args.length == 1 ){
System.out.println();
n = Integer.parseInt(args[0]); //Sets n to commandline int
int[] locations = new int[ n + 2 ]; //Sets location size
for ( j=0; j < n; j++ ){ //For loop - Initially all
locations[j] = 0; //discs are on tower 1
}
locations[ n + 1 ] = 2; //Final disk destination
numMoves = 1;
for ( i = 1; i <= n; i++){ //Calculates minimum steps
numMoves *= 2; //based on disc size then
} //subtracts one. ( standard
numMoves -= 1; //algorithm 2^n - 1 )
//Begins iterative solution. Bound by min number of steps.
for ( i = 1; i <= numMoves; i++ ){
if ( i%2 == 1 ){ //Determines odd or even.
second = locations[1];
locations[1] = ( locations[1] + 1 ) % 3;
System.out.print("Move disc 1 to ");
System.out.println((char)('A'+locations[1]));
}
else { //If number is even.
third = 3 - second - locations[1];
pos2 = n + 1;
for ( j = n + 1; j >=2; j-- ) //Iterative vs Recursive.
if ( locations[j] == second )
pos2 = j;
pos3 = n + 1;
for ( j = n + 1; j >= 2; j-- ) //Iterative vs Recursive.
if ( locations[j] == third )
pos3 = j;
System.out.print("Move disc "); //Assumes something is moving.
//Iterative set. Much slower here than Recursive.
if ( pos2 < pos3 ){
System.out.print( pos2 );
System.out.print(" to ");
System.out.println((char)('A' + third));
locations[pos2] = third;
}
//Iterative set. Much slower here than Recursive.
else {
System.out.print( pos3 );
System.out.print(" to ");
System.out.println((char)('A' + second));
locations[ pos3 ] = second;
}
}
}
}
} //Protects Program Integrity.
catch( Exception e ){
System.err.println("YOU SUCK. ENTER A VALID INT VALUE FOR #");
System.err.println("FORMAT : java HanoiIterative #");
} //Protects Program Integrity.
finally{
System.out.println();
System.out.println("CREATED BY: KEVIN SEITER");
System.out.println();
}
}
}//HanoiIterative
//--------------------------------------------------------------------------------
我猜你没有真正读过那个迭代的。我没有。它复杂得多。你在这里和那里改变一些东西,但最终它总是变得复杂,没有办法解决它。虽然任何递归算法都可以转换为迭代形式,但有时代码要复杂得多,有时甚至效率也会大大降低。
答案 2 :(得分:0)
如何搜索充满子目录的子目录等目录(如JB Nizet所述,树节点)或计算斐波那契序列比使用递归更容易?
答案 3 :(得分:0)
所有算法都可以从递归转换为迭代。最糟糕的情况是,您可以显式使用堆栈来跟踪您的数据(而不是调用堆栈)。因此,如果效率真的是最重要的,并且你知道递归正在减慢显着,那么总是可以回到迭代版本上。请注意,某些语言具有将尾递归方法转换为迭代对应方的编译器,例如Scala。
递归方法的优点在于,大多数时候它们更易于编写和理解,因为它们非常直观。理解和编写递归程序是一种很好的做法,因为许多算法可以通过这种方式自然地表达。递归只是编写表达和正确程序的工具。再一次,一旦你知道你的递归代码是正确的,就可以更容易地将它转换为迭代代码。
答案 4 :(得分:0)
递归通常比其他方法更优雅和直观。但它并不是万能的通用解决方案。
以Fibonacci序列为例。您可以通过递归使用斐波纳契数的定义(以及n == 1的基本情况)找到第n个项。但是你会发现自己不止一次计算第m项(m 使用数组[1,1]并将下一个第i项附加为[i-1] + a [i-2]的总和。你会发现线性算法比另一个快得多。 但你会喜欢递归。 它优雅且通常很强大。
答案 5 :(得分:0)
想象一下,你想要遍历一棵树来按顺序打印一些东西。它可能是这样的:
public void print (){
if( this == null )
return;
left.print();
System.out.println(value);
right.print();
}
要使用while循环进行此操作,您需要执行自己的回溯堆栈,因为它具有不在尾部位置的调用(尽管有一个调用)。它不会像这样容易理解,而IMO在技术上它仍然是递归,因为goto + stack是递归。
如果你的树木不是太深,你就不会打击堆叠,程序也可以运行。没有必要进行过早的优化。我甚至会在更改它之前增加JVM的堆栈以执行它自己的堆栈。
现在,在运行时的未来版本中,即使JVM可以像正常的运行时一样获得tail call optimization。然后尾部位置的所有递归都不会增加堆栈,然后它与其他控制结构没有区别,所以你选择哪个具有最清晰的语法。
答案 6 :(得分:0)
我的理解是,当您的数据集较小,边缘情况最少时,标准迭代循环更适用,并且逻辑条件很容易确定迭代一个或多个专用函数的次数。
更重要的是,递归函数在应用于更复杂的嵌套数据结构时更有用,在这些数据结构中,您可能无法直观或准确地估计需要循环的次数,因为您需要专用函数的次数重申是基于一些条件,其中一些条件可能不是互斥的,并且为了便于阅读和调试,您需要特别考虑调用堆栈的顺序和基本案例的直观路径***
答案 7 :(得分:-6)
建议将递归用于非程序员或初级程序员的原型编程。对于更严肃的编程,你应该尽可能地避免递归。请阅读 NASA coding standard