it-swarm.cn

为什么quicksort比mergesort更好?

我在接受采访时被问到这个问题。它们都是O(nlogn)但是大多数人使用Quicksort而不是Mergesort。这是为什么?

337
Malik Daud Ahmad Khokhar

Quicksort有O(ñ2)最坏情况运行时和O(ñ日志ñ)平均案例运行时。但是,它在许多场景中优于合并排序,因为许多因素会影响算法的运行时间,并且当将它们全部放在一起时,快速排序会胜出。

特别地,经常引用的排序算法运行时指的是执行排序数据所需的比较次数或交换次数。这确实是衡量性能的一个很好的指标,特别是因为它独立于底层硬件设计。但是,其他的东西 - 比如引用的位置(即我们读取了很多可能在缓存中的元素?) - 在当前的硬件上也扮演着重要的角色。 Quicksort特别需要很少的额外空间并且具有良好的缓存局部性,这使得它在许多情况下比合并排序更快。

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

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

251
Konrad Rudolph

正如许多人所指出的,快速排序的平均案例性能比mergesort快。 但是 只有当你假设有时间按需访问任何内存时才会出现这种情况。

在RAM这个假设通常不是太糟糕(因为缓存并不总是如此,但它并不太糟糕)。但是,如果您的数据结构足够大,可以存放在磁盘上,那么快速排序会得到 death 这是因为您的平均磁盘每秒执行200次随机搜索。但是同一个磁盘在顺序读取或写入每秒兆字节的数据方面没有问题。这正是mergesort的作用。

因此,如果必须在磁盘上对数据进行排序,那么您真的非常希望在mergesort上使用一些变体。 (通常,您会快速排序子列表,然后开始将它们合并到一些大小阈值之上。)

此外,如果您必须对该大小的数据集执行 anything ,请认真考虑如何避免寻找磁盘。例如,这就是为什么在数据库中执行大量数据加载之前删除索引的标准建议,然后再重建索引。在加载期间维护索引意味着不断寻求磁盘。相比之下,如果删除索引,那么数据库可以通过首先对要处理的信息进行排序(当然使用mergesort!)然后将其加载到索引的BTREE数据结构中来重建索引。 (BTREE自然会按顺序保存,因此您可以从排序的数据集中加载一个,只需很少的搜索到磁盘。)

在很多情况下,理解如何避免磁盘搜索让我使数据处理工作需要数小时而不是数天或数周。

270
user11318

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

QuickSort更受欢迎,因为它:

  1. 就地(MergeSort需要额外的内存线性到要排序的元素数量)。
  2. 有一个小的隐藏常数。
87
Dark Shikari

“然而大多数人使用Quicksort而不是Mergesort。为什么会这样?”

没有给出的一个心理原因就是Quicksort更加巧妙地命名。即良好的营销。

是的,具有三重分区的Quicksort可能是最好的通用排序算法之一,但是没有克服“快速”排序听起来比“合并”排序更强大的事实。

31
Ash

正如其他人所说,Quicksort的最坏情况是O(n ^ 2),而mergesort和heapsort留在O(nlogn)。然而,在一般情况下,所有三个都是O(nlogn);所以他们对绝大多数情况都具有可比性。

使Quicksort平均更好的原因是内循环意味着将几个值与单个值进行比较,而另外两个值对于每个比较都是不同的。换句话说,Quicksort读取的数量是其他两种算法的一半。在现代CPU上,性能主要受访问时间的影响,因此最终Quicksort成为首选。

15
Javier

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

但是,说实话,在实际情况下,大多数人只需要良好的平均表现,快速排序就是......快=)

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

8
Antti Rasinen

来自 Quicksort上的维基百科条目

