快速排序比归并排序慢吗?

21

昨天我在实现快速排序算法,然后我运行它,期望它比归并排序(我也实现了)有更快的运行时间。我运行了这两种算法,虽然快速排序在小数据集<100个元素时更快(我确保它是可行的),但归并排序很快就成为了更快的算法。我被教导快速排序几乎总是比归并排序“更快”,我知道这个话题存在一些争议,但我至少预计它们之间的差距不会这么大。对于大于10000个元素的数据集,归并排序快了4倍以上。这是否是可以预料的,或者我的快速排序代码存在错误?

归并排序:

public static void mergeSort(int[ ] e)
{
    if (e.length <= 1) return;
    int[] first = new int[e.length/2];
    int[] second = new int[e.length - first.length];
    System.arraycopy(e, 0, first, 0, first.length);
    System.arraycopy(e, first.length, second, 0, second.length);
    mergeSort(first);
    mergeSort(second);
    System.arraycopy(merge(first, second), 0, e, 0, e.length);
}

private static int[] merge(int[] first, int[] second) {
    int iFirst = 0;
    int iSecond = 0;
    int iCombined = 0;

    int[] combined = new int[first.length + second.length];
    while(iFirst < first.length && iSecond < second.length) {
        if (first[iFirst] > second[iSecond]) {
            combined[iCombined++] = second[iSecond++];
        }
        else combined[iCombined++] = first[iFirst++];
    }
    for(; iFirst < first.length; iFirst++) {
        combined[iCombined++] = first[iFirst];
    }
    for(; iSecond < second.length; iSecond++) {
        combined[iCombined++] = second[iSecond];
    }
    return combined;
}

快速排序:

public static void quicksort(int[] a, int first, int last) {
    if (first >= last) return;

    int partitionIndex = partition(a, first, last);
    quicksort(a, first, partitionIndex - 1);
    quicksort(a, partitionIndex + 1, last);
}

public static int partition(int[] x, int first, int last) {
    int left = first;
    int right = last;
    int pivot = x[first];
    int pivotIdx = first;

    while(left <= right) {
        while(left < x.length && x[left] <= pivot) left++;
        while(right >= 0 && x[right] > pivot) right--;
        if (left <= right) {
            int temp = x[left];
            x[left] = x[right];
            x[right] = temp;
        }
    }
    pivotIdx = right;
    x[first] = x[right];
    x[pivotIdx] = pivot;
    return pivotIdx;
}
15个回答

10
我刚刚用C语言编写了一个“链表比较排序演示程序”,得出了类似的结论(在大多数情况下,合并排序会胜过快速排序),尽管有人告诉我快速排序通常不用于链表。我想指出的是选择枢轴值是一个非常重要的因素——我的初始版本使用随机节点作为枢轴,当我稍微改进一下,取两个(随机)节点的平均值后,1000000条记录的执行时间从4分钟以上降至不到10秒,与合并排序相当。
合并排序和快速排序在最好情况下具有相同的大O复杂度(n*log(n)),而且尽管人们可能试图声称,但大O实际上是关于迭代次数而不是比较次数的。两者之间能产生的最大差异总是对快速排序不利,并且涉及已经大部分排序或包含大量绑定的列表(当快速排序做得比合并排序好时,差异不会那么大)。这是因为绑定或已排序的段可以直接通过合并排序流畅地进行;当两个拆分列表返回要合并时,如果一个列表已经包含所有较小的值,则左侧的所有值将逐个与右侧的第一个元素进行比较,然后(由于返回的列表具有内部顺序)无需进行进一步的比较,右侧只需简单地迭代到末尾。这意味着迭代次数将保持不变,但比较次数减半。如果你正在谈论实际时间并且正在排序字符串,则比较是昂贵的。
如果快速排序中存在绑定和已排序段,并且未仔细确定枢轴值,则很容易导致不平衡的列表,这些不平衡的列表(例如,右侧一个,左侧十个)会导致减速。因此,如果您可以让您的快速排序在已排序列表上像在随机列表上一样表现良好,那么您就有了一种找到枢轴的好方法。
如果您感兴趣,演示程序会产生如下输出:
[root~/C] ./a.out -1 3 
Using "", 0 records
Primary Criteria offset=128

Command (h for help, Q to quit): N
How many records? 4000000
New list is 562500.00 kb

Command (h for help, Q to quit): m

Mergesorting..............3999999 function calls
123539969 Iterations     Comparison calls: 82696100
Elapsed time: 0 min 9 sec


Command (h for help, Q to quit): S
Shuffled.

Command (h for help, Q to quit): q

Quicksorting..............4000000 function calls
190179315 Iterations     Comparison calls: 100817020
Elapsed time: 0 min 23 sec

虽然没有疯狂的颜色,但与此相关的内容有更多关于我在这个页面中间位置的信息(链接)

另外,这两种排序方法都不需要使用链表额外的内存。


1
这是一个不相关的答案,因为它使用了链表作为后备存储。 - Stephan Eggermont
你说“归并排序和快速排序在最好情况下的时间复杂度都是O(n*log(n))”,但我想提醒一下,大O符号严格来说只是用于上界运行时间(仅限于最坏情况),而大Omega符号则描述了下界(最佳情况)。 - talloaktrees

4

如果数据是基于随机数组的,而且它适合内存,那么归并排序会慢得多。这是我第一次看到有人对此进行辩论。

  • 首先对最短的子数组进行快速排序。
  • 在5-25个元素以下时,切换到插入排序。
  • 进行正常的枢轴选择。

你的快速排序非常慢,因为它试图对长度为2和3的数组进行分区和排序。


