何时迭代比递归更具可读性?

时间:2018-06-06 03:51:49

标签: recursion functional-programming iteration procedural-programming

前言

我已经阅读了很多关于为什么更喜欢递归而不是迭代,但我还没有看到任何解释何时更喜欢迭代而不是递归的问题。

动机

当然,我相信没有最好的工具可以解决所有问题。

例如,勺子和叉子都是吃工具,但勺子更适合吃米饭,而叉子更适合吃面条。

显然,如果我说叉子(或勺子)是最好吃的工具,我会变得狂热。

因此,它与递归或迭代相同,它们都是工具,它们各有各的利弊。

然而,在搜索了Googles和StackOverflow之后,虽然我在为什么递归比迭代更具可读性上找到了很多例子,但是当迭代时我仍然没有在上找到任何示例比递归更容易阅读

因此,我试图找出迭代将更加可读的情况。就像古代人类认识叉子在处理面条时比勺子更好。

预期答案

因此,我希望得到满足以下要求的答案:

  1. 解释迭代何时比递归更具可读性的情况
  2. 伪代码中举例说明算法(例如Bubblesort等),以证明迭代在这种情况下比递归更强可读
  3. 注意

    • 请注意,我不关心性能,我关心的是代码可读性
    • 因此,您可以使用任何命令式语言,但不能使用Haskell(或其派生词),因为它们不支持循环。

    参考

2 个答案:

答案 0 :(得分:2)

为避免基于意见的回答,我想扩展递归的含义:

在谈论递归时,区分递归定义的函数和生成递归过程的函数非常重要。

在这两个方面中,后者更为重要(假设您正在编写一个程序来解决实际问题)。鉴于迭代过程和递归过程之间的选择,前者是更好的,无论可读性或优雅,因为效率的差异是巨大的。

作为一个例子,考虑计算斐波纳契数,让我们在Haskell中做,以表明这不是关于循环与函数调用:

fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

这会生成一个递归过程,例如fib(4)

                   fib(4)
                    |
          ----------+----------
         fib(3)              fib(2)  
           |                   |
      -----+----          -----+---- 
    fib(2)    fib(1)   fib(1)     fib(0)
       |          |      |          |
       |          1      1          0
  -----+----  
fib(1)   fib(0)
 |        |
 1        0

将此与迭代过程(由递归定义的函数生成)进行比较:

fibIter :: Int -> Int
fibIter n = iter 1 0 n
    where
    iter :: Int -> Int -> Int -> Int
    iter _ b 0 = b
    iter a b m = iter (a+b) a (m-1)

fib(4)生成的过程是

fibIter 4
iter 1 0 4
iter 1 1 3
iter 2 1 2
iter 3 2 1
iter 5 3 0
3

请注意,递归过程会生成指数个函数调用,其中迭代过程执行线性数量的函数调用(如果消除尾调用,则只需要常量空间)。

在其他情况下,并且在使用尾部调用消除并​​具有循环结构的语言中,我说在编写循环或使用递归之间的选择是品味或编程约定的问题(基于此观点)

更新关于bubblesort实现的可读性:

可以说,递归公式更清晰 直接将算法表示为

  1. 冒出最大的元素并将其放在结果
  2. 使用bubblesort
  3. 对其余元素进行排序

    - 再次,可以说 - 与循环和局部变量的迭代公式相比,需要更少的精神管理。

    为清楚起见,我可能会写一些类似

    的递归bubblesort
    bubbleSort :: Ord a =>  [a] -> [a]
    bubbleSort [] = []
    bubbleSort l = bubbleSort rest ++ [largest]
        where tmp = bubbleUpLargest l
              largest = last tmp
              rest = init tmp
              bubbleUpLargest (x:y:xs)
                | x > y = y:bubbleUpLargest (x:xs)
                | otherwise = x:bubbleUpLargest (y:xs)
              bubbleUpLargest x = x
    

答案 1 :(得分:0)

基于迭代的代码 - 在大多数情况下 - 内存密集程度较低。也许这个问题的最佳答案在于 递归是如何工作的。

当程序使用递归重定向到函数的“start”时,C ++语言不会重用该函数的现有实例。而是创建函数的 new 实例,并将程序计数器移动到新实例的开头。旧实例的地址放在堆栈缓冲区上,等待新实例的返回。

但是,使用迭代时,程序计数器将移动到现有代码实例的开头。 无需筹码

这就是在许多使用C / C ++的低级编程应用程序中强烈支持迭代的原因。编程微控制器时,您几乎不会想要使用递归,因为堆栈缓冲区只能容纳非常少量的函数地址。

然而,在某些情况下,递归才有意义。在信号处理领域,数学通常分解为需要递归的公式,并且您不能总是找到方程的封闭形式版本。在这种情况下,当在具有足够内存的计算机上运行时,递归可能是最佳选择。

此外,递归在导航文件树时非常有用。请考虑以下代码Code Example。特别注意他的void DFS(struct node *head)函数的简单性。

事实是,有了足够的内存,递归可能非常有用。所以它并不是递归和迭代之间的竞争,而是理解每种方法的局限性。