Quicksort还与mergesort竞争,这是另一种递归排序算法,但具有最坏情况Θ(nlogn)运行时间的好处。 Mergesort是一种稳定的类型,与quicksort和heapsort不同,并且可以轻松地适应在链接列表和存储在缓慢访问介质(如磁盘存储或网络附加存储)上的非常大的列表上运行。尽管可以编写快速排序以在链接列表上操作,但是它通常会在没有随机访问的情况下遭受不良的数据透视选择。 mergesort的主要缺点是,当在数组上操作时,它在最佳情况下需要Θ(n)辅助空间,而具有就地分区和尾递归的快速排序的变体仅使用Θ(logn)空间。 (请注意,在链接列表上操作时,mergesort只需要一小块恒定的辅助存储空间。)

7
gnobal

慕! Quicksort不是更好,它比mergesort更适合不同类型的应用程序。

如果速度至关重要,Mergesort是值得考虑的,不能容忍坏的最坏情况,并且有额外的空间. 1

你说他们'他们都是O(nlogn) [...]»。这是错的。 «Quicksort在最坏的情况下使用大约n ^ 2/2比较。» 1

然而,根据我的经验,最重要的属性是在使用具有命令式范例的编程语言时可以在排序时轻松实现顺序访问。

1 Sedgewick,算法

7
Roman Glass

Quicksort是实践中最快的排序算法,但有许多病理案例可以使它的表现与O(n2)一样糟糕。

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

6
Niyaz

维基百科的解释是:

通常,快速排序在实践中比其他Θ(nlogn)算法快得多,因为它的内循环可以在大多数架构上有效地实现,并且在大多数现实世界数据中,可以做出设计选择,从而最小化需要二次时间的概率。 。

Quicksort

Mergesort

我认为快速排序实现所没有的Mergesort所需的存储量(即Ω(n))也存在问题。在最坏的情况下,它们的算法时间相同,但mergesort需要更多的存储空间。

5
Mat Mannion

我想在现有的优秀答案中添加一些关于QuickSort在与最佳案例分歧时的表现以及可能性如何的数学,我希望这将有助于人们更好地理解为什么O(n ^ 2)案例不是真实的关注更复杂的QuickSort实现。

除了随机访问问题之外,有两个主要因素会影响QuickSort的性能,它们都与枢轴与正在排序的数据的比较有关。

1)数据中的少量键。所有相同值的数据集将在香草2分区QuickSort上排序n ^ 2次,因为除了枢轴位置之外的所有值每次都放在一侧。现代实现通过诸如使用3分区排序的方法来解决此问题。这些方法在O(n) time中对所有相同值的数据集执行。因此,使用这样的实现意味着具有少量密钥的输入实际上改善了性能时间并且不再是一个问题。

2)非常糟糕的枢轴选择会导致最坏的情况。在理想情况下,枢轴总是这样,50%的数据较小,50%的数据较大,因此在每次迭代期间输入将被分成两半。这给了我们比较和交换时间log-2(n)递归O(n * logn)时间。

非理想的枢轴选择对执行时间有多大影响?

让我们考虑这样一种情况,即一致地选择枢轴,使得75%的数据位于枢轴的一侧。它仍然是O(n * logn),但现在日志的基数已经变为1/0.75或1.33。更改base时的性能关系始终是log(2)/ log(newBase)表示的常量。在这种情况下,该常数为2.4。因此,这种枢轴选择的质量比理想选择长2.4倍。

这变得多快?

在枢轴选择变得非常糟糕之前不是很快:

  • 一边50%:(理想情况)
  • 一方75%:2.4倍
  • 一方90%:6.6倍
  • 一方95%:13.5倍
  • 一方99%:69倍

当我们在一侧接近100%时,执行的日志部分接近n并且整个执行渐近地接近O(n ^ 2)。

在QuickSort的简单实现中,诸如排序数组(用于第一元素枢轴)或反向排序数组(用于最后一个元素枢轴)的情况将可靠地产生最坏情况的O(n ^ 2)执行时间。此外,具有可预测的枢轴选择的实现可能会受到旨在产生最坏情况执行的数据的DoS攻击。现代实现通过各种方法避免这种情况,例如在排序之前随机化数据,选择3个随机选择的索引的中位数等。通过混合中的这种随机化,我们有2个案例:

  • 小数据集。最坏情况是合理可能的,但O(n ^ 2)不是灾难性的,因为n足够小,n ^ 2也很小。
  • 大数据集。最坏的情况在理论上是可能的,但在实践中却不是。

