【问题标题】:Quicksort slower than Mergesort?快速排序比合并排序慢?
【发布时间】:2010-10-04 14:39:17
【问题描述】:

我昨天正在努力实现一个快速排序,然后我运行它,期望比 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;
}

【问题讨论】:

    标签: java algorithm quicksort mergesort


    【解决方案1】:

    实际上我刚刚用 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 没有疯狂的色彩。我还有一些关于它的东西,大约在中途this page

    ps。两种排序都不需要链表的额外内存。

    【讨论】:

    • 这是一个无关紧要的答案,因为它使用链表后备存储
    • 您说“合并排序和快速排序具有相同的大 O 最佳情况 (n*log(n))”,但我想提一下,大 O 严格限制运行时间(它是仅限最坏情况)Big Omega 描述下限(最佳情况)
    【解决方案2】:

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

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

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

    【讨论】:

    • +1 对于插入排序的切换,应该会有不错的改进
    • 您为什么建议优化快速排序实现而不是合并排序实现?合并排序也可以从切换到插入排序中受益(以 timsort 为例)。顺便说一句,许多编程语言实现在内部使用了优化版本的合并排序:Java、Python、C 和 GNU libc... 后者甚至将快速排序称为“较慢的算法”。
    【解决方案3】:

    之前在 SO 上讨论过:“Why is quicksort better than mergesort?

    ~

    【讨论】:

      【解决方案4】:

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

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

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

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

      【讨论】:

        【解决方案5】:

        根据此维基百科article,您的结果是预期的。

        【讨论】:

        • @Stephan Eggermont:你能指出 John 实现中的错误吗?
        【解决方案6】:

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

        【讨论】:

        • 我不明白这个论点。如果快速排序是 O(n log(n)) 平均,这是因为存在低于平均水平的情况,无论您如何选择枢轴,您都无法避免它们。还是我忽略了什么?
        【解决方案7】:

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

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

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

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

        【讨论】:

          【解决方案8】:

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

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

          (3) 选择中值数据进行数据透视可能会更快,

          【讨论】:

          • 即使调用 qsort() 也可能不是纯快速排序。
          【解决方案9】:

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

          快速排序是最好的一般情况算法,因为它的开销很低,因此它对于 n 值高达大约 10000 左右具有良好的速度,并且对于 n 的任意天文值仍然具有良好的运行时间。合并排序具有编写堆栈帧的不幸开销,这是每个递归调用都需要的。因此,对于较低的 n 值,它在 RT = cnlogn 中的 c 值非常高,因此它不是首选的一般排序方法。

          编辑:Software Monkey 指出了一个矛盾:快速排序平均 O(nlogn) 随机输入,但 O(n^2) 最坏的情况。所以它实际上在某种程度上受数据熵的约束——或者你可以随机选择枢轴。不过,我可能还是有点走神。

          【讨论】:

          • 快速排序不能同时是“平均情况 O(nlogn)”和“平均 ... 在 O(nlogn) 和 O(n^2) 之间”。
          • 抱歉,随机输入的平均 O(nlogn),但最坏的情况为 O(n^2) 所以它实际上在某种程度上受熵的约束
          【解决方案10】:

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

          如果您不需要稳定的排序,并且不对链表进行排序,我认为这将是您可以做到的最快速度。

          Merge sort

          【讨论】:

            【解决方案11】:

            我认为只要数据适合内存,好的合并排序实现就比好的快速排序实现更好。

            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%,这是由于每一步的拆分不均匀。

            【讨论】:

              【解决方案12】:

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

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

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

              【讨论】:

                【解决方案13】:

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

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

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

                【讨论】:

                  【解决方案14】:

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

                  【讨论】:

                    【解决方案15】:

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

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

                    【讨论】:

                      猜你喜欢
                      • 2021-06-06
                      • 2021-06-19
                      • 2015-06-25
                      • 2016-12-08
                      • 1970-01-01
                      • 2017-03-18
                      • 2016-12-23
                      • 2021-03-11
                      • 1970-01-01
                      相关资源
                      最近更新 更多