递归还是迭代?


226

如果我们在算法可以使用相同目的的情况下使用循环而不是递归,反之亦然吗?例如:检查给定的字符串是否是回文。我已经看到许多程序员使用递归来证明简单的迭代算法何时可以胜任。编译器在决定使用什么时起重要作用吗?


4
@Warrior并非总是如此。例如,使用国际象棋程序,可以更轻松地读取递归。国际象棋代码的“迭代”版本并没有真正帮助提高速度,并且可能使其变得更加复杂。
Mateen Ulhaq 2011年

12
为什么锤子要比锯子更受青睐?锥子上的螺丝刀?凿钻上的凿子?
韦恩·康拉德

3
没有收藏夹。它们都是工具,每个工具都有自己的目的。我会问:“迭代比迭代更擅长于哪种问题,反之亦然?”
韦恩·康拉德

9
“递归到底有什么好处?” ...就是递归了。; o)
Keng

9
错误的前提。递归不好;实际上,这非常糟糕。任何编写健壮软件的人都将尝试消除所有递归,因为除非可以进行尾调用优化或对数限制或类似级别的级别数,否则递归几乎总是会导致不良的堆栈溢出
R .. GitHub停止帮助ICE,

Answers:


181

取决于递归函数是否为尾递归(最后一行是递归调用),递归可能会更昂贵。尾递归应该由编译器识别,并针对其迭代对等进行优化(同时保持代码中简洁明了的实现)。

我将以最有意义的方式编写该算法,并且对于必须在几个月或几年内维护代码的可怜的傻瓜(无论是您本人还是其他人)最清晰。如果遇到性能问题,请先对代码进行概要分析,然后再转向迭代实现,然后再进行优化。您可能需要研究记忆动态编程


12
可以通过归纳证明其正确性的算法倾向于以递归形式自然地编写自己。再加上编译器优化了尾递归,您最终会看到更多递归表达的算法。
Binil Thomas

15
回复:tail recursion is optimized by compilers但并非所有的编译器都支持尾递归..
凯文·梅雷迪思

347

循环可以提高程序的性能。递归可以为您的程序员提高性能。选择哪种在您的情况下更重要!


3
@LeighCaldwell:我认为这正是我的总结。可怜无所不能并没有改变。我当然有 :)
安德·特纳

35
您是否知道您因回答短语而被书中引荐?哈哈 amazon.com/Grokking-Algorithms-illustrated-programmers-curious/…–艾比
Aipi)

4
我很喜欢这个答案..我喜欢这本书“所著的Grokking算法”)
马克斯

因此,至少我和341个人阅读了Grokking Algorithms书!
zzfima

78

将递归与迭代进行比较就像将十字螺丝刀与平头螺丝刀进行比较。在大多数情况下,您可以卸下任何平头的十字头螺钉,但是如果您使用了专为该螺钉设计的螺丝刀,那会更容易吗?

某些算法由于其设计方式而适合进行递归(斐波那契序列,遍历树状结构等)。递归使算法更加简洁和易于理解(因此可共享和可重用)。

此外,某些递归算法使用“惰性评估”,这使其比迭代兄弟更有效。这意味着它们仅在需要时进行昂贵的计算,而不是每次循环运行时都进行。

那应该足以让您入门。我也将为您挖掘一些文章和示例。

链接1: Haskel vs PHP(递归vs迭代)

这是程序员必须使用PHP处理大型数据集的示例。他展示了使用递归在Haskel中进行处理有多么容易,但是由于PHP没有简单的方法来完成相同的方法,因此他被迫使用迭代来获得结果。

http://blog.webspecies.co.uk/2011-05-31/lazy-evaluation-with-php.html

链接2:掌握递归

递归的不良声誉多数来自命令性语言的高成本和低效率。本文的作者讨论了如何优化递归算法以使其更快,更有效。他还介绍了如何将传统循环转换为递归函数,以及使用尾端递归的好处。他的闭幕词确实总结了我的一些主要观点:

“递归编程为程序员提供了一种更好的方式来组织代码,这种方式既可维护又在逻辑上是一致的。”

https://developer.ibm.com/articles/l-recurs/

链接3:递归比循环快吗?(回答)

这是一个类似于您的stackoverflow问题答案的链接。作者指出,很多有两种递归关联或循环的基准是非常特定语言。命令性语言通常使用循环更快,而使用递归则更慢,反之亦然。我想从这个链接中得出的主要观点是,很难以语言不可知论/情境盲目的回答问题。

递归比循环快吗?


4
非常喜欢螺丝刀的类比
jh314


16

递归在内存中的开销更大,因为每个递归调用通常都需要将一个内存地址压入堆栈-以便以后程序可以返回到该点。

尽管如此,在很多情况下,递归比循环更自然,更易读-例如使用树时。在这些情况下,我建议您坚持递归。


5
当然,除非您的编译器优化了Scala之类的尾调用。
本·哈迪

11

通常,人们会期望性能损失落在另一个方向上。递归调用可能导致构造额外的堆栈帧;惩罚因人而异。同样,在某些语言(如Python)(更正确的说,在某些语言的某些实现中...)中,对于可能需要递归指定的任务,例如在树数据结构中查找最大值,您很容易遇到堆栈限制。在这些情况下,您确实想坚持使用循环。

编写良好的递归函数可以在一定程度上降低性能损失,前提是您拥有优化尾部递归的编译器,等等。(还要仔细检查以确保该函数确实是尾部递归-这是很多人犯错的事情之一上。)

除了“边缘”情况(高性能计算,非常大的递归深度等)之外,最好采用能最清楚地表达您的意图,精心设计并且可维护的方法。仅在确定需要后进行优化。


8

对于可以分解为多个较小部分的问题,递归优于迭代。

例如,要创建递归Fibonnaci算法,您可以将fib(n)分解为fib(n-1)和fib(n-2)并计算这两个部分。迭代仅允许您一次又一次地重复单个功能。

但是,斐波那契实际上是一个残破的例子,我认为迭代实际上更有效。请注意,fib(n)= fib(n-1)+ fib(n-2)和fib(n-1)= fib(n-2)+ fib(n-3)。fib(n-1)被计算两次!

一个更好的例子是一棵树的递归算法。分析父节点的问题可以分解为分析每个子节点的多个较小问题。与斐波那契示例不同,较小的问题彼此独立。

所以是的-对于可以分解为多个,较小,独立,相似的问题的问题,递归优于迭代。


1
通过记忆实际上可以避免两次计算。
悉达多

7

使用递归时,性能会下降,因为以任何语言调用方法都意味着大量准备工作:调用代码发布了一个返回地址,调用参数,某些其他上下文信息(例如处理器寄存器)可能会保存在某个位置,并且在返回时会被调用的方法发布一个返回值,然后调用者将其取回,并且先前保存的所有上下文信息都将被恢复。迭代方法和递归方法之间的性能差异在于这些操作花费的时间。

从实现的角度来看,当处理调用上下文所花费的时间与方法执行所花费的时间相当时,您才真正开始注意到差异。如果您的递归方法需要更长的时间才能执行,则调用上下文管理部分,请采用递归方式,因为代码通常更具可读性和易懂性,并且您不会注意到性能损失。否则出于效率原因进行迭代。


那并非总是如此。在可以进行尾部调用优化的某些情况下,递归与迭代一样有效。stackoverflow.com/questions/310974/...
希德刹帝利


5

在很多情况下,它提供了比迭代方法更为优雅的解决方案,常见的示例是遍历二叉树,因此不一定要更难维护。通常,迭代版本通常会快一些(并且在优化过程中很可能会替换递归版本),但是递归版本更易于理解和正确实现。


5

在某些情况下,递归非常有用。例如,考虑用于查找阶乘的代码

int factorial ( int input )
{
  int x, fact = 1;
  for ( x = input; x > 1; x--)
     fact *= x;
  return fact;
}