我们有多大可能看到糟糕的表现?

机会是非常小。让我们考虑一下5,000种值:

我们的假设实现将使用3个随机选择的索引的中值选择一个支点。我们将考虑在25%-75%范围内的枢轴为“良好”,并且在0%-25%或75%-100%范围内的枢轴为“差”。如果你使用3个随机索引的中位数来观察概率分布,每次递归都有11/16的机会以一个好的支点结束。让我们做两个保守(和假)假设来简化数学:

  1. 良好的枢轴总是精确地处于25%/ 75%的分割,并且在2.4 *理想情况下运行。我们永远不会得到理想的分裂或任何超过25/75的分裂。

  2. 坏枢轴总是最坏的情况,并且基本上对解决方案没有贡献。

我们的QuickSort实现将在n = 10时停止并切换到插入排序,因此我们需要22个25%/ 75%的透视分区来打破到目前为止的5,000值输入。 (10 * 1.333333 ^ 22> 5000)或者,我们需要4990个最坏情况的支点。请记住,如果我们在任意点累积22个好的枢轴,那么排序将完成,所以最坏的情况或接近它的任何事情都需要极其运气不好。如果我们需要88次递归才能真正实现排序到n = 10所需的22个好的枢轴,这将是4 * 2.4 *理想情况或大约是理想情况的执行时间的10倍。在88次递归后,我们有多大可能达到所需的22个好的枢轴?

二项概率分布 可以回答,答案大约是10 ^ -18。 (n是88,k是21,p是0.6875)你的用户在点击[SORT]所需的1秒钟内被闪电击中的可能性大约是看到5,000项物品运行_ {更糟比10 *理想情况。随着数据集变大,这种机会变小。以下是一些数组大小及其相应的运行时间超过10 *的机会:

  • 640个项目的数组:10 ^ -13(在60次尝试中需要15个好的轴心点)
  • 5,000件物品的数组:10 ^ -18(在88次尝试中需要22个好的枢轴)
  • 40,000项目的数组:10 ^ -23(116个中需要29个好的枢轴)

请记住,这是有两个比现实更糟糕的保守假设。因此实际表现更好,剩余概率的平衡更接近理想。

最后,正如其他人所提到的,如果递归堆栈太深,切换到堆排序也可以消除这些荒谬的不可能的情况。因此,对于QuickSort的良好实现,TLDR是最坏的情况并不存在因为它已被设计出来并且执行在O(n * logn)时间内完成。

4
Lance Wisely

Quicksort并不比mergesort好。使用O(n ^ 2)(最坏情况很少发生),快速排序可能比合并排序的O(nlogn)慢得多。 Quicksort的开销较小,因此对于小型和慢速计算机,它更好。但是今天的计算机速度如此之快,以至于mergesort的额外开销可以忽略不计,而且快速排序速度非常慢的风险远远超过了大多数情况下mergesort的微不足道的开销。

此外,mergesort以原始顺序保留具有相同键的项,这是一个有用的属性。

4
xpda

与合并排序不同,快速排序不使用辅助空间。而Merge Sort使用辅助空间O(n)。但是Merge Sort具有O(nlogn)的最坏情况时间复杂度,而Quick Sort的最坏情况复杂度是O(n ^ 2),当数组已经被排序时发生。

3
Shantam Mittal

对于原始值的DualPivotQuickSort带来的变化,答案会略微倾向于快速输入w.r.t.它在 Java 7 中用于排序 Java.util.Arrays

It is proved that for the Dual-Pivot Quicksort the average number of
comparisons is 2*n*ln(n), the average number of swaps is 0.8*n*ln(n),
whereas classical Quicksort algorithm has 2*n*ln(n) and 1*n*ln(n)
respectively. Full mathematical proof see in attached proof.txt
and proof_add.txt files. Theoretical results are also confirmed
by experimental counting of the operations.

你可以在这里找到Java7的实现 - http://grepcode.com/file/repository.grepcode.com/Java/root/jdk/openjdk/7-b147/Java/util/Arrays.Java

关于DualPivotQuickSort的更多精彩阅读 - http://permalink.gmane.org/gmane.comp.Java.openjdk.core-libs.devel/2628

3
SSR

在merge-sort中,通用算法是:

  1. 对左子阵列进行排序
  2. 对右子阵列进行排序
  3. 合并2个排序的子数组

在顶层,合并2个排序的子阵列涉及处理N个元素。

比这低一级,步骤3的每次迭代都涉及处理N/2个元素,但是你必须重复这个过程两次。所以你还在处理2 * N/2 == N个元素。

低于此级别,您将合并4 * N/4 == N个元素,依此类推。递归堆栈中的每个深度都涉及在该深度的所有调用中合并相同数量的元素。

请考虑使用快速排序算法:

  1. 选择一个支点
  2. 将轴心点放在数组中的正确位置,左边是所有较小的元素,右边是较大的元素
  3. 对左子阵列进行排序
  4. 对右子阵列进行排序

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

在这之下的一个级别,您将处理2个子组合,这些子组合的大小为N-1(即,减去较早的枢轴点)。您为每个子阵列选择一个轴心点,最多可增加2个轴心点。

由于与上述相同的原因,您将处理4个组合大小为N-3的子阵列。

那么N-7 ......那么N-15 ......那么N-32 ......

递归堆栈的深度保持大致相同(logN)。使用merge-sort,您总是在递归堆栈的每个级别处理N元素合并。但是,通过快速排序,当您下到堆栈时,您正在处理的元素数量会减少。例如,如果您查看递归堆栈中间的深度,则您要处理的元素数量为N - 2 ^((logN)/ 2))== N - sqrt(N)。

