【问题标题】:When will iteration be more readable than recursion? [closed]什么时候迭代比递归更具可读性? [关闭]
【发布时间】:2018-11-15 15:27:39
【问题描述】:

前言

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

动机

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

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

很明显,如果我说叉子(或勺子)是最好的吃饭工具,我会很狂热。

所以,它和递归或迭代一样,都是工具,各有优缺点。

但是,通过谷歌和 StackOverflow 搜索后,虽然我找到了很多关于为什么递归比迭代更具可读性的例子,但我仍然没有找到任何关于 何时迭代的例子比递归更具可读性

因此,我试图找出迭代更易读的情况。就像古人如何意识到叉子比勺子更擅长处理面条一样。

预期答案

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

  1. 说明迭代何时比递归更具可读性
  2. 伪代码中给出一个算法示例(例如冒泡排序等),以证明在这种情况下迭代比递归更可读

注意

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

参考

【问题讨论】:

  • 是否有您关注的特定语言?不同的语言可能对其中一种有不同的优化。
  • 其实我并不关心是哪种语言,我想从理论上解释一下。
  • @WongJiaHau - 你是对的。它应该已经关闭了。我也投过那个票。
  • 我的意思是说你的问题是“基于意见的”是没有人对递归与迭代的优雅程度进行任何测量。例如,我不知道有任何 fMRI 研究可以测量在查看递归代码与迭代代码时大脑的哪些区域(例如,快乐中心)被激发。我也不认为有研究可以测量两种编码方式时释放的多巴胺水平。没有这个,没有人可以客观地回答你的问题。只需使用更喜欢或感觉更舒服的东西。
  • @WongJiaHau 哈哈,我没有意识到,我很抱歉。公平地说,我不确定这在堆栈溢出答案的空间中是否可以回答。我不确定它是否可以回答。优雅可能比可读性更接近你想要的。可读性取决于上下文:[].slice.call(arguments) 对每个在行业工作的 JavaScript 开发人员都很熟悉,但对其他人来说却相当不透明。而我认为当我们说某件事是优雅时,我们有更高程度的普遍认同:例如欧拉恒等式优雅,快速排序算法优雅等等。

标签: recursion functional-programming iteration procedural-programming


【解决方案1】:

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

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

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

例如,考虑计算斐波那契数,让我们在 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

请注意,递归过程会产生指数级的函数调用,而迭代过程会执行线性数量的函数调用(如果尾调用被消除,则只需要恒定的空间)。

在其他情况下,以及在执行尾调用消除并​​具有循环结构的语言中,我会说是编写循环还是使用递归之间的选择取决于品味或编程约定(基于此观点)。

更新关于冒泡排序实现的可读性:

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

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

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

为了清楚起见,我可能会写出类似的递归冒泡排序

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

【讨论】:

  • 感谢您回答我的问题。实际上,我提出这个问题是因为我发现bubblesort的递归版本实际上比迭代版本要清晰得多。 (比较 Bubblesort in HaskellBubblesort in Javascript)。因此,我想知道是否有更多关于这个问题的例子。
  • @WongJiaHau 我对冒泡排序示例添加了评论
【解决方案2】:

在大多数情况下,基于迭代的代码占用的内存较少。也许这个问题的最佳答案在于 递归是如何工作的。

当程序使用递归重定向到函数的“开始”时,C++ 语言不会重用函数的现有实例。相反,会创建函数的 new 实例,并将程序计数器移动到新实例的开头。旧实例的地址被放置在 Stack Buffer 中等待新实例的返回。

但是,当使用迭代时,程序计数器会移动到现有代码实例的开头。 不需要堆栈

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

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

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

事实上,只要有足够的内存,递归就非常有用。所以这并不是递归和迭代之间的真正竞争,而是了解每种方法的局限性。

【讨论】:

  • 感谢您抽出宝贵时间回答我的问题,但我认为您可能误解了我的问题。首先,我了解递归的概念,并且我了解如果您对某些算法(例如斐波那契数列)使用迭代,则需要保留自定义堆栈。其次,我当然理解迭代会比递归有更好的性能(虽然不是一直),但正如我所提到的,我不关心性能,我关心的是代码优雅
  • @WongJiaHau “代码优雅” 是基于意见的。你想让别人决定你喜欢什么吗?
  • 这如何回答这个问题??
  • @AGNGazer 也许我用错了术语,我的意思是可读性
  • @WongJiaHau - 社区的作用是努力提高所有关于 SO 的问题和答案的质量。这可能意味着暴露缺陷,但它是关于为特定问题创建清晰而明确的答案。主要基于意见的问题不属于该领域。
猜你喜欢
  • 1970-01-01
  • 2016-07-21
  • 1970-01-01
  • 2019-05-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-03-25
  • 1970-01-01
相关资源
最近更新 更多