为什么Haskell为什么使用mergesort而不是quicksort?

时间:2018-09-08 17:25:55

标签: performance sorting haskell

Wikibooks' Haskell中,有the following claim

  

Data.List提供用于对列表进行排序的排序功能。它不使用快速排序;相反,它使用称为mergesort的算法的有效实现。

在Haskell中使用mergesort而不是quicksort的根本原因是什么? Quicksort通常具有更好的实用性能,但在这种情况下可能没有。我认为,使用Haskell列表很难实现Quicksort的就地好处(不可能吗?)。

a related question on softwareengineering.SE,但这并不是真正的为什么使用mergesort。

我自己实现了这两种类型的概要分析。 Mergesort优越(对于2 ^ 20个元素的列表,速度大约是它的两倍),但是我不确定我的quicksort实现是否是最优的。

编辑:这是我对mergesort和quicksort的实现:

mergesort :: Ord a => [a] -> [a]
mergesort [] = []
mergesort [x] = [x]
mergesort l = merge (mergesort left) (mergesort right)
    where size = div (length l) 2
          (left, right) = splitAt size l

merge :: Ord a => [a] -> [a] -> [a]
merge ls [] = ls
merge [] vs = vs
merge first@(l:ls) second@(v:vs)
    | l < v = l : merge ls second
    | otherwise = v : merge first vs

quicksort :: Ord a => [a] -> [a]
quicksort [] = []
quicksort [x] = [x]
quicksort l = quicksort less ++ pivot:(quicksort greater)
    where pivotIndex = div (length l) 2
          pivot = l !! pivotIndex
          [less, greater] = foldl addElem [[], []] $ enumerate l
          addElem [less, greater] (index, elem)
            | index == pivotIndex = [less, greater]
            | elem < pivot = [elem:less, greater]
            | otherwise = [less, elem:greater]

enumerate :: [a] -> [(Int, a)]
enumerate = zip [0..]

编辑 2 3:我被要求提供实现的时间安排和Data.List中的排序时间。按照@Will Ness的建议,我用-O2标志编译了this gist,每次在main中更改提供的排序,并用+RTS -s执行。排序后的列表是一个廉价创建的伪随机[Int]列表,其中包含2 ^ 20个元素。结果如下:

  • Data.List.sort:0.171秒
  • mergesort:1.092秒(比Data.List.sort慢6倍)
  • quicksort:1.152秒(比Data.List.sort慢7倍)

5 个答案:

答案 0 :(得分:70)

在命令式语言中,Quicksort通过对数组进行变异来就地执行。正如您在代码示例中所演示的那样,您可以通过构建单链接列表来使Quicksort适应Haskell这样的纯函数式语言,但这不是那么快。

另一方面,Mergesort并不是就地算法:简单的命令式实现将合并的数据复制到其他分配中。对于Haskell而言,这是一个更好的选择,因为Haskell本质上必须无论如何都要复制数据。

让我们退后一步:Quicksort的性能优势是“传奇”-几十年前在与我们今天使用的机器大不相同的机器上建立的声誉。即使您使用相同的语言,这种知识也需要不时重新检查,因为实际情况可能会发生变化。我在该主题上阅读的最后一篇基准测试论文仍然把Quicksort放在首位,但是即使在C / C ++中,它也比Mergesort领先。

Mergesort具有其他优点:无需进行调整即可避免Quicksort的O(n ^ 2)最坏情况,并且它自然稳定。因此,如果您由于其他因素而失去了很小的性能差异,则Mergesort是一个明显的选择。

答案 1 :(得分:28)

我认为@comingstorm的答案非常可观,但这是有关GHC排序功能历史的更多信息。

Data.OldList的源代码中,您可以找到sort的{​​{3}},并亲自验证这是一种合并排序。该文件中的定义正下方是以下注释:

Quicksort replaced by mergesort, 14/5/2002.

From: Ian Lynagh <igloo@earth.li>

I am curious as to why the List.sort implementation in GHC is a
quicksort algorithm rather than an algorithm that guarantees n log n
time in the worst case? I have attached a mergesort implementation along
with a few scripts to time it's performance...

因此,最初使用的是功能快速排序(功能qsort仍然存在,但已注释掉)。 Ian的基准测试表明,在“随机列表”案例中,他的归类排序与Quicksort具有竞争性,在已排序数据的情况下,其表现大大优于Quicksort。后来,根据该文件中的其他注释,Ian的版本被另一种实现方式取代,该实现速度快大约两倍。