免责声明:在merge-sort上,因为每次将数组分成2个完全相等的块,递归深度正好是logN。在快速排序时,因为您的枢轴点不可能完全位于数组的中间,递归堆栈的深度可能略大于logN。我没有做过数学计算,看看这个因素和上面描述的因素在算法的复杂性中实际发挥了多大作用。

3
RvPr

Quicksort具有更好的平均案例复杂性,但在某些应用程序中它是错误的选择。 Quicksort易受拒绝服务攻击。如果攻击者可以选择要排序的输入,他可以轻松地构造一个最差情况时间复杂度为o(n ^ 2)的集合。

Mergesort的平均案例复杂性和最坏情况复杂性是相同的,因此不会遇到同样的问题。 merge-sort的这个属性也使它成为实时系统的最佳选择 - 正是因为没有病态的情况导致它运行得多,慢得多。

由于这些原因,我是Mergesort的忠实粉丝,而不是Quicksort。

2
Simon Johnson

虽然它们都处于相同的复杂性类中,但这并不意味着它们都具有相同的运行时。 Quicksort通常比mergesort更快,因为它更容易编写严格的实现,并且它的操作可以更快。这是因为快速排序通常比人们使用它而不是mergesort更快。

然而!我个人经常会使用mergesort或quicksort变体,当quicksort表现不佳时会降级为mergesort。记得。 Quicksort只有O(n log n)on average 。最坏的情况是O(n ^ 2)! Mergesort总是O(n log n)。如果必须使用实时性能或响应能力并且您的输入数据可能来自恶意来源,您不应该使用普通快速排序。

2
DJ Capelis

快速排序是最坏情况O(n ^ 2),但是,平均情况始终执行合并排序。每个算法都是O(nlogn),但你需要记住,在谈论Big O时,我们忽略了较低的复杂因素。当涉及常数因素时,快速排序比合并排序有显着改进。

