在minimum = head . sort 中,sort 不会完全完成,因为它不会提前完成。 sort 只会根据head 的要求来生成第一个元素。
在例如合并排序,首先将列表中的n 号码进行成对比较,然后将获胜者配对并比较(n/2 号码),然后是新的获胜者(n/4)等。总而言之,O(n)比较以产生最小元素。
mergesortBy less [] = []
mergesortBy less xs = head $ until (null.tail) pairs [[x] | x <- xs]
where
pairs (x:y:t) = merge x y : pairs t
pairs xs = xs
merge (x:xs) (y:ys) | less y x = y : merge (x:xs) ys
| otherwise = x : merge xs (y:ys)
merge xs [] = xs
merge [] ys = ys
上面的代码可以被扩充,以标记它产生的每个数字,并在它的产生中进行一些比较:
mgsort xs = go $ map ((,) 0) xs where
go [] = []
go xs = head $ until (null.tail) pairs [[x] | x <- xs] where
....
merge ((a,b):xs) ((c,d):ys)
| (d < b) = (a+c+1,d) : merge ((a+1,b):xs) ys -- cumulative
| otherwise = (a+c+1,b) : merge xs ((c+1,d):ys) -- cost
....
g n = concat [[a,b] | (a,b) <- zip [1,3..n] [n,n-2..1]] -- a little scrambler
运行它几个列表长度,我们看到确实是~ n:
*Main> map (fst . head . mgsort . g) [10, 20, 40, 80, 160, 1600]
[9,19,39,79,159,1599]
要查看排序代码本身是否为~ n log n,我们对其进行更改,使每个产生的数字只携带自己的成本,然后通过对整个排序列表求和来找到总成本:
merge ((a,b):xs) ((c,d):ys)
| (d < b) = (c+1,d) : merge ((a+1,b):xs) ys -- individual
| otherwise = (a+1,b) : merge xs ((c+1,d):ys) -- cost
这里是不同长度列表的结果,
*Main> let xs = map (sum . map fst . mgsort . g) [20, 40, 80, 160, 320, 640]
[138,342,810,1866,4218,9402]
*Main> map (logBase 2) $ zipWith (/) (tail xs) xs
[1.309328,1.2439256,1.2039552,1.1766101,1.1564085]
上面显示了empirical orders of growth 列表长度的增加,n,正如 ~ n log n 计算所显示的那样,它正在迅速减少。另见this blog post。这是一个快速的相关性检查:
*Main> let xs = [n*log n | n<- [20, 40, 80, 160, 320, 640]] in
map (logBase 2) $ zipWith (/) (tail xs) xs
[1.3002739,1.2484156,1.211859,1.1846942,1.1637106]
编辑: 惰性评估可以比喻为一种生产者/消费者习语1,以独立的记忆存储作为中介。我们编写的任何生产性定义都定义了一个生产者,该生产者将在其消费者要求时,一点一点地生产其输出 - 但不会更快。无论生产什么都被记忆,因此如果另一个消费者以不同的速度消费相同的输出,它会访问相同的存储空间,之前已填充。
当没有更多的消费者引用一个存储时,它会被垃圾收集。有时,通过优化编译器能够完全取消中间存储,将中间人排除在外。
1 另请参阅:Simple Generators v. Lazy Evaluation,作者 Oleg Kiselyov、Simon Peyton-Jones 和 Amr Sabry。