以下是定义的两个函数,用于查找数字列表的最大值。
mx :: (Ord a) => [a] -> a
mx [] = error "Empty list"
mx [x] = x
mx (x:xs)
| x > (mx xs) = x
| otherwise = (mx xs)
mx' (x:xs) = findMax x xs
where
findMax cmx [] = cmx
findMax cmx (x:xs) | x > cmx = findMax x xs
| otherwise = findMax cmx xs
main = do
print $ mx [1..30]
定时上述代码,首先针对mx' (tail-recursive)和mx(非尾递归)的下一个,我们有以下时间。
Lenovo-IdeaPad-Y510P:/tmp$ time ./t
30
real 0m0.002s
user 0m0.000s
sys 0m0.001s
Lenovo-IdeaPad-Y510P:/tmp$ ghc -O2 t.hs
[1 of 1] Compiling Main ( t.hs, t.o )
Linking t ...
Lenovo-IdeaPad-Y510P:/tmp$ time ./t
30
real 0m6.272s
user 0m6.274s
sys 0m0.000s
有人可以解释为什么只有30个元素的列表会有如此巨大的性能差异吗?
答案 0 :(得分:11)
正如其他人所指出的那样,GHC不会执行常见的子表达式消除(CSE),导致您的第一个片段在指数时间内运行。
要了解原因,请考虑例如。
test1 = length [1..1000] + sum [1..1000]
test2 = let l = [1..1000] in length l + sum l
这两个示例在语义上是等效的,但test1
在常量空间中运行
线性空间中的test2
(分配了整个1000个单元格)。基本上,在这种情况下CSE
否定了懒惰的好处。
由于CSE可以导致更糟的表现,GHC在应用它时相当保守。
GHC常见问题中的更多解释:
https://www.haskell.org/haskellwiki/GHC/FAQ#Does_GHC_do_common_subexpression_elimination.3F
答案 1 :(得分:9)
问题不在于尾递归,而是在mx
一般情况下你计算mx xs
两次:一次将它与x
进行比较,然后第二次归还它。这些调用中的每一个本身都会调用mx xs
两次,然后执行相同的操作等等...导致指数运行时间。
您可以通过简单地保存第一次调用的结果来第二次使用它来删除此问题:
mx :: (Ord a) => [a] -> a
mx [] = error "Empty list"
mx [x] = x
mx (x:xs) =
let mxxs = mx xs in
if x > mxxs then x else mxxs
答案 2 :(得分:5)
你的第二个算法是线性的,它最终会通过你的列表进行一次传递。在这种情况下,您的第一个算法具有指数运行时(这恰好是最坏的情况)。您最终基本上检查列表中的所有内容,以确定第一个元素1不是最大值。然后你考虑第二个元素,2,并查看列表的其余部分,以了解它也不是最大值。
如果您使用mx
和22
,23
,...,30
等值运行程序,您会看到运行时明显呈指数增长。< / p>
特别是,这不仅仅是尾递归的问题,而是一种低效的递归算法与有效递归算法。您可以使用没有尾递归的语言实现这些,并且仍然可以看到mx'
比mx
更快的性能。
答案 3 :(得分:3)
致电mx [1..3]
会导致以下来电:
mx [1..3]
mx [2..3] -- x > (mx xs) in mx [1..3]
mx [3] -- x > (mx xs) in mx [1..2]
mx [3] -- otherwise = (mx xs) in mx [1..2]
mx [2..3] -- otherwise = (mx xs) in mx [1..3]
mx [3] -- x > (mx xs) in mx [1..2]
mx [3] -- otherwise = (mx xs) in mx [1..2]
查找mx
的最大[1..n]
次呼叫的数量为O(2^n)
:2^n - 1
,确切地说。
mx'
拨打O(n)
电话:n + 1
,确切地说。
mx' [1..3]
findMax 1 [2, 3]
findMax 2 [3]
findMax 3 []
对于n = 30
,与您的测试一样,mx
拨打1073741823
个电话,mx'
仅拨打29
。