什么时候迭代比递归更具可读性



前言

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

赋予动机

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

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

显然,如果我说叉子(或勺子)是最好的进食工具,我会是一个狂热分子。

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

然而,在搜索了Googles和StackOverflow之后,虽然我找到了很多关于为什么递归比迭代更具可读性的例子,但我仍然没有找到任何关于迭代比递归更具可读性的例子。

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

预期答案

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

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

注意

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

参考

  • 为什么递归优先于迭代?
  • https://www.quora.com/Computer-Science-Why-is-recursion-more-elegant-than-iteration
  • https://softwareengineering.stackexchange.com/questions/182314/recursion-or-while-loops

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

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

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

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

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

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

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

这就是迭代在许多使用 C/C++ 的低级编程应用程序中受到强烈青睐的原因。在对微控制器进行编程时,您几乎永远不希望使用递归,因为堆栈缓冲区只能容纳非常少量的函数地址。

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

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

事实是,如果有足够的内存,递归可能非常有用。因此,这不是递归和迭代之间的竞争,而是了解每种方法的局限性。

相关内容

  • 没有找到相关文章

最新更新