21

昨天我正在努力实现一个快速排序,然后我运行它,期望比 Mergesort 更快的运行时间(我也实现了)。我运行了这两个,虽然快速排序对于小于 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;
}
4

15 回答 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

Altho 没有疯狂的色彩。在这一页的一半左右,我还有一些关于它的东西。

附言。两种排序都不需要链接列表的额外内存。

于 2009-01-31T01:29:43.833 回答
4

Mergesort 对于基于随机数组的数据要慢得多,只要它适合 ram。这是我第一次看到它辩论。

  • qsort 首先排序最短的子数组。
  • 切换到 5-25 个元素以下的插入排序
  • 做一个正常的枢轴选择

您的 qsort 非常慢,因为它尝试对长度为 2 和 3 的数组进行分区和 qsort 排序。

于 2009-12-10T00:18:26.860 回答
3

之前在SO上讨论过:“为什么quicksort比mergesort好?

~

于 2009-01-31T00:26:30.870 回答
3

对于相对较小的数组大小,快速排序的优点之一只是硬件实现的产物。

在数组上,可以就地完成快速排序,这意味着您正在读取和写入同一内​​存区域。另一方面,合并排序通常需要分配新的缓冲区,这意味着您的内存访问更加分散。您可以在示例实现中看到这两种行为。

因此,对于相对较小的数据集,快速排序更有可能获得缓存命中,因此往往在大多数硬件上运行得更快。

正如您的实验所证实的那样,对于大型数据集或其他数据结构(如链表)来说,合并排序仍然是一个很好的解决方案。

于 2009-01-31T01:11:40.203 回答
2

根据这篇维基百科文章,您的结果是预期的。

于 2009-01-31T00:27:18.623 回答
2

合并排序的最坏情况是快速排序的平均情况,所以如果你没有一个好的实现,合并排序总体上会更快。让快速排序快速工作是为了避免低于平均水平的情况。选择一个更好的支点(3 的中位数有帮助),您会看到不同。

于 2009-01-31T00:57:38.093 回答
1

(1) 有一个 qsort 算法,由 C qsort() 使用,它不需要额外的内存。这很可能是霍尔发明的。使得 qsort() 在 C 中快速。

(2) 在运行 qsort 之前随机化数据几乎总是会加快速度。

(3) 为枢轴选择中值数据可能会使其更快,

于 2009-01-31T01:39:10.863 回答
1

我可以想象,通过直接访问内存,例如使用 C,可以比使用 Mergesort 提高 Quicksort 的性能。

另一个原因是 Mergesort 需要更多内存,因为很难将其实现为就地排序。

特别是对于您的实现,您可以改进枢轴的选择,有很多不同的算法可以找到一个好的枢轴。

正如在 wikipedia上所见,可以以不同的方式实现快速排序。

于 2009-01-31T00:36:08.120 回答
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) 最坏的情况。所以它实际上在某种程度上受数据熵的约束——或者你可以随机选择枢轴。不过,我可能还是有点走神。

于 2009-01-31T04:42:04.817 回答
1

如果在快速排序最坏的情况下将堆排序实现为基本排序算法,则可以实现 theta(n log n) 算法。

如果您不需要稳定的排序,也不对链表进行排序,我认为这将是您最快的。

合并排序

于 2009-01-31T09:26:16.460 回答
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 实现中合并排序优于快速排序。这种差异背后的主要原因似乎是因为快速排序的比较次数比合并排序多 10-20%,这是由于每一步的拆分不均匀。

于 2012-11-21T22:37:22.153 回答
1

我进行了类似的测试,结果证明纯快速排序(随机选择枢轴)比大型数组的合并排序慢得多。

选择枢轴作为第一个、中间和最后一个元素的中位数提高了快速排序的性能,但在大型数组(> 100000 个元素)上,快速排序仍然肯定比合并排序更差。

当我实现介绍排序时,我看到了很大的改进,即如果递归深度超过某个阈值,快速排序会退回到堆排序。我的介绍排序实现几乎和我的合并排序实现一样快。当然,介绍排序不再是纯快速排序,因为当纯快速排序遇到一些不良数据时,它使用堆排序将复杂度恢复到 n log(n)。如果您有兴趣,我可以发布结果。

于 2012-05-15T18:08:03.607 回答
0

你的数据集足够随机吗?它们是部分排序的吗?

这可能会影响排序的速度......

就像 QuickSort 的 partition() 一样,如果数字按排序顺序,你会跳过,直到找到一个不是的。

于 2009-01-31T00:47:06.413 回答
0

这可能取决于您为测试排序的数据类型(已经排序的列表、随机的、反向排序的)。此外,如果您选择随机枢轴而不是使用第一个元素,则快速排序通常可能会更快。

于 2009-01-31T00:48:56.707 回答
0

为了快速排序的良好性能,重要的是不要一直递归到长度为 1 的列表

如果需要,您应该考虑将 2、3 甚至 4 的列表排序为嵌套的 ifs 交换。让我们知道性能如何变化。

于 2009-01-31T09:04:00.170 回答