如果使用循环控制结构可以完成相同的任务,为什么要使用递归?

时间:2014-04-22 19:22:52

标签: java recursion

当我开始学习递归时,不同的问题正在我脑海中浮现。递归为堆栈使用更多内存,并且由于维护堆栈通常会更慢。

如果我仍然可以使用for循环,使用递归有什么好处?我们描述了在条件为真之前反复重复的操作,我们可以使用递归或for循环。

为什么我会选择递归版本,而我可以选择更快的控制结构?

8 个答案:

答案 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