一个长期被误会的问题,这下说清楚了——迭代与递归的性能
递归真的会比迭代性能差吗?
在《Racket指南》(2.3.4 递归和迭代)中,Racket的作者做了清晰的解释:
在许多语言中,尽可能地将尽可能多的计算合并成迭代形式是很重要的。否则,性能会变差,不太大的输入都会导致堆栈溢出。类似地,在Racket中,有时很重要的一点是要确保在易于计算的常数空间中使用尾递归避免O(n)空间消耗。
然而,在Racket里递归不会导致特别差的性能,而且没有堆栈溢出那样的事情;如果一个计算涉及到太多的上下文,你可能耗尽内存,但耗尽内存通常需要比可能触发其它语言中的堆栈溢出更多数量级以上的更深层次的递归。基于这些考虑因素,加上尾递归程序会自动和一个循环一样运行的事实相结合,引导Racket程序员接受递归形式而不是避免它们。
例如,假设你想从一个列表中去除连续的重复项。虽然这样的一个函数可以写成一个循环,为每次迭代记住前面的元素,但一个Racket程序员更可能只写以下内容:
(define (remove-dups l)
(cond
[(empty? l) empty]
[(empty? (rest l)) l]
[else
(let ([i (first l)])
(if (equal? i (first (rest l)))
(remove-dups (rest l))
(cons i (remove-dups (rest l)))))]))
> (remove-dups (list "a" "b" "b" "b" "c" "c"))
'("a" "b" "c")
一般来说,这个函数为一个长度为n的输入列表消耗O(n)的空间,但这很好,因为它产生一个O(n)结果。如果输入列表恰巧是连续重复的,那么得到的列表可以比O(n)小得多——而且remove-dups也将使用比O(n)更少的空间!原因是当函数放弃重复,它返回一个remove-dups的直接调用结果,所以尾部调用“优化”加入:
(remove-dups (list "a" "b" "b" "b" "b" "b"))
= (cons "a" (remove-dups (list "b" "b" "b" "b" "b")))
= (cons "a" (remove-dups (list "b" "b" "b" "b")))
= (cons "a" (remove-dups (list "b" "b" "b")))
= (cons "a" (remove-dups (list "b" "b")))
= (cons "a" (remove-dups (list "b")))
= (cons "a" (list "b"))
= (list "a" "b")
基于上述的描述,我们应该认真地重新认识递归的意义和价值,而且,在函数式编程中,你会发现用递归完成很多应用非常简洁,甚至用迭代是难以实现的(会用相对复杂的代码,而且可读性变差)。
而且使用尾递归优化后,更是做到了性能与表达的完美结合。