合并排序还需要O(2n) memory,而快速排序可以就地完成(只需要O(n))。这是快速排序通常优于合并排序的另一个原因。

额外信息:

当枢轴选择不当时,会发生最快的快速排序。请考虑以下示例:

[5,4,3,2,1]

如果选择枢轴作为组中的最小或最大数字,则快速排序将在O(n ^ 2)中运行。选择列表中最大或最小25%的元素的概率为0.5。这使得算法成为一个良好的支点。如果我们采用典型的枢轴选择算法(比如选择一个随机元素),我们就有0.5个机会为每个枢轴选择一个好的枢轴。对于大尺寸的集合,总是选择不良枢轴的概率是0.5 * n。基于此概率,快速排序对于平均(和典型)情况是有效的。

2
Wade Anderson

为什么Quicksort好?

  • QuickSort在最坏的情况下采用N ^ 2和NlogN平均情况。最糟糕的情况发生在数据排序时。在排序开始之前,可以通过随机混洗来减轻这种情况。
  • QuickSort不会占用合并排序所需的额外内存。
  • 如果数据集很大且项目相同,则Quicksort的复杂性通过使用3路分区而降低。更多相同的项目更好的排序。如果所有项目都相同,则按线性时间排序。 [这是大多数库中的默认实现]

Quicksort总是比Mergesort好吗?

不是真的。

  • Mergesort稳定但Quicksort不稳定。因此,如果您需要输出稳定性,则可以使用Mergesort。在许多实际应用中需要稳定性。
  • 记忆现在很便宜。因此,如果Mergesort使用的额外内存对您的应用程序并不重要,那么使用Mergesort没有任何害处。

注意: 在Java中,Arrays.sort()函数使用Quicksort作为原始数据类型,使用Mergesort作为对象数据类型。因为对象消耗了内存开销,所以为Mergesort添加一点开销可能不是性能观点的任何问题。

参考 :观看 第3周的QuickSort视频,Coursera的普林斯顿算法课程

2
Sanjeev Kumar Dangi

当我尝试使用两种排序算法时,通过计算递归调用的数量,quicksort始终比mergesort具有更少的递归调用。这是因为quicksort具有枢轴,并且枢轴不包括在下一个递归调用中。这样,quicksort可以比mergesort更快地达到递归基本情况。

2
Aldian Fazrihady

快速与合并排序的小增加。

它也可以取决于分拣物品的种类。如果访问项目,交换和比较不是简单的操作,比如比较平面内存中的整数,那么合并排序可以是更好的算法。

例如,我们使用远程服务器上的网络协议对项目进行排序。

此外,在像“链表”这样的自定义容器中,快速排序也没有好处。
1。在链表上合并排序,不需要额外的内存。 2.快速排序中元素的访问不是顺序的(在内存中)

1
minorlogic

这很难说。最差的MergeSort是n(log2n)-n + 1,如果n等于2 ^ k,这是准确的(我已经证明了这一点)。对于任何n,它在(n lg n - n +之间) 1)和(n lg n + n + O(lg n))。但是对于quickSort,最好的是nlog2n(也是n等于2 ^ k)。如果你用quickSort划分Mergesort,当n是无穷时它等于1。好像MergeSort的最坏情况好比QuickSort最好的情况好,我们为什么要使用quicksort?但是请记住,MergeSort不到位,它需要2n memeroy空间。而MergeSort也需要做很多阵列拷贝,我们在一个Word中,MergeSort比theroy中的quicksort真的更加苛刻,但实际上你需要考虑记忆空间,数组复制的成本,合并比快速排序慢。我曾经做过一个实验我在Java中通过Random类给出了1000000个数字,并且它通过mergesort获得了2610ms,通过quicksort获得了1370ms。

1
Peter

这是一个非常古老的问题,但由于我最近在这里处理了两个问题,我的2c:

