为什么极简示例的Haskell快速排序不是一个“真正”的快速排序?

125

Haskell的网站介绍了一个非常吸引人的只有5行的快速排序函数,如下所示。

quicksort [] = []
quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
    where
        lesser = filter (< p) xs
        greater = filter (>= p) xs

他们还包括一个用C语言实现的“真正的快速排序”。
// To sort array a[] of size n: qsort(a,0,n-1)

void qsort(int a[], int lo, int hi) 
{
  int h, l, p, t;

  if (lo < hi) {
    l = lo;
    h = hi;
    p = a[hi];

    do {
      while ((l < h) && (a[l] <= p)) 
          l = l+1;
      while ((h > l) && (a[h] >= p))
          h = h-1;
      if (l < h) {
          t = a[l];
          a[l] = a[h];
          a[h] = t;
      }
    } while (l < h);

    a[hi] = a[l];
    a[l] = p;

    qsort( a, lo, l-1 );
    qsort( a, l+1, hi );
  }
}

在C版本下方的链接指向一个页面,上面写着:“介绍中引用的快速排序不是“真正”的快速排序,而且对于像C代码那样更长的列表缩放效果不佳。”
为什么上述Haskell函数不是真正的快速排序?它如何无法缩放到更长的列表?

17
不是就地操作,因此速度相当慢?其实这是一个很好的问题! - fuz
4
@FUZxxl说:Haskell的列表是不可变的,因此在使用默认数据类型时没有任何操作会就地进行。至于它的速度-它不一定会更慢; GHC是一个令人印象深刻的编译器技术,使用不可变数据结构的Haskell解决方案往往与其他语言中使用可变数据结构的解决方案具有相同的速度。 - Callum Rogers
1
它实际上不是qsort吗?请记住,qsort的运行时间为O(N^2) - Thomas Eding
2
需要注意的是,上面的例子是Haskell的一个入门示例,并且快速排序对于排序列表来说是一个非常糟糕的选择。Data.List中的sort在2002年改为了mergesort:http://hackage.haskell.org/packages/archive/base/3.0.3.1/doc/html/src/Data-List.html#sort,在那里您还可以看到以前的快速排序实现。当前的实现是一个mergesort,它是在2009年制作的:http://hackage.haskell.org/packages/archive/base/4.4.0.0/doc/html/src/Data-List.html#sort。 - HaskellElephant
所以,可变性...但我认为说这不是“真正的”快速排序有点傻。无论如何,我仍然很困惑为什么这在大型列表上无法“扩展”。我的猜测是速度会线性损失并额外使用O(log(n))的内存。这完全不正确吗? - dividebyzero
显示剩余4条评论
12个回答

-2

从列表中获取第一个元素会导致非常糟糕的运行时间。使用三数中值法:首、中、尾。


3
如果列表是随机的,取第一个元素是可以的。 - Keith Thompson
3
对已排序或近乎有序的列表进行排序是常见的操作。 - Joshua
8
qsort的平均时间复杂度为n log n,最坏情况下为n^2。 - Joshua
3
从技术上讲,除非输入已经排序或接近排序,否则它并不比随机选择更糟。不好的轴点是远离中位数的轴点;如果第一个元素接近最小值或最大值,则第一个元素只是一个不好的轴点。 - Platinum Azure
1
选择枢轴是一个小细节,根本原因在于它不是原地排序。 - sdcvvc
显示剩余3条评论

-2

请任何人用Haskell编写快速排序算法,你会得到基本相同的程序-显然是快速排序。以下是一些优缺点:

优:它通过稳定性改进了“真正”的快速排序,即保留相等元素之间的顺序。

优:可以轻松地将其推广为三路划分(<=>),从而避免由于某个值出现O(n)次而导致的二次行为。

优:易于阅读-即使必须包括filter的定义。

缺:它使用更多的内存。

缺:通过进一步取样来泛化枢轴选择是昂贵的,这可以避免在某些低熵排序中出现二次行为。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接