1
+1 对于改用插入排序,应该会有很好的改进。 - helpermethod
1
你为什么建议优化快速排序的实现,而不是归并排序的实现呢?归并排序也可以从转换到插入排序中获益(以timsort为例)。另外,许多编程语言实现内部使用优化版本的归并排序:Java、Python、C with GNU libc等。后者甚至将快速排序称为“较慢的算法”。 - Erwan Legrand

3

3
相对较小的数组大小,快速排序的一个优点只是硬件实现的产物。
在数组上,快速排序可以就地进行,这意味着您正在从同一块内存区域读取和写入。另一方面,归并排序通常需要分配新的缓冲区,这意味着您的内存访问更加分散。您可以在示例实现中看到这两种行为。
因此,对于相对较小的数据集,快速排序更有可能获得高速缓存命中,因此通常在大多数硬件上运行更快。
归并排序仍然是大型数据集或其他数据结构(如链表)的相当不错的解决方案,正如您的实验所证实的那样。

2

根据这篇维基百科文章,您应该能得到期望的结果。


@Stephan Eggermont:你能指出John的实现中的错误吗? - Giorgio

2
合并排序的最坏情况相当于快速排序的平均情况,因此如果您没有一个好的实现,合并排序总体上将更快。使快速排序快速工作的关键是避免次优情况。选择更好的轴(中位数法有帮助),您将看到不同之处。

我不理解这个论点。如果快速排序在平均情况下是O(n log(n)),那是因为存在次优情况,无论你如何选择枢轴,都无法避免它们。或者我有什么地方看漏了吗? - Giorgio

1

我认为只要数据适合存储在内存中,良好的归并排序实现比良好的快速排序实现更优秀。

qsort() 最广泛使用的实现之一,glibc qsort(),在大多数情况下,当数据适合存储在内存中时,内部使用归并排序。这个归并排序分配了一个用于合并的临时内存空间,增加了一些内存开销,但大多数时候,它通过良好的主元选择和优化,性能超越了自己内部的快速排序实现。仅当数据和用于归并排序的临时内存无法适合内存时,glibc 才使用快速排序。

我在我的机器上使用 2.1GHz 的 CPU 和数 GB 的 RAM 测量了这两种实现的性能。输入是由伪随机生成器生成的,每个键都是 32 位无符号整数,这意味着与整数比较相比,由于比较函数的接口,需要进行更多的比较周期。

对于归并排序:

2 MB, time_diff 165.156000 ms, 78.752518 ns per byte
4 MB, time_diff 344.298000 ms, 82.087040 ns per byte
8 MB, time_diff 730.926000 ms, 87.133169 ns per byte
16 MB, time_diff 1541.215000 ms, 91.863573 ns per byte
32 MB, time_diff 3088.924000 ms, 92.057109 ns per byte
64 MB, time_diff 6262.868000 ms, 93.324006 ns per byte
128 MB, time_diff 12887.018000 ms, 96.015766 ns per byte
256 MB, time_diff 26731.597000 ms, 99.582959 ns per byte

快速排序:

2 MB, time_diff 243.519000 ms, 116.118908 ns per byte
4 MB, time_diff 504.975000 ms, 120.395422 ns per byte
8 MB, time_diff 1075.276000 ms, 128.182888 ns per byte
16 MB, time_diff 2183.865000 ms, 130.168498 ns per byte
32 MB, time_diff 4343.993000 ms, 129.461080 ns per byte
64 MB, time_diff 8714.166000 ms, 129.851192 ns per byte
128 MB, time_diff 17881.344000 ms, 133.226395 ns per byte
256 MB, time_diff 36751.029000 ms, 136.908252 ns per byte

你可以看到这两种实现之间的性能差异很明显,这也是为什么在如此广泛使用的qsort实现中,mergesort比quicksort更受欢迎的主要原因。这种差异背后的主要原因似乎是快速排序比归并排序多了10-20%的比较次数,因为每一步的分割不均匀。

1

我可以想象,例如使用C语言直接访问内存,可以比使用归并排序更大程度地提高快速排序的性能。

另一个原因是归并排序需要更多的内存,因为实现就地排序很困难。

特别适用于您的实现,您可以改进选择枢轴的方式,有许多不同的算法可以找到好的枢轴。

维基百科所示,可以以不同的方式实现快速排序。


1
(1)C语言的qsort()函数使用了一种不需要额外内存的快速排序算法,很可能是由Hoare发明的。这使得C中的qsort()函数非常快速。
(2)在运行qsort()之前对数据进行随机化几乎总能提高其速度。
(3)选择中位数作为枢轴可能会使它更快。

即使它被称为qsort(),它可能并不是一个纯粹的快速排序算法。 - Giorgio

1

这与算法分析一致。 归并排序对于任何输入和每个运行时都保证O(nlogn)。 快速排序的最佳情况是O(nlogn),平均情况也是O(nlogn),但最坏情况是O(n^2),因此平均执行时间将介于O(nlogn)和O(n^2)之间。

快速排序是最好的通用算法,因为它的开销很低,所以对于n值小于约10000左右的值具有良好的速度,并且对于任意天文数字级别的n值仍具有良好的运行时间。归并排序不幸的是需要写入堆栈帧的开销,这是每个递归调用所必需的。因此,对于较小的n值,它在RT = cnlogn中具有极高的c值,不是首选的通用排序方法。

编辑:软件猴指出了一个矛盾点:快速排序对于随机输入平均为O(nlogn),但最坏情况为O(n^2)。因此,它实际上受到数据熵的限制--或者您可以随机选择枢轴。我可能还有点偏差。


快速排序不能同时是“平均情况下O(nlogn)”和“平均情况下介于O(nlogn)和O(n^2)”。 - Lawrence Dol
抱歉,对于随机输入,平均时间复杂度为O(nlogn),但最坏情况下为O(n^2)。因此它实际上受到熵的限制。 - Overflown

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