现在考虑使用递归函数

int factorial ( int input )
{
  if (input == 0)
  {
     return 1;
  }
  return input * factorial(input - 1);
}

通过观察这两个,我们可以看到递归很容易理解。但是,如果不小心使用它,那么也很容易出错。假设如果我们错过了if (input == 0),那么代码将执行一段时间并通常以堆栈溢出结束。


6
我实际上发现迭代版本更容易理解。我想对每个人来说。
2011年

@Maxpm,一个高阶递归解决方案要好得多:foldl (*) 1 [1..n]就是这样。
SK-logic

5

在许多情况下,由于缓存,递归速度更快,从而提高了性能。例如,这是使用传统合并例程的合并排序的迭代版本。由于缓存提高了性能,因此它的运行速度将比递归实现慢。

迭代实现

public static void sort(Comparable[] a)
{
    int N = a.length;
    aux = new Comparable[N];
    for (int sz = 1; sz < N; sz = sz+sz)
        for (int lo = 0; lo < N-sz; lo += sz+sz)
            merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
}

递归实施

private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi)
{
    if (hi <= lo) return;
    int mid = lo + (hi - lo) / 2;
    sort(a, aux, lo, mid);
    sort(a, aux, mid+1, hi);
    merge(a, aux, lo, mid, hi);
}

PS-这是凯文·韦恩教授(普林斯顿大学)在Coursera上介绍的算法课程中所讲的内容。


4

使用递归,每次“迭代”都会导致函数调用的开销,而使用循环,通常只需支付增量/减量。因此,如果循环的代码比递归解决方案的代码复杂得多,则循环通常优于递归。


1
实际上,编译后的Scala尾递归函数会简化为字节码中的一个循环,如果您愿意看一下的话(推荐)。没有函数调用开销。其次,尾递归函数的优点是不需要可变变量/副作用或显式循环,因此更容易证明正确性。
本·哈迪

4

递归和迭代取决于您要实现的业务逻辑,尽管在大多数情况下,它可以互换使用。大多数开发人员都希望进行递归,因为它更易于理解。



3

如果您只是遍历一个列表,那么可以肯定地,遍历。

还有其他几个答案提到了(深度优先)树遍历。这确实是一个很好的例子,因为对非常常见的数据结构进行处理是很常见的事情。对于此问题,递归非常直观。

在此处查看“查找”方法:http : //penguin.ewu.edu/cscd300/Topic/BSTintro/index.html


3

递归比任何可能的迭代定义都更简单(因此也更基础)。您可以定义仅包含一对组合器的图灵完备系统(是的,即使递归本身也是这种系统中的派生概念)。拉姆达演算是具有同等功能的基本系统,具有递归函数。但是,如果要正确定义迭代,则需要更多的原语。

至于代码-不,实际上,递归代码比纯粹的迭代代码更易于理解和维护,因为大多数数据结构都是递归的。当然,为了正确处理,至少需要一种语言支持高阶函数和闭包-才能使所有标准组合器和迭代器整齐。当然,在C ++中,除非您是FC ++等的核心用户,否则复杂的递归解决方案可能看起来很难看。


递归代码极难遵循,特别是在参数顺序改变或每次递归类型改变的情况下。迭代代码可以非常简单和描述性。重要的是首先为可读性(并因此为可靠性)编码,无论是迭代还是递归,然后在必要时进行优化。
马库斯·克莱门茨

2

我认为在(非尾部)递归中,每次调用函数时都会分配新的堆栈等,这会对性能产生影响(当然取决于语言)。


2

它取决于“递归深度”。它取决于函数调用开销将影响总执行时间的多少。

例如,由于以下原因,以递归方式计算经典阶乘非常低效:-数据溢出的风险-堆栈溢出的风险-函数调用开销占据执行时间的80%

在开发用于下棋游戏中的位置分析的最小-最大算法时,可以在“分析深度”上递归地实现对后续N个动作的分析(如我正在做的^ _ ^)


