我理解:
head (map (2**) [1..999999])
实际上只会评估2 ** 1,其余的都没有,但我正在阅读的那本书说:
head (sort somelist)
只需要找到列表中的最小项目,因为这就是所有使用的项目。这是如何运作的?据我所知,使用我所知的排序算法(如气泡排序)这是不可能的。
我认为这样做的唯一方法是,如果排序算法要遍历整个列表,寻找最小的项目,然后在没有该项目的情况下递归列表。对我来说,这听起来很慢。
这是排序功能的工作方式,还是有其他我不知道的排序算法,可以像它那样短路?
答案 0 :(得分:10)
此:
只需要找到列表中最小的项目,因为这就是所有使用的项目。
...应该说该函数只需要完成最小量的工作,排序算法需要来找到最小的元素。
例如,如果我们使用quicksort作为基础排序算法,那么head . quicksort
等同于最佳(!)选择算法,称为“quickselect”,这是最坏情况的线性。此外,我们只能通过take k . quicksort
实现 k -quickselect。
维基百科在其关于选择算法的文章中指出(我的重点):
由于语言对排序的支持越来越普遍,因此尽管在速度上存在缺点,但在许多环境中,首选的是简单的排序方法,然后进行索引。 的确,对于懒惰的语言,如果您的排序足够懒,这种简单的方法甚至可以为k最小/最大排序(最大/最小作为特殊情况)提供最佳复杂性。
Quicksort在这种情况下工作得很好,而Haskell中的默认排序(合并排序)也不是很好,因为它返回的工作量超过了返回排序列表中每个元素的严格要求。正如this post on the Haskell mailing list所述:
lazy quicksort能够生成批量的 中的前k个最小元素O(n + k log k)总时间[1]
而懒惰的mergesort需要
O(n + k log n)总时间[2]
有关详情,请阅读this blog post。
答案 1 :(得分:6)
如果你创建一个跟踪其参数的比较函数,就像在GHCi的命令行中那样:
> :module + Data.List Debug.Trace
> let myCompare x y = trace ("\tCmp " ++ show x ++ " " ++ show y) $ compare x y
然后你可以自己看看这个行为:
> sortBy myCompare "foobar"
" Cmp 'f' 'o'
Cmp 'o' 'b'
Cmp 'f' 'b'
Cmp 'a' 'r'
Cmp 'b' 'a'
a Cmp 'b' 'r'
b Cmp 'f' 'o'
Cmp 'f' 'r'
f Cmp 'o' 'o'
Cmp 'o' 'r'
o Cmp 'o' 'r'
or"
Haskell懒洋洋地评估字符串,一次一个字符。在找到每个字符时打印左侧列,右侧列记录所需的比较,由“跟踪”打印。
请注意,如果您编译它,特别是在启用优化时,您可能会得到不同的结果。优化器运行一个严格性分析器,可能会注意到整个字符串被打印出来,因此热切地评估它会更有效。
然后尝试
> head $ sortBy myCompare "foobar"
Cmp 'f' 'o'
Cmp 'o' 'b'
Cmp 'f' 'b'
Cmp 'a' 'r'
Cmp 'b' 'a'
'a'
如果您想了解其工作原理,请查看sort函数的源代码,并在纸上手动评估'sort“foobar”。
qsort [] = []
qsort (x:xs) = qsort less ++ [x] ++ qsort greater
where (less, greater) = partition (< x) xs
所以
qsort ('f':"oobar")
= qsort ('b':"a") ++ "f" ++ qsort ('o':"or")
= ("a" ++ "b") ++ "f" ++ qsort ('o':"or")
现在我们已经做了足够的事情,发现'a'是结果中的第一项,而不必评估对“qsort”的另一个调用。我省略了实际比较,因为它隐藏在对“分区”的调用中。实际上“分区”也是懒惰的,所以事实上其他“qsort”的论据并没有根据我的表现进行评估。
答案 2 :(得分:2)
您刚才描述的算法有一个特定的名称:“选择排序”。它是O(n 2 )所以它不是你能做的最快的事情。但是,如果你想要排序数组中的第一个“k”元素,那么复杂性将是O(kn),如果“k”足够小(如你的例子),那就很好。
请注意,您在函数式语言中使用纯函数。通过查看函数的组合方式,编译器很可能在两种情况下都能为sort
生成优化代码。在撰写head
和sort
时,您可以很容易地推断出您想要最小元素。