为什么递归优先于迭代?

时间:2010-02-02 16:08:32

标签: performance language-agnostic recursion iteration

迭代比递归更高效,对吧?那么为什么有些人认为递归比迭代更好(用他们的话来说更优雅)?我真的不明白为什么像Haskell这样的语言不允许迭代并鼓励递归?鼓励表现不佳的东西并不是那么荒谬(当更高性能的选项即递归可用时也是如此)?请详细说明一下。感谢。

18 个答案:

答案 0 :(得分:62)

  

迭代比表现更好   递归,对吗?

不一定。 这个概念来自许多类C语言,其中调用函数,无论是否递归,都有很大的开销,并为每次调用创建一个新的堆栈帧。

对于许多语言而言并非如此,递归与迭代版本相同或更高效。这些天,即使是一些C编译器也会将一些递归构造重写为迭代版本,或者重复使用堆栈帧进行尾递归调用。

答案 1 :(得分:36)

尝试递归和迭代地实现深度优先搜索并告诉我哪一个给你一个更容易的时间。或合并排序。对于很多问题,它归结为显式维护自己的堆栈而不是将数据留在函数堆栈上。

我无法与Haskell交谈,因为我从未使用它,但这是为了解决标题中提出的问题的更一般部分。

答案 2 :(得分:14)

Haskell不允许迭代,因为迭代涉及可变状态(索引)。

答案 3 :(得分:9)

正如其他人所说的那样,递归的本质上没有什么特别之处。有些语言会慢一些,但这不是一个普遍的规则。

话虽如此,对我而言,递归是一种工具,在有意义时使用。有些算法更好地表示为递归(正如有些算法通过迭代更好)。

案例:

fib 0 = 0
fib 1 = 1
fib n = fib(n-1) + fib(n-2)

我无法想象一个迭代解决方案可能会使意图更清晰。

答案 4 :(得分:5)

以下是有关专业人士的一些信息。递归和利弊迭代c:

http://www.stanford.edu/~blp/writings/clc/recursion-vs-iteration.html

对我而言,递归有时比迭代更容易理解。

答案 5 :(得分:4)

有几件事:

  1. 迭代不一定更快
  2. 万恶之源:只是因为它可能适度加快而鼓励一些事情为时过早;还有其他一些考虑因素。
  3. 递归往往更简洁明确地传达您的意图
  4. 通常避免使用可变状态,函数式编程语言更容易推理和调试,递归就是一个例子。
  5. 递归比迭代占用更多内存。

答案 6 :(得分:4)

迭代只是一种特殊的递归形式。

答案 7 :(得分:3)

递归是理论上看起来优雅或高效的东西之一,但在实践中通常效率较低(除非编译器或动态重新编译器)正在改变代码所做的事情。通常,导致不必要的子程序调用的任何事情都会变慢,特别是当推送/弹出多于1个参数时。你可以做的任何事情来删除处理器周期,即处理器必须咀嚼的指令是公平的游戏。这些天编译器总体上可以做得很好但是知道如何手动编写高效的代码总是很好。

答案 8 :(得分:2)

我认为递归没有任何本质上不那么高效的东西 - 至少在摘要中是这样。递归是一种特殊的迭代形式。如果一种语言被设计为能够很好地支持递归,那么它可以像迭代一样执行。

通常,递归使得人们明确了解您在下一次迭代中提出的状态(它是参数)。这可以使语言处理器更容易并行执行。至少这是语言设计者试图利用的方向。

答案 9 :(得分:2)

我发现很难说一个人总是优于另一个人。

我正在开发一个需要在用户文件系统上进行后台工作的移动应用程序。其中一个后台线程需要不时扫描整个文件系统,以便为用户维护更新的数据。因此,由于担心Stack Overflow,我编写了一个迭代算法。今天我写了一个递归的,为同样的工作。令我惊讶的是,迭代算法更快:递归 - > 37s,迭代 - > 34s(在完全相同的文件结构上工作)。