这里完全同意ugasoft ...这取决于递归深度...及其迭代实现的复杂性...您需要将两者进行比较,看看哪种效率更高...没有这样的经验法则。 ..
rajya vardhan 2011年

2

递归?我从哪里开始,Wiki会告诉您“这是以自相似的方式重复项目的过程”

早在我做C语言的那一天,C ++递归是天赐之物,例如“ Tail recursion”。您还将发现许多排序算法都使用递归。快速排序示例: http //alienryderflex.com/quicksort/

递归就像对特定问题有用的任何其他算法一样。也许您可能不会立即或经常发现用途,但是会有问题,您会很高兴它的可用性。


我认为您已经对编译器进行了优化。编译器将在可能的情况下将递归函数优化为迭代循环,以避免堆栈增长。
CoderDennis

公平地说,这是倒退。但是我不确定这是否仍然适用于尾递归。
Nickz 2013年

2

在C ++中,如果递归函数是模板化的,则编译器有更多机会对其进行优化,因为所有类型推导和函数实例化都将在编译时发生。如果可能的话,现代编译器也可以内联函数。因此,如果使用诸如-O3-O2中的优化标记g++,则递归可能会比迭代更快。在迭代代码中,编译器对其进行优化的机会较小,因为它已处于或多或少的最佳状态(如果编写得足够好)。

就我而言,我试图通过递归和迭代方式使用Armadillo矩阵对象进行平方来实现矩阵求幂。可以在这里找到算法... https://en.wikipedia.org/wiki/Exponentiation_by_squaring。我的函数是模板化的,并且我已经计算出1,000,000 12x12幂的矩阵10。我得到以下结果:

iterative + optimisation flag -O3 -> 2.79.. sec
recursive + optimisation flag -O3 -> 1.32.. sec

iterative + No-optimisation flag  -> 2.83.. sec
recursive + No-optimisation flag  -> 4.15.. sec

这些结果是使用带有c ++ 11标志(-std=c++11)的gcc-4.8 和带有Intel mkl的Armadillo 6.1获得的。英特尔编译器也显示类似结果。


1

迈克是正确的。Java编译器或JVM 并未优化尾递归。您总是会出现如下所示的堆栈溢出:

int count(int i) {
  return i >= 100000000 ? i : count(i+1);
}

3
除非你在斯卡拉;-)写
本·哈迪

1

您必须记住,使用太深的递归会导致堆栈溢出,具体取决于允许的堆栈大小。为避免这种情况,请确保提供一些基本情况以结束递归。


1

递归具有一个缺点,即使用递归编写的算法具有O(n)空间复杂度。迭代方法的空间复杂度为O(1),这是在迭代上使用迭代的优势。那为什么要使用递归呢?

见下文。

有时使用递归编写算法会更容易,而使用迭代编写相同的算法会更困难。在这种情况下,如果您选择遵循迭代方法,则必须自己处理堆栈。


1

如果迭代是原子的并且数量级比推入新的堆栈框架创建新的线程要贵几个数量级,并且您拥有多个内核,并且运行时环境可以使用所有这些内核,那么当与多线程。如果平均迭代次数不可预测,那么最好使用线程池,该线程池将控制线程分配并防止您的进程创建太多线程并占用系统。

例如,在某些语言中,有递归多线程合并排序实现。

但是同样,多线程可以与循环一起使用,而不是与递归一起使用,因此这种组合的工作效果取决于更多因素,包括操作系统及其线程分配机制。


0

据我所知,Perl不会优化尾递归调用,但是您可以伪造它。

sub f{
  my($l,$r) = @_;

  if( $l >= $r ){
    return $l;
  } else {

    # return f( $l+1, $r );

    @_ = ( $l+1, $r );
    goto &f;

  }
}

首次调用时,它将在堆栈上分配空间。然后它将更改其参数,并重新启动子例程,而无需在堆栈中添加任何其他内容。因此,它将假装它从未调用自身,而是将其更改为迭代过程。

请注意,没有“ my @_;”或“ local @_;”,如果您这样做将不再起作用。


