前言
我已经读了很多关于为什么更喜欢递归而不是迭代的文章,但我仍然没有看到任何问题来解释何时更喜欢迭代而不是递归。
赋予动机
当然,我相信没有最好的工具来解决所有问题。
例如,勺子和叉子都是吃的工具,但勺子更适合吃米饭,而叉子更适合吃面条。
显然,如果我说叉子(或勺子)是最好的进食工具,我会是一个狂热分子。
所以,它和递归或迭代是一样的,它们都是工具,它们各有优缺点。
然而,在搜索了Googles和StackOverflow之后,虽然我找到了很多关于为什么递归比迭代更具可读性的例子,但我仍然没有找到任何关于迭代比递归更具可读性的例子。
因此,我试图找出迭代更具可读性的情况。就像古人意识到叉子比勺子更擅长处理面条一样。
预期答案
因此,我希望得到满足以下要求的答案:
解释何时迭代- 比递归更具可读性的情况
- 举一个伪代码中的算法(如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
请注意,递归过程生成指数数量的函数调用,其中迭代过程执行线性数量的函数调用(并且只有在消除尾部调用时才需要常量空间)。
在其他情况下,以及在执行尾调用消除并具有循环结构的语言中,我会说编写循环或使用递归之间的选择是品味或编程约定的问题(使这种观点基于)。
有关气泡排序实现的可读性的更新:
可以说,递归公式更清晰,因为它 直接将算法表示为
冒- 泡最大的元素并将其放在结果的最后一个
- 使用气泡排序对其余元素进行排序
这 - 再次,可以说 - 比具有循环和局部变量的迭代公式需要更少的心理内务管理。
为了清楚起见,我可能会写递归气泡排序,如下所示
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)
函数的简单性。
事实是,如果有足够的内存,递归可能非常有用。因此,这不是递归和迭代之间的竞争,而是了解每种方法的局限性。