<强>递归:

private long recursive(File rootFile, long counter) {
            long duration = 0;
            sendScanUpdateSignal(rootFile.getAbsolutePath());
            if(rootFile.isDirectory()) {
                File[] files = getChildren(rootFile, MUSIC_FILE_FILTER);
                for(int i = 0; i < files.length; i++) {
                    duration += recursive(files[i], counter);
                }
                if(duration != 0) {
                    dhm.put(rootFile.getAbsolutePath(), duration);
                    updateDurationInUI(rootFile.getAbsolutePath(), duration);
                }   
            }
            else if(!rootFile.isDirectory() && checkExtension(rootFile.getAbsolutePath())) {
                duration = getDuration(rootFile);
                dhm.put(rootFile.getAbsolutePath(), getDuration(rootFile));
                updateDurationInUI(rootFile.getAbsolutePath(), duration);
            }  
            return counter + duration;
        }

迭代: - 迭代深度优先搜索,带递归回溯

private void traversal(File file) {
            int pointer = 0;
            File[] files;
            boolean hadMusic = false;
            long parentTimeCounter = 0;
            while(file != null) {
                sendScanUpdateSignal(file.getAbsolutePath());
                try {
                    Thread.sleep(Constants.THREADS_SLEEP_CONSTANTS.TRAVERSAL);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                files = getChildren(file, MUSIC_FILE_FILTER);

                if(!file.isDirectory() && checkExtension(file.getAbsolutePath())) {
                    hadMusic = true;
                    long duration = getDuration(file);
                    parentTimeCounter = parentTimeCounter + duration;
                    dhm.put(file.getAbsolutePath(), duration);
                    updateDurationInUI(file.getAbsolutePath(), duration);
                }

                if(files != null && pointer < files.length) {
                    file = getChildren(file,MUSIC_FILE_FILTER)[pointer];
                }
                else if(files != null && pointer+1 < files.length) {
                    file = files[pointer+1];
                    pointer++;
                }
                else {
                    pointer=0;
                    file = getNextSybling(file, hadMusic, parentTimeCounter);
                    hadMusic = false;
                    parentTimeCounter = 0;
                }
            }
        }

private File getNextSybling(File file, boolean hadMusic, long timeCounter) {
            File result= null;
            //se o file é /mnt, para
            if(file.getAbsolutePath().compareTo(userSDBasePointer.getParentFile().getAbsolutePath()) == 0) {
                return result;
            }
            File parent = file.getParentFile();
            long parentDuration = 0;
            if(hadMusic) { 
                if(dhm.containsKey(parent.getAbsolutePath())) {
                    long savedValue = dhm.get(parent.getAbsolutePath());
                    parentDuration = savedValue + timeCounter;
                }
                else {
                    parentDuration = timeCounter; 
                }
                dhm.put(parent.getAbsolutePath(), parentDuration);
                updateDurationInUI(parent.getAbsolutePath(), parentDuration);
            }

            //procura irmao seguinte
            File[] syblings = getChildren(parent,MUSIC_FILE_FILTER);
            for(int i = 0; i < syblings.length; i++) {
                if(syblings[i].getAbsolutePath().compareTo(file.getAbsolutePath())==0) {
                    if(i+1 < syblings.length) {
                        result = syblings[i+1];
                    }
                    break;
                }
            }
            //backtracking - adiciona pai, se tiver filhos musica
            if(result == null) {
                result = getNextSybling(parent, hadMusic, parentDuration);
            }
            return result;
        }

当然迭代并不优雅,但是目前它的实现方式还不尽如人意,它仍然比递归的更快。我可以更好地控制它,因为我不希望它全速运行,并且会让垃圾收集器更频繁地完成它的工作。

无论如何,我不会理所当然地认为一种方法比另一种方法更好,并且将审查当前递归的其他算法。但至少从上面的2个算法中,迭代的算法将是最终产品中的算法。

答案 10 :(得分:2)

在Java中,递归解决方案通常优于非递归解决方案。在C中,它往往是相反的方式。我认为这适用于自适应编译语言与提前编译语言。

编辑: “通常”我的意思是像60/40分裂。它非常依赖于语言处理方法调用的效率。我认为JIT编译更倾向于递归,因为它可以选择如何处理内联并在优化中使用运行时数据。它非常依赖于有问题的算法和编译器。特别是Java继续变得更聪明地处理递归。

Java (PDF link)的定量研究结果。请注意,这些主要是排序算法,并使用较旧的Java虚拟机(如果我正确读取,则为1.5.x)。 他们有时通过使用递归实现获得2:1或4:1的性能提升,并且很少递归明显变慢。根据我的个人经验,差异通常不是那么明显,而是50当我明智地使用递归时,%改进很常见。

答案 11 :(得分:1)

我认为这有助于了解性能的真正含义。 This link显示了一个完全合理编码的应用程序实际上有很多优化空间 - 即因子为43!这些都与迭代与递归无关。

当应用程序调整到目前为止时,它会到达通过迭代保存的周期而不是递归的点可能实际上有所不同。

答案 12 :(得分:1)

我会将递归与爆炸物进行比较:你可以立刻达到很大的效果。但如果你谨慎使用它,结果可能是灾难性的。

通过证明计算斐波纳契数here的递归的复杂性,我印象非常深刻。在这种情况下的递归具有复杂度O((3/2)^ n),而迭代仅为O(n)。用c#上的递归计算n = 46需要半分钟!哇...

只有当实体的性质适合于递归(树,语法解析,......)并且从不因为美学时才应该使用恕我直言递归。需要仔细检查任何“神圣”递归代码的性能和资源消耗。

答案 13 :(得分:1)

递归是迭代的典型实现。它只是一个较低的抽象层次(至少在Python中):

class iterator(object):
    def __init__(self, max):
        self.count = 0
        self.max = max

    def __iter__(self):
        return self

    # I believe this changes to __next__ in Python 3000
    def next(self):
        if self.count == self.max:
            raise StopIteration
        else:
            self.count += 1
            return self.count - 1

# At this level, iteration is the name of the game, but
# in the implementation, recursion is clearly what's happening.
for i in iterator(50):
    print(i)

答案 14 :(得分:1)

作为低级别的ITERATION处理CX注册表来计算循环,当然还有数据注册表。 RECURSION不仅处理它还添加了对堆栈指针的引用,以保留先前调用的引用,然后再返回.-

我的大学老师告诉我,无论你用递归做什么都可以通过迭代和反面来完成,但是有时通过递归比迭代(更优雅)更简单,但在性能水平上更好地使用迭代.- < / p>

答案 15 :(得分:0)

“迭代比递归更高效”实际上是语言和/或编译器特定的。想到的情况是编译器进行循环展开时。如果你在这种情况下实现了一个递归解决方案,它将会慢得多。

这是成为一名科学家(测试假设)和了解你的工具所付出的代价......

答案 16 :(得分:0)

  

迭代比递归更高效,对吧?

然而, 当您遇到完全映射到递归数据结构的问题时,更好的解决方案始终是递归的

如果您假装使用 迭代 来解决问题,那么与优雅<相比,您将最终重新发明堆栈并创建更加混乱和丑陋的代码/ em>代码的递归版本。

也就是说, 迭代 总是比 递归 更快。 (在Von Neumann架构中),所以如果你总是使用递归,即使循环就足够了,你也会付出性能损失。

Is recursion ever faster than looping?

答案 17 :(得分:-2)

在ntfs UNC最大路径上为32K
C:\ A \ B \ X \ C ....可以创建超过16K的文件夹......

但你甚至不能用任何递归方法计算文件夹的数量,迟早都会给堆栈溢出。

只应使用良好的轻量级迭代代码来专业扫描文件夹。

不管你信不信,大多数顶级防病毒软件都无法扫描UNC文件夹的最大深度。