0

仅使用Chrome 45.0.2454.85 m,递归似乎要快得多。

这是代码:

(function recursionVsForLoop(global) {
    "use strict";

    // Perf test
    function perfTest() {}

    perfTest.prototype.do = function(ns, fn) {
        console.time(ns);
        fn();
        console.timeEnd(ns);
    };

    // Recursion method
    (function recur() {
        var count = 0;
        global.recurFn = function recurFn(fn, cycles) {
            fn();
            count = count + 1;
            if (count !== cycles) recurFn(fn, cycles);
        };
    })();

    // Looped method
    function loopFn(fn, cycles) {
        for (var i = 0; i < cycles; i++) {
            fn();
        }
    }

    // Tests
    var curTest = new perfTest(),
        testsToRun = 100;

    curTest.do('recursion', function() {
        recurFn(function() {
            console.log('a recur run.');
        }, testsToRun);
    });

    curTest.do('loop', function() {
        loopFn(function() {
            console.log('a loop run.');
        }, testsToRun);
    });

})(window);

结果

//使用标准的for循环运行100次

循环运行100倍。完成时间:7.683ms

//使用带尾递归的功能递归方法运行100次

100x递归运行。完成时间:4.841ms

在下面的屏幕截图中,以每个测试300个周期运行时,递归再次以更大的优势获胜

递归再次获胜!


该测试无效,因为您正在循环函数内部调用该函数-这使循环最显着的性能优势之一就是缺少指令跳转(包括对于函数调用,堆栈分配,堆栈弹出等)的无效。如果在循环中执行任务(不仅仅称为函数)与在递归函数中执行任务,您将获得不同的结果。(PS性能是实际任务算法的问题,有时指令跳转比避免它们所需的计算便宜)。
Myst

0

我发现这些方法之间的另一个区别。它看起来简单而无关紧要,但是在您准备面试时它起着非常重要的作用,因此这个话题出现了,因此请仔细观察。

简而言之:1)迭代后遍历并不容易-这使DFT变得更加复杂2)循环使用递归更容易检查

细节:

在递归的情况下,创建遍历前后很容易:

想象一个非常标准的问题:“当任务依赖于其他任务时,打印应该执行以执行任务5的所有任务”

例:

    //key-task, value-list of tasks the key task depends on
    //"adjacency map":
    Map<Integer, List<Integer>> tasksMap = new HashMap<>();
    tasksMap.put(0, new ArrayList<>());
    tasksMap.put(1, new ArrayList<>());

    List<Integer> t2 = new ArrayList<>();
    t2.add(0);
    t2.add(1);
    tasksMap.put(2, t2);

    List<Integer> t3 = new ArrayList<>();
    t3.add(2);
    t3.add(10);
    tasksMap.put(3, t3);

    List<Integer> t4 = new ArrayList<>();
    t4.add(3);
    tasksMap.put(4, t4);

    List<Integer> t5 = new ArrayList<>();
    t5.add(3);
    tasksMap.put(5, t5);

    tasksMap.put(6, new ArrayList<>());
    tasksMap.put(7, new ArrayList<>());

    List<Integer> t8 = new ArrayList<>();
    t8.add(5);
    tasksMap.put(8, t8);

    List<Integer> t9 = new ArrayList<>();
    t9.add(4);
    tasksMap.put(9, t9);

    tasksMap.put(10, new ArrayList<>());

    //task to analyze:
    int task = 5;


    List<Integer> res11 = getTasksInOrderDftReqPostOrder(tasksMap, task);
    System.out.println(res11);**//note, no reverse required**

    List<Integer> res12 = getTasksInOrderDftReqPreOrder(tasksMap, task);
    Collections.reverse(res12);//note reverse!
    System.out.println(res12);

    private static List<Integer> getTasksInOrderDftReqPreOrder(Map<Integer, List<Integer>> tasksMap, int task) {
         List<Integer> result = new ArrayList<>();
         Set<Integer> visited = new HashSet<>();
         reqPreOrder(tasksMap,task,result, visited);
         return result;
    }

