采访中有人问我这个问题。它们都是O(nlogn),但是大多数人都使用Quicksort而不是Mergesort。这是为什么?
"easier to hack a mergesort to do it than a quicksort"
?您可以引用任何具体示例吗?
采访中有人问我这个问题。它们都是O(nlogn),但是大多数人都使用Quicksort而不是Mergesort。这是为什么?
"easier to hack a mergesort to do it than a quicksort"
?您可以引用任何具体示例吗?
Answers:
Quicksort具有O(n 2)最坏情况运行时和O(n log n)平均情况运行时。但是,在许多情况下合并排序会更好,因为许多因素会影响算法的运行时间,并且将它们放在一起时,快速排序会胜出。
特别地,经常引用的排序算法的运行时是指执行比较或对数据进行排序所需的交换次数。这确实是一个很好的性能衡量标准,尤其是因为它独立于底层硬件设计。但是,其他因素(例如引用的局部性(即,我们是否读取了很多可能在缓存中的元素?))在当前硬件上也起着重要作用。特别是Quicksort,几乎不需要额外的空间,并且具有良好的缓存局部性,因此在许多情况下,它比合并排序要快。
此外,通过适当选择枢轴(例如随机选择),几乎完全可以避免quicksort的最坏情况下的运行时间O(n 2)。
实际上,quicksort的许多现代实现(特别是libstdc ++的std::sort
)实际上是introsort,其理论上最差的情况是O(n log n),与归并排序相同。它通过限制递归深度,并在超过log n时切换到其他算法(heapsort)来实现此目的。
正如许多人所指出的,Quicksort的平均案例性能比mergesort更快。 但这只有在假设您有恒定的时间按需访问任何内存时才是正确的。
在RAM中,此假设通常不太差(由于高速缓存,它并不总是正确的,但也不太糟)。但是,如果您的数据结构足够大,可以存储在磁盘上,那么快速排序就会被您的平均磁盘每秒执行200次随机寻道的速度杀死。但是,同一张磁盘没有顺序顺序读取或写入每秒兆字节数据的麻烦。这正是mergesort所做的。
因此,如果必须在磁盘上对数据进行排序,那么您真的很想在mergesort上使用一些变体。(通常,您先对子列表进行快速排序,然后在某个大小阈值以上将它们合并在一起。)
此外,如果您必须对如此大小的数据集执行任何操作,请认真考虑如何避免寻找磁盘。例如,这就是为什么这样的建议,即在数据库中进行大量数据加载之前先删除索引,然后再重建索引,这是标准建议。在加载期间保持索引意味着不断寻找磁盘。相反,如果删除索引,则数据库可以通过以下方式重建索引:首先对要处理的信息进行排序(当然要使用mergesort!),然后将其加载到该索引的BTREE数据结构中。(BTREE本质上是保持顺序的,因此您可以从排序的数据集中加载一个,而很少有磁盘寻道。)
在很多情况下,了解如何避免磁盘寻道使我使数据处理作业花费数小时而不是数天或数周。
0
进入n
,下次便从n
进入0
。这带来了以下优势:撤消(排序)内存(缓存)中已经可用的数据块,并且仅对一个磁盘访问进行两次攻击。我认为大多数DBMS都使用这种优化技术。
实际上,QuickSort是O(n 2)。它的平均运行时间为O(nlog(n)),但最差的运行时间为O(n 2),当您在包含很少的唯一项目的列表上运行它时,会发生这种情况。随机化为O(n)。当然,这不会改变最坏的情况,它只是防止恶意用户使您的排序花费很长时间。
QuickSort之所以受欢迎,是因为它:
“但是大多数人使用Quicksort而不是Mergesort。为什么呢?”
尚未给出的心理原因之一就是Quicksort的命名更加巧妙。即良好的营销。
是的,具有三重分割的Quicksort可能是最好的通用排序算法之一,但是“ Quick”排序听起来比“ Merge”排序强大得多。
我想补充一下到目前为止提到的三个算法(mergesort,quicksort和堆排序),只有mergesort是稳定的。也就是说,对于具有相同键的那些值,顺序不会更改。在某些情况下,这是理想的。
但是,说实话,在实际情况下,大多数人只需要良好的平均表现,而quicksort是... quick =)
所有排序算法都有其起伏。请参阅Wikipedia文章中有关排序算法的完整概述。
Quicksort还与另一种递归排序算法mergesort竞争,但它具有最坏情况下Θ(nlogn)运行时间的优点。与快速排序和堆排序不同,合并排序是一种稳定的排序,可以轻松地对其进行调整,以对存储在访问速度较慢的介质(例如磁盘存储或网络连接存储)上的链表和非常大的列表进行操作。尽管可以将quicksort编写为可在链接列表上进行操作,但通常会在没有随机访问权限的情况下遭受枢轴选择不当的困扰。mergesort的主要缺点是,在数组上进行操作时,在最佳情况下需要Θ(n)辅助空间,而具有就地分区和尾递归的quicksort变体仅使用Θ(logn)空间。(请注意,在对链表进行操作时,mergesort仅需要少量恒定的辅助存储。)
亩! Quicksort并不是更好,它比mergesort更适合于其他类型的应用程序。
如果速度至关重要,那么Mergesort值得考虑,不能容忍最差的糟糕表现,并且可以提供额外的空间。1个
您说过他们“他们都是O(nlogn)[…]»”。错了 «在最坏的情况下,Quicksort使用大约n ^ 2/2的比较。» 1。
但是,根据我的经验,最重要的属性是在使用带有命令式范式的编程语言时在排序时可以轻松使用顺序访问。
1 Sedgewick,算法
维基百科的解释是:
通常,快速排序在实践中比其他Θ(nlogn)算法快得多,因为它的内部循环可以在大多数体系结构上有效实现,并且在大多数实际数据中,可以进行设计选择,从而将需要二次时间的可能性降到最低。
我认为Mergesort所需的存储量(Ω(n))也存在一些问题,而Quicksort实施则没有。在最坏的情况下,它们的算法时间相同,但是mergesort需要更多的存储空间。
我想在现有的很好的答案中添加一些有关QuickSort在脱离最佳情况时的表现以及该可能性的可能性的一些数学信息,希望这有助于人们更好地理解O(n ^ 2)情况为何不真实的原因。关注QuickSort的更复杂的实现。
除随机访问问题外,还有两个主要因素可能会影响QuickSort的性能,它们都与数据透视表与正在排序的数据进行比较的方式有关。
1)数据中有少量键。具有相同值的数据集将在香草2分区QuickSort上以n ^ 2的时间排序,因为除枢轴位置以外的所有值每次都放置在一侧。现代实现通过诸如使用3分区排序的方法来解决此问题。这些方法在O(n)时间内对所有相同值的数据集执行。因此,使用这种实现方式意味着具有少量键的输入实际上可以提高性能,并且不再需要担心。
2)极差的枢轴选择会导致最坏的情况。在理想情况下,枢轴将始终保持这样的状态,即50%的数据较小而50%的数据较大,以便在每次迭代期间将输入分成两半。这给了我们n次比较,并将log-2(n)个递归时间交换为O(n * logn)时间。
非理想的数据透视选择对执行时间有多大影响?
让我们考虑这样一种情况:一致选择枢轴,使得75%的数据位于枢轴的一侧。它仍然是O(n * logn),但是现在日志的底数已更改为1 / 0.75或1.33。更改基准时性能上的关系始终是由log(2)/ log(newBase)表示的常数。在这种情况下,该常数为2.4。因此,这种枢轴选择的质量比理想的时间长2.4倍。
这种情况恶化有多快?
直到枢轴选择(总是)变得非常糟糕之前,速度不是很快:
当我们一侧接近100%时,执行的对数部分接近n,整个执行渐近地接近O(n ^ 2)。
在QuickSort的简单实施中,诸如排序数组(用于第一个元素枢轴)或反向排序数组(用于最后一个元素枢轴)的情况将可靠地产生最坏情况的O(n ^ 2)执行时间。此外,具有可预测枢轴选择的实现可能会受到旨在产生最坏情况执行的数据的DoS攻击。现代的实现通过多种方法来避免这种情况,例如在排序之前对数据进行随机化,选择3个随机选择的索引的中位数等。在这种混合方式下,我们有2种情况:
我们看到糟糕表现的可能性有多大?
机会是微乎其微。让我们考虑一下5,000个值:
我们的假设实现将使用3个随机选择的索引的中位数来选择枢轴。我们将认为25%-75%范围内的枢轴为“良好”,而0%-25%或75%-100%范围内的枢轴为“不良”。如果使用3个随机索引的中值查看概率分布,则每个递归最终都有11/16的机会以良好的支点结束。让我们做出两个保守(和错误)的假设来简化数学:
好的支点总是精确地以25%/ 75%的比例分配,并在2.4 *理想情况下运行。我们永远不会获得理想的拆分或任何优于25/75的拆分。
错误的数据透视总是最坏的情况,对解决方案基本上没有任何帮助。
我们的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 *理想值的机会:
请记住,这是基于两个比实际情况差的保守假设。因此实际性能更好,剩余概率的平衡比不更接近理想。
最后,正如其他人所提到的,如果递归堆栈太深,甚至可以通过切换到堆排序来消除这些不太可能的情况。因此,TLDR是,对于QuickSort的良好实现而言,最坏的情况并不真正存在,因为它已被设计出来并且执行时间为O(n * logn)。
为什么Quicksort很好?
Quicksort总是比Mergesort更好吗?
并不是的。
注意:在Java中,Arrays.sort()函数将Quicksort用于原始数据类型,将Mergesort用于对象数据类型。因为对象消耗内存开销,所以从性能的角度来看,为Mergesort添加一点开销可能不是什么问题。
参考:在Coursera观看普林斯顿算法课程第3周的QuickSort视频
Quicksort并不比mergesort好。使用O(n ^ 2)(最罕见的情况,很少发生),快速排序可能比合并排序的O(nlogn)慢得多。Quicksort的开销较小,因此对于n较小且速度较慢的计算机,它会更好。但是今天的计算机是如此之快,以至于合并排序的额外开销可以忽略不计,而且非常慢的快速排序的风险在大多数情况下远远超过合并排序的无关紧要的开销。
此外,mergesort会按原始顺序保留具有相同键的项目,这是一个有用的属性。
<=
用于比较而不是<
,并且没有理由不这样做。
答案会稍微倾向于quicksort,而不是DualPivotQuickSort为原始值带来的更改。它在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 implmentation 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
在归并排序中,通用算法为:
在顶层,合并2个排序的子数组涉及处理N个元素。
在此之下一级,步骤3的每次迭代都涉及处理N / 2个元素,但是您必须重复此过程两次。因此,您仍在处理2 * N / 2 == N个元素。
在此之下一级,您正在合并4 * N / 4 == N个元素,依此类推。递归堆栈中的每个深度都涉及在该深度的所有调用中合并相同数量的元素。
请考虑使用快速排序算法:
在顶层,您正在处理大小为N的数组。然后选择一个枢轴点,将其放置在正确的位置,然后可以在算法的其余部分中完全忽略它。
在此之下一级,您正在处理两个子数组,它们的总大小为N-1(即减去先前的枢轴点)。您为每个子阵列选择一个枢轴点,最多可以有2个附加枢轴点。
由于与上述相同的原因,因此在这之下一级,您正在处理4个组合大小为N-3的子阵列。
然后是N-7 ...然后是N-15 ...然后是N-32 ...
递归堆栈的深度大致保持不变(logN)。使用merge-sort,您将始终在递归堆栈的每个级别上处理N个元素的合并。但是,通过快速排序,您要处理的元素数量会随着堆栈的减少而减少。例如,如果您查看递归堆栈中间的深度,则要处理的元素数为N-2 ^((logN)/ 2))== N-sqrt(N)。
免责声明:在归并排序时,由于每次将数组分为2个完全相等的块,因此递归深度为logN。在快速排序中,由于枢轴点不太可能恰好位于数组的中间,因此递归堆栈的深度可能比logN稍大。我还没有做数学运算来了解这个因素和上述因素实际上在算法的复杂性中起多大作用。
Quicksort具有更好的平均案例复杂性,但是在某些应用程序中,这是错误的选择。Quicksort容易受到拒绝服务攻击。如果攻击者可以选择要排序的输入,则他可以轻松构建一个集合,该集合的最坏情况下的时间复杂度为o(n ^ 2)。
Mergesort的平均大小写复杂度和最坏情况的复杂度是相同的,因此不会遇到相同的问题。归并排序的这一特性也使其成为实时系统的绝佳选择-正是因为没有病理情况导致它的运行速度大大降低。
由于这些原因,我比Mergesort更喜欢Mergesort。
很难说.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)。如果将Mergesort除以quickSort,则当n为无穷大时等于1。好像MergeSort的最坏情况比QuickSort的最好情况要好,为什么我们要使用quicksort?但是请记住,MergeSort没有到位,它需要2n的memeroy空间。而且MergeSort还需要做很多数组拷贝,这总而言之,MergeSort确实比快速排序更吸引人,但实际上您需要考虑内存空间,数组复制的成本,合并要比快速排序慢。实验中,随机类为我提供了1000000个Java数字,mergesort用了2610毫秒,quicksort用了1370毫秒。
快速排序是最坏的情况O(n ^ 2),但是,平均情况始终一致执行合并排序。每种算法都是O(nlogn),但是您需要记住,在谈论Big O时,我们忽略了较低的复杂性因素。当涉及到恒定因素时,快速排序相对于合并排序具有重大改进。
合并排序还需要O(2n)内存,而快速排序可以就地完成(仅需要O(n))。这是快速排序通常优于合并排序的另一个原因。
额外信息:
快速排序的最坏情况发生在枢轴选择不当时。考虑以下示例:
[5,4,3,2,1]
如果将数据透视表选为组中的最小或最大数,则快速排序将以O(n ^ 2)进行。选择列表中最大或最小25%的元素的概率为0.5。这给算法带来了0.5个很好的机会。如果我们采用典型的枢轴选择算法(例如选择随机元素),则每次枢轴选择都有0.5个机会选择一个好的枢轴。对于较大的集合,始终选择差的轴点的概率为0.5 * n。基于此概率,对于平均(和典型)情况,快速排序是有效的。
这是一个很老的问题,但是由于我最近都处理过这两个问题,所以这里是我的2c:
合并排序平均需要约N个log N比较。对于已经(几乎)排序的已排序数组,它降至1/2 N log N,因为在合并时,我们(几乎)总是选择1/2 N次“左侧”部分,然后仅复制右侧1/2 N个元素。另外,我可以推测,已经排序的输入使处理器的分支预测变量发光,但几乎可以正确猜测几乎所有分支,从而防止了管道停顿。
快速排序平均需要〜1.38 N log N个比较。从比较方面来说,它并不能从已经排序的数组中获得很大的好处(但是,在交换方面,甚至在CPU内部的分支预测方面,它都可以这样做)。
我在相当现代的处理器上的基准测试显示以下内容:
如果比较函数是回调函数(例如qsort()libc实现中的回调函数),则对随机输入而言,快速排序的速度比合并排序要慢15%,对于64位整数的已排序数组,快速排序要慢30%。
另一方面,如果比较不是回调,则我的经验是,快速排序比合并排序要好25%。
但是,如果您的(大)数组具有很少的唯一值,则无论如何合并合并排序都会开始超过快速排序。
因此,也许最重要的是:如果比较昂贵(例如,回调函数,比较字符串,比较结构的许多部分,大多数情况下达到“ if”的三分之一),那么您可能会变得更好与合并排序。对于更简单的任务,快速排序将更快。
话虽如此,所有上述说法都是正确的:-Quicksort可以是N ^ 2,但是Sedgewick声称,良好的随机化实现比雷击N ^ 2的计算机更有可能被闪电击中,-Mergesort需要额外的空间
当我尝试两种排序算法时,通过计算递归调用的数量,与sortsort相比,quicksort始终具有较少的递归调用。这是因为quicksort有枢轴,并且枢轴不包含在下一个递归调用中。这样一来,快速排序可以比合并排序更快地到达递归基础案例。
这是访谈中常见的问题,尽管合并排序在最坏的情况下表现更好,但快速排序被认为比合并排序更好,尤其是对于大量输入。由于某些原因,哪种快速排序更好:
1-辅助空间:快速排序是一种就地排序算法。就地排序意味着无需额外的存储空间即可执行排序。另一方面,合并排序需要一个临时数组来合并排序后的数组,因此它不是就地。
2-最坏的情况:O(n^2)
使用随机快速排序可以避免快速排序的最坏情况。选择正确的枢轴可以很容易地避免这种情况。通过选择正确的枢轴元素来获得平均情况下的行为,使其即兴发挥性能并变得像合并排序一样高效。
3-引用位置: Quicksort特别具有良好的缓存位置,这使其在许多情况下(例如在虚拟内存环境中)比合并排序要快。
4-尾递归: QuickSort是尾递归,而合并排序则不是。尾递归函数是其中递归调用是该函数最后执行的函数。尾部递归函数比非尾部递归函数更好,因为可以通过编译器优化尾部递归。
虽然它们都在同一复杂度类中,但这并不意味着它们都具有相同的运行时。Quicksort通常比mergesort快,这是因为编写紧凑的实现更容易,而且它执行的操作可以更快。这是因为人们通常使用Quicksort而不是mergesort来更快。
然而!我个人经常会使用mergesort或quicksort变体,当quicksort表现不佳时,它会降级为mergesort。记得。Quicksort 平均仅为O(n log n)。最坏的情况是O(n ^ 2)!合并排序始终为O(n log n)。如果必须具有实时性能或响应能力,并且您的输入数据可能来自恶意源,则不应使用简单的快速排序。
在所有条件都相同的情况下,我希望大多数人使用最方便的方式,这往往是qsort(3)。除了快速排序以外,在数组上还非常快,就像mergesort是列表的常见选择一样。
我想知道的是为什么为什么很少见到基数或存储桶排序。它们是O(n),至少在链表上,并且所需要的只是某种将密钥转换为序数的方法。(字符串和浮点数很好用。)
我在想原因与计算机科学的教学有关。我什至不得不向我的算法分析讲师证明,确实有可能比O(n log(n))更快地排序。(他证明了你无法比较比O(n log(n))排序,这是对的。)
在其他新闻中,浮点数可以按整数排序,但是之后必须将负数转过来。
编辑:实际上,这是对floats-as-integers进行排序的一种更恶性的方法:http : //www.stereopsis.com/radix.html。请注意,无论您实际使用哪种排序算法,都可以使用位翻转技巧。
qsort
则是一种合并排序。
快速排序是一种就地排序算法,因此它更适合于数组。另一方面,合并排序需要O(N)的额外存储,并且更适合链接列表。
与数组不同,在喜欢的列表中,我们可以在O(1)空间和O(1)时间的中间插入项目,因此可以在没有任何额外空间的情况下实现合并排序中的合并操作。但是,为数组分配和取消分配额外的空间会对合并排序的运行时间产生不利影响。合并排序还有利于链接列表,因为顺序访问数据无需太多随机内存访问。
另一方面,快速排序需要大量的随机内存访问,使用数组,我们可以直接访问内存,而无需按照链表的要求进行任何遍历。当用于数组时,快速排序也具有很好的引用位置,因为数组连续存储在内存中。
即使两种排序算法的平均复杂度为O(NlogN),通常用于普通任务的人员都使用数组进行存储,因此,快速排序应成为首选算法。
编辑:我刚刚发现合并排序最坏/最佳/平均情况总是nlogn,但是快速排序可以从n2(元素已经排序时的最坏情况)到nlogn(当数据透视始终将数组分为两部分时的平均/最好情况)一半)。
在c / c ++领域中,当不使用stl容器时,我倾向于使用quicksort,因为它是运行时内置的,而mergesort不是。
因此,我认为,在许多情况下,这只是阻力最小的途径。
此外,对于整个数据集不适合工作集的情况,使用快速排序可以提高性能。
qsort
是一种合并排序,除非元素的数量确实是巨大的或无法分配临时内存。cvs.savannah.gnu.org/viewvc/libc/stdlib/…–
原因之一是更具哲学性。Quicksort是自上而下的理念。有n个要排序的元素,就有n个!可能性。通过m和nm的两个分区互斥,可能性的数量下降了几个数量级。米!*(nm)!比n小几倍!单独。想象5!vs 3!* 2 !. 5!比2个分区2和3的可能性多10倍。并推断为100万阶乘与900K!* 100K!与之相对,因此不必担心在范围或分区内建立任何顺序,而只需在更广泛的分区内建立顺序,并减少分区内的可能性。如果分区本身不是互斥的,则在该范围内较早建立的任何顺序将在以后受到干扰。
诸如合并排序或堆排序之类的任何自下而上的方法都类似于工人或雇员的方法,在这种方法中,人们开始在微观层面进行比较。但是,一旦稍后在它们之间找到一个元素,就必然会丢失该顺序。这些方法非常稳定且非常可预测,但是需要做一些额外的工作。
快速排序就像管理方法,其中一开始不关心任何订单,而只是满足一个广泛的标准而无需考虑订单。然后将分区缩小,直到获得排序集。Quicksort的真正挑战在于,当您对排序元素一无所知时,在黑暗中找到一个分区或标准。这就是为什么我们需要花费一些精力来查找中值或以随机或任意“管理”方法选择1的原因。要找到理想的中位数可能需要花费大量精力,并再次导致愚蠢的自下而上方法。因此,Quicksort说,只是随机选择一个枢轴,希望它会出现在中间某处,或者做一些工作以找到3,5的中位数,或者找到更多的值以找到更好的中位数,但不打算做到完美&不要 不要在最初订购时浪费任何时间。如果您很幸运,或者当您没有中位数而只是抓住机会时有时降级到n ^ 2的话,这似乎很好。任何方式的数据都是随机的。对。因此,我更同意快速排序的自上而下的逻辑方法,事实证明,与之相比,任何细致而彻底的稳定自下而上的方法,像之前节省的枢轴选择和比较这样的机会似乎可以更好地工作。合并排序。但 较早保存的比较似乎比任何细致而彻底的稳定的自下而上的方法(如合并排序)效果更好。但 较早保存的比较似乎比任何细致而彻底的稳定的自下而上的方法(如合并排序)效果更好。但
qsort
,Python的list.sort
和Array.prototype.sort
Firefox的JavaScript中的所有内容都是合并的。(GNU STLsort
改用Introsort,但这可能是因为在C ++中,交换可能胜过复制。)