我在一次面试中被问到这个问题。它们都是O(nlogn),但大多数人使用快速排序而不是归并排序。为什么呢?


当前回答

快速排序是在实践中最快的排序算法,但有一些病态的情况,可以使它的表现差到O(n2)。

堆排序保证在O(n*ln(n))中运行,并且只需要有限的额外存储空间。但是有许多真实世界的测试表明堆排序比快速排序平均要慢得多。

其他回答

实际上,快速排序是O(n2)。它的平均情况运行时间是O(nlog(n)),但最坏情况是O(n2),这发生在在包含很少唯一项的列表上运行时。随机化花费O(n)。当然,这并没有改变最坏的情况,它只是防止恶意用户使您的排序花费很长时间。

快速排序更受欢迎,因为它:

(MergeSort需要额外的内存,与要排序的元素数量成线性关系)。 有一个小的隐藏常数。

快速排序有O(n2)最差情况运行时和O(nlogn)平均情况运行时。然而,在许多情况下,它优于归并排序,因为许多因素影响算法的运行时,并且,当把它们放在一起时,快速排序胜出。

In particular, the often-quoted runtime of sorting algorithms refers to the number of comparisons or the number of swaps necessary to perform to sort the data. This is indeed a good measure of performance, especially since it’s independent of the underlying hardware design. However, other things – such as locality of reference (i.e. do we read lots of elements which are probably in cache?) – also play an important role on current hardware. Quicksort in particular requires little additional space and exhibits good cache locality, and this makes it faster than merge sort in many cases.

此外,通过使用适当的枢轴选择,几乎完全避免快速排序的最坏情况运行时间O(n2)是非常容易的——比如随机选择它(这是一个很好的策略)。

在实践中,许多现代的快速排序实现(特别是libstdc++的std::sort)实际上是introsort,其理论上的最差情况是O(nlogn),与归并排序相同。它通过限制递归深度,并在超过logn时切换到不同的算法(heapsort)来实现这一点。

我想补充的是,到目前为止提到的三种算法(归并排序,快速排序和堆排序)只有归并排序是稳定的。也就是说,对于那些具有相同键的值,顺序不会改变。在某些情况下,这是可取的。

但是,说实话,在实际情况下,大多数人只需要良好的平均性能和快速排序…快速=)

所有排序算法都有其起伏。有关排序算法的概述,请参阅维基百科的文章。

正如许多人所注意到的,快速排序的平均情况性能要比归并排序快。但这只适用于假设按需访问任何内存段的时间为常数的情况。

在RAM中,这种假设通常不太坏(由于缓存的存在,这种假设并不总是正确的,但也不太坏)。然而,如果你的数据结构足够大,可以存储在磁盘上,那么快速排序就会因为磁盘平均每秒进行200次随机查找而被扼杀。但是,同样的磁盘在按顺序每秒读取或写入兆字节的数据方面没有任何问题。这正是归并排序所做的。

因此,如果数据必须在磁盘上排序,你真的,真的想使用归并排序的一些变体。(通常你快速排序子列表,然后开始将它们合并到某个大小阈值以上。)

Furthermore if you have to do anything with datasets of that size, think hard about how to avoid seeks to disk. For instance this is why it is standard advice that you drop indexes before doing large data loads in databases, and then rebuild the index later. Maintaining the index during the load means constantly seeking to disk. By contrast if you drop the indexes, then the database can rebuild the index by first sorting the information to be dealt with (using a mergesort of course!) and then loading it into a BTREE datastructure for the index. (BTREEs are naturally kept in order, so you can load one from a sorted dataset with few seeks to disk.)

在许多情况下,了解如何避免磁盘寻道使我将数据处理工作花费数小时而不是数天或数周。

在归并排序中,一般算法为:

对左子数组进行排序 对右子数组进行排序 合并两个已排序的子数组

在顶层,合并两个已排序的子数组涉及处理N个元素。

再往下一层,第3步的每次迭代都涉及处理N/2个元素,但您必须重复此过程两次。所以你仍然在处理2 * N/2 == N个元素。

再往下一层,你要合并4 * N/4 == N个元素,以此类推。递归堆栈中的每个深度都涉及合并相同数量的元素,涉及对该深度的所有调用。

考虑一下快速排序算法:

选择一个枢轴点 将枢轴点放置在数组中的正确位置,所有较小的元素放在左边,较大的元素放在右边 对左子数组进行排序 对右子数组排序

在顶层,你处理的是一个大小为n的数组,然后选择一个枢轴点,把它放在正确的位置,然后可以在算法的其余部分完全忽略它。

再往下一层,您将处理2个子数组,它们的组合大小为N-1(即减去之前的枢轴点)。为每个子数组选择一个枢轴点,总共有2个额外的枢轴点。

再往下一层,您将处理4个子数组,它们的组合大小为N-3,原因与上面相同。

然后N-7…然后c15…然后N-32…

递归堆栈的深度保持大致相同(logN)。使用归并排序,你总是在递归堆栈的每一层处理n个元素的归并。但是使用快速排序,你要处理的元素数量会随着你在堆栈中向下移动而减少。例如,如果你在递归堆栈中查看深度,你正在处理的元素数量是N - 2^((logN)/2)) == N -根号(N)。

声明:对于归并排序,因为每次都将数组分割为两个完全相等的块,所以递归深度正好是logN。在快速排序时,由于枢轴点不太可能恰好位于数组的中间,因此递归堆栈的深度可能略大于logN。我还没有做过数学计算,看看这个因素和上面描述的因素在算法复杂性中究竟扮演了多大的角色。