合并排序需要平均~N log N比较。对于已经(几乎)排序的排序数组,这降低到1/2 N log N,因为在合并时我们(几乎)总是选择“左”部分1/2 N次,然后只复制右1/2 N个元素。此外,我可以推测已经排序的输入使处理器的分支预测器闪耀,但几乎所有分支都正确猜测,从而防止管道停滞。

平均快速排序需要~1.38 N log N比较。在比较方面,它没有从已经排序的数组中获益很大(但是它在交换方面确实有用,并且可能在CPU内部的分支预测方面)。

我对相当现代的处理器的基准测试显示如下:

当比较函数是回调函数时(如在qsort()libc实现中),quicksort比随机输入的mergesort慢15%,对于64位整数,已经排序的数组为30%。

另一方面,如果比较不是回调,我的经验是quicksort比mergesort高出25%。

但是,如果您的(大型)数组具有非常少的唯一值,则在任何情况下,合并排序都会开始获得快速排序。

所以也许最重要的是:如果比较是昂贵的(例如回调函数,比较字符串,比较结构的许多部分,大多数得到第二个 - 第三个“if”以产生差异) - 你可能会更好合并排序。对于更简单的任务,quicksort会更快。

这说以前所说的都是真的: - Quicksort可以是N ^ 2,但是Sedgewick声称一个好的随机实现有更多机会让计算机执行排序被闪电击中而不是去N ^ 2 - Mergesort需要额外的空间

1
virco

在所有条件相同的情况下,我希望大多数人都能使用最方便的东西,而且往往是qsort(3)。除了快速排序之外,已知数组上的快速排序非常快,就像mergesort是列表的常见选择一样。

我想知道为什么看到 radix 或桶排序是如此罕见。它们是O(n),至少在链表上,所需要的只是将密钥转换为序数的一些方法。 (字符串和浮点数工作正常。)

我认为原因与计算机科学的教学方式有关。我甚至不得不向算法分析的讲师证明,确实可以比O(n log(n))更快地排序。 (他有证据证明你不能比较比O(n log(n))排序更快,这是真的。)

在其他新闻中,浮点数可以按整数排序,但您必须在之后转换负数。

编辑:实际上,这是一种更加恶毒的方式来整理浮点数 - 整数: http://www.stereopsis.com/radix.html 。请注意,无论您实际使用哪种排序算法,都可以使用位翻转技巧...

1
Anders Eurenius

考虑时间和空间复杂性。对于合并排序:时间复杂度:O(nlogn),空间复杂度:O(nlogn)

对于快速排序:时间复杂度:O(n ^ 2),空间复杂度:O(n)

现在,他们都在一场比赛中获胜。但是,使用随机数据透视表,您几乎总能将快速排序的时间复杂度降低到O(nlogn)。

因此,快速排序在许多应用程序中是首选,而不是合并排序。

0
pankaj

快速排序是一种就地排序算法,因此它更适合于数组。另一方面,合并排序需要额外存储O(N),并且更适合链接列表。

与数组不同,在喜欢的列表中我们可以在O(1) space和O(1) time的中间插入项目,因此合并排序中的合并操作可以在没有任何内容的情况下实现额外的空间。但是,为数组分配和取消分配额外空间会对合并排序的运行时间产生负面影响。合并排序也有利于链表,因为数据是按顺序访问的,没有太多的随机内存访问。

另一方面,快速排序需要大量随机内存访问,并且通过数组,我们可以直接访问内存而无需链接列表所需的任何遍历。当用于数组时,快速排序具有良好的引用局部性,因为数组连续存储在存储器中。

尽管两种排序算法的平均复杂度都是O(NlogN),但普通任务的人通常使用数组进行存储,因此快速排序应该是首选算法。

编辑:我刚刚发现合并排序最差/最佳/平均情况总是nlogn,但快速排序可以从n2(元素已经排序的最坏情况)到nlogn(avg /最佳情况,当pivot总是将数组分成两个时)半)。

0
Saad