private static void reqPreOrder(Map<Integer, List<Integer>> tasksMap, int task, List<Integer> result, Set<Integer> visited) {

    if(!visited.contains(task)) {
        visited.add(task);
        result.add(task);//pre order!
        List<Integer> children = tasksMap.get(task);
        if (children != null && children.size() > 0) {
            for (Integer child : children) {
                reqPreOrder(tasksMap,child,result, visited);
            }
        }
    }
}

private static List<Integer> getTasksInOrderDftReqPostOrder(Map<Integer, List<Integer>> tasksMap, int task) {
    List<Integer> result = new ArrayList<>();
    Set<Integer> visited = new HashSet<>();
    reqPostOrder(tasksMap,task,result, visited);
    return result;
}

private static void reqPostOrder(Map<Integer, List<Integer>> tasksMap, int task, List<Integer> result, Set<Integer> visited) {
    if(!visited.contains(task)) {
        visited.add(task);
        List<Integer> children = tasksMap.get(task);
        if (children != null && children.size() > 0) {
            for (Integer child : children) {
                reqPostOrder(tasksMap,child,result, visited);
            }
        }
        result.add(task);//post order!
    }
}

请注意,递归后顺序遍历不需要后续结果反转。儿童先打印,问题中的任务打印最后。一切都很好。您可以进行递归的预遍历(也如上所示),并且需要对结果列表进行翻转。

迭代方法不是那么简单!在迭代(单栈)方法中,您只能进行预排序遍历,因此必须在最后对结果数组求逆:

    List<Integer> res1 = getTasksInOrderDftStack(tasksMap, task);
    Collections.reverse(res1);//note reverse!
    System.out.println(res1);

    private static List<Integer> getTasksInOrderDftStack(Map<Integer, List<Integer>> tasksMap, int task) {
    List<Integer> result = new ArrayList<>();
    Set<Integer> visited = new HashSet<>();
    Stack<Integer> st = new Stack<>();


    st.add(task);
    visited.add(task);

    while(!st.isEmpty()){
        Integer node = st.pop();
        List<Integer> children = tasksMap.get(node);
        result.add(node);
        if(children!=null && children.size() > 0){
            for(Integer child:children){
                if(!visited.contains(child)){
                    st.add(child);
                    visited.add(child);
                }
            }
        }
        //If you put it here - it does not matter - it is anyway a pre-order
        //result.add(node);
    }
    return result;
}

看起来很简单,不是吗?

但这是一些采访中的陷阱。

它的含义如下:使用递归方法,您可以实现“深度优先遍历”,然后选择需要的先后顺序(仅通过更改“打印”的位置即可,在本例中为“添加到结果列表”中) )。使用迭代(单堆栈)方法,您可以轻松地仅执行预遍历,因此在需要先打印子项的情况下(几乎所有情况下,您需要从底部节点开始打印,然后向上打印)-您处于麻烦。如果遇到这种麻烦,可以稍后再撤消,但这将是对算法的补充。如果面试官在看他的手表,那对您来说可能是个问题。存在多种进行迭代后序遍历的复杂方法,但是它们并不简单。例:https://www.geeksforgeeks.org/iterative-postorder-traversal-using-stack/

因此,最重要的是:我将在采访中使用递归,它更易于管理和解释。在任何紧急情况下,您都可以轻松地进行从订单遍历到后遍历。有了迭代,您就没有那么灵活了。

我将使用递归,然后说:“好吧,但是迭代可以为我提供对已用内存的更直接控制,我可以轻松地测量堆栈大小并禁止某些危险的溢出。”

递归的另一个优点-避免/注意图中的循环更简单。

示例(preudocode):

dft(n){
    mark(n)
    for(child: n.children){
        if(marked(child)) 
            explode - cycle found!!!
        dft(child)
    }
    unmark(n)
}

0

将其编写为递归或实践可能会很有趣。