原始qsort的主要问题是它没有使用随机枢轴。相反,它以列表中的第一个值为中心。这显然很糟糕,因为这意味着对于排序(或几乎排序)的输入,性能将是最差的情况(或接近)。不幸的是,从“先枢轴”转换为替代方案(随机的,或者-在您的实现中-在“中间”的某个位置)时,会遇到一些挑战。在没有副作用的函数式语言中,管理伪随机输入有点问题,但是可以说您解决了这个问题(可能是通过在排序函数中构建一个随机数生成器)。您仍然有一个问题,当对一个不可变的链表进行排序时,找到一个任意的枢轴,然后基于它进行分区,将涉及多个列表遍历和子列表副本。

我认为,实现快速排序的好处的唯一方法是将列表写到向量中,将其排序到适当的位置(并牺牲排序稳定性),然后再将其写回到列表中。我认为这不可能是一个整体胜利。另一方面,如果您已经在向量中包含数据,那么就地快速排序绝对是一个合理的选择。

答案 2 :(得分:5)

在单链列表上,可以在适当位置进行mergesort。更重要的是,幼稚的实现扫描了整个列表的一半以获取第二个子列表的开始,但是第二个子列表的开始由于对第一个子列表进行排序而产生了副作用,因此不需要额外的扫描。快速排序比合并排序要解决的一件事是缓存一致性。 Quicksort处理内存中彼此接近的元素。一旦间接元素进入其中,就像对指针数组而不是数据本身进行排序一样,这种优势就会减弱。

Mergesort坚决保证最坏情况下的行为,并且很容易对其进行稳定的排序。

答案 3 :(得分:1)

许多关于为什么在Haskell中不使用Quicksort的说法似乎是合理的。但是,对于随机情况,至少Quicksort不会比Mergesort慢。基于理查德·伯德(Richard Bird)的书在Haskell中进行功能性思考中给出的实现,我进行了3种方法的快速排序:

tqsort [] = []
tqsort (x:xs) = sortp xs [] [x] [] 
  where
    sortp [] us ws vs     = tqsort us ++ ws ++ tqsort vs
    sortp (y:ys) us ws vs =
      case compare y x of 
        LT -> sortp ys (y:us) ws vs 
        GT -> sortp ys us ws (y:vs)
        _  -> sortp ys us (y:ws) vs

我以几种情况为基准,例如,大小为10 ^ 4的列表包含0到10 ^ 3或10 ^ 4之间的Int,依此类推。结果是3路Quicksort甚至Bird版本都比GHC的Mergesort好,这比ghc的Mergesort快1.x〜3.x,具体取决于数据类型(很多重复吗?非常稀疏?)。 criterion生成以下统计信息:

benchmarking Data.List.sort/Diverse/10^5
time                 223.0 ms   (217.0 ms .. 228.8 ms)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 226.4 ms   (224.5 ms .. 228.3 ms)
std dev              2.591 ms   (1.824 ms .. 3.354 ms)
variance introduced by outliers: 14% (moderately inflated)

benchmarking 3-way Quicksort/Diverse/10^5
time                 91.45 ms   (86.13 ms .. 98.14 ms)
                     0.996 R²   (0.993 R² .. 0.999 R²)
mean                 96.65 ms   (94.48 ms .. 98.91 ms)
std dev              3.665 ms   (2.775 ms .. 4.554 ms)

但是,Haskell 98 / 2010中还对sort提出了另一要求:它必须稳定。使用Data.List.partition的典型Quicksort实现是 stable ,但是上面的不是。


以后添加:评论中提到的稳定的3向Quicksort看起来和这里的tqsort一样快。

答案 4 :(得分:0)

我不确定,但是就我所知,看代码我不认为Data.List.sort是Mergesort。它仅以sequences函数以ascendingdescending函数的美丽的三角形相互递归方式进行一次传递,以生成所需序列中已升序或降序的块的列表订购。只有这样,它才开始合并。

这是编码中诗歌的体现。与Quicksort不同,它的最坏情况(总随机输入)具有O(nlogn)时间复杂度,而最好情况(已进行升序或降序排序)为O(n)。

我认为没有其他排序算法可以胜过它。