但是,如果要在生产中使用该代码,则需要考虑堆栈溢出的可能性。

尾递归优化可以消除堆栈溢出,但是您是否想要解决它的麻烦,并且您需要知道可以依靠它在您的环境中进行优化。

每次算法递归时,数据大小或n减少多少?

如果要减少数据大小,或者n每次递归都减少一半,那么通常就不必担心堆栈溢出。假设,如果程序需要溢出的深度为4,000级或10,000个深度,那么您的程序堆栈的数据大小就需要约为2 4000。从一个角度来看,最近最大的存储设备可以容纳2 61个字节,如果您有2 61个这样的设备,则仅处理2 122个数据大小。如果您查看宇宙中的所有原子,估计它可能小于2 84。如果您需要处理自估计140亿年前的宇宙诞生以来每毫秒的宇宙及其状态中的所有数据,则可能只有2 153。因此,如果您的程序可以处理2 4000个数据单位或n,则可以处理Universe中的所有数据,并且该程序不会堆栈溢出。如果您不需要处理最大为2 4000(4000位整数)的数字,那么通常不必担心堆栈溢出。

但是,如果降低数据或大小n恒定的量每次递归的时候,那么你就可以在你的程序运行良好,当遇到堆栈溢出n1000,但在某些情况下,当n仅仅变成20000

因此,如果您有堆栈溢出的可能性,请尝试使其成为迭代解决方案。


-1

我将通过“归纳”设计Haskell数据结构来回答您的问题,这是递归的“双重”。然后,我将展示这种双重性如何带来美好的事物。

我们介绍一种简单树的类型:

data Tree a = Branch (Tree a) (Tree a)
            | Leaf a
            deriving (Eq)

我们可以将这个定义读为“一棵树是一个分支(包含两棵树)或一棵叶子(包含数据值)”。因此,叶子是一种最小的情况。如果一棵树不是叶子,那么它必须是包含两棵树的复合树。这些是唯一的情况。

让我们来做一棵树:

example :: Tree Int
example = Branch (Leaf 1) 
                 (Branch (Leaf 2) 
                         (Leaf 3))

现在,让我们假设我们要在树中的每个值上加1。我们可以这样调用:

addOne :: Tree Int -> Tree Int
addOne (Branch a b) = Branch (addOne a) (addOne b)
addOne (Leaf a)     = Leaf (a + 1)

首先,请注意,这实际上是一个递归定义。它以数据构造函数“分支”和“叶子”为例(并且由于“叶子”是最小的,并且这是唯一可能的情况),因此我们确定函数将终止。

以迭代方式编写addOne会怎样?循环成任意数量的分支会是什么样子?

同样,就“功能”而言,通常可以排除这种递归。我们可以通过定义以下内容使树成为函子:

instance Functor Tree where fmap f (Leaf a)     = Leaf (f a)
                            fmap f (Branch a b) = Branch (fmap f a) (fmap f b)

并定义:

addOne' = fmap (+1)

我们可以考虑其他递归方案,例如代数数据类型的同构(或折叠)。使用变形,我们可以写:

addOne'' = cata go where
           go (Leaf a) = Leaf (a + 1)
           go (Branch a b) = Branch a b

-2

仅当您使用内置内存管理中没有的语言进行编程时,才会发生堆栈溢出。否则,请确保函数中包含某些内容(或函数调用,STDLb等)。如果不进行递归,就不可能拥有Google或SQL之类的东西,或者任何必须高效地对大型数据结构(类)或数据库进行排序的地方。

如果您要遍历文件,则可以使用递归,可以肯定的是,找到* | ?grep *'有效。Kinda双重递归,尤其是在管道上(但是,如果要把它放给别人使用的话,不要像很多人那样做一堆syscall)。

高级语言,甚至clang / cpp都可能在后台实现相同的功能。


1
“仅当您使用内置内存管理中没有的语言进行编程时,才会发生堆栈溢出”。大多数语言使用有限大小的堆栈,因此递归将很快导致失败。
StaceyGirl '17
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.