为什么实践中quicksort比其他排序算法更好?


308

在标准的算法当然我们被教导快速排序平均和ø Ñ 2在最坏的情况下。同时,还研究了其他排序算法,它们在最坏的情况下为O n log n (例如mergesortheapsort),在最坏的情况下甚至是线性时间(例如bubbleort),但还有一些额外的内存需求。O(nlogn)O(n2)O(nlogn)

快速浏览一下更多的运行时间后,自然可以说quicksort 应该不如其他高效。

另外,考虑到学生在基础编程课程中学习到,递归通常并不太好,因为它会占用过多的内存,等等。因此(尽管这不是一个真正的论点),但这样的想法是快速排序可能不是真的很好,因为它是一种递归算法。

那么,为什么在实践中快速排序优于其他排序算法?它与真实数据的结构有关吗?它与计算机中内存的工作方式有关吗?我知道有些记忆要比其他记忆快,但是我不知道这是否是这种违反直觉的表现的真正原因(与理论估计相比)。


更新1:一个规范的答案是说,平均情况的所涉及的常数小于其他O n log n 算法所涉及的常数。但是,我还没有看到用正确的计算代替仅凭直觉的想法的适当理由。O(nlogn)O(nlogn)

无论如何,正如某些答案所暗示的那样,似乎真正的区别发生在内存级别,在这种级别上,实现利用了计算机的内部结构,例如使用高速缓存比RAM快。讨论已经是有趣的,但我还是喜欢看关于内存管理更详细,因为它似乎是回答有什么关系。


更新2:有几个网页提供了排序算法的比较,其中有些比其他网页更出色(最著名的是sorting-algorithms.com)。除了提供不错的视觉辅助外,这种方法也无法回答我的问题。


2
合并排序在最坏的情况下为,并且可以在O n 时间内使用计数排序对整数数组进行排序,在整数大小的已知边界上进行排序。O(nlogn)O(n)
卡尔·默默特

13
sorting-algorithms.com对排序算法进行了相当全面的比较。
2012年

2
广告更新1:我猜想,你可以有严格的分析现实的假设。我都没有看过。例如,大多数形式分析仅计算比较。
拉斐尔

9
这个问题赢得了最近关于程序员的竞赛
拉斐尔

3
有趣的问题。不久前,我使用随机数据以及快速排序和合并排序的简单实现进行了一些测试。两种算法对于较小的数据集(最多100000个项目)都表现良好,但是合并后的结果却要好得多。这似乎与一般的假设相反,即快速排序非常好,而我仍然没有找到解释。我能想到的唯一想法是,通常将快速排序一词用于诸如介绍性排序之类的更复杂的算法,而使用随机数据透视的快速排序的天真的实现并不那么好。
Giorgio

Answers:


215

简短答案

缓存效率参数已经详细说明。此外,还有一个内在的论点,为什么Quicksort快速。如果喜欢用两个“交叉指针”,实现如在这里,内循环有一个非常小的身体。由于这是最常执行的代码,因此很值得。

长答案

首先,

平均情况不存在!

由于最佳和最坏情况在实践中很少会出现极端情况,因此需要进行平均情况分析。但是任何平均案例分析都假设输入有一定分布!对于排序,典型的选择是随机排列模型(在维基百科上是默认的)。

为什么使用符号?O

丢弃算法分析中的常量的主要原因之一是:如果我对准确的运行时间感兴趣,则需要所有相关基本操作的相对)成本(即使仍然忽略缓存问题,现代处理器中的流水线处理……)。数学分析可以计算每条指令执行的频率,但是单条指令的运行时间取决于处理器的详细信息,例如32位整数乘法是否需要与加法一样长的时间。

有两种解决方法:

  1. 修复某些机器模型。

    这是在唐· 克努斯(Don Knuth)的书系列“计算机编程的艺术”中完成的,该书是作者发明的人工“典型”计算机。在第3卷中,您可以找到许多排序算法的准确平均案例结果,例如

    • 快速排序:11.667(n+1)ln(n)1.74n18.74
    • 合并排序:12.5nln(n)
    • 堆排序: 16nln(n)+0.01n
    • Insertionsort: [ 来源 ]2.25n2+7.75n3ln(n) 几种排序算法的运行时

    这些结果表明Quicksort最快。但是,它仅在Knuth的人造机器上得到证明,对于x86 PC而言,它不一定意味着任何东西。另请注意,对于小量输入,算法的相关性有所不同:
    小型输入的几种排序算法的运行时
    [ ]

  2. 分析抽象的基本操作

    对于基于比较的排序,通常是交换键比较。在罗伯特·塞奇威克(Robert Sedgewick)的书中,例如“算法”,就是采用了这种方法。你在那里找到

    • 2nln(n)13nln(n)
    • 1.44nln(n)8.66nln(n)
    • 14n214n2

    如您所见,这并不容易将算法进行比较,以进行精确的运行时分析,但是结果与机器详细信息无关。

其他输入分布

如上所述,平均情况总是与某种输入分布有关,因此可以考虑随机排列以外的情况。例如,已经对具有相同元素的Quicksort进行了研究并且有一篇不错的文章介绍了Java中标准sort函数


8
通过插入机器相关的常量,可以将类型2的结果转换为类型1的结果。因此,我认为2.是一种更好的方法。
拉斐尔

2
@Raphael +1。我想您假设与机器有关的也是与实现有关的,对吗?我的意思是,快速的机器+糟糕的实施可能不是很有效。
Janoma'3

2
@Janoma我假设所分析的算法将以非常详细的形式给出(因为分析是详细的),并且实现应尽可能以字母表示。但是,是的,实施也会考虑在内。
拉斐尔

3
实际上,第2类分析在实践中是次等的。现实世界中的机器是如此复杂,以致于无法将类型2的结果转换为类型1。将其与类型1进行比较:绘制实验运行时间需要5分钟的工作。
朱尔斯

4
@Jules:“绘制实验运行时间” 不是类型1;这不是形式化的分析,也无法转移到其他计算机。毕竟,这就是我们进行形式分析的原因。
拉斐尔

78

关于这个问题可以提出很多观点。

快速排序通常很快

O(n2)

n1O(nlogn)

快速排序通常比大多数排序更快

O(nlogn)O(n2)n

O(nlogn)O(nBlog(nB))B

这种高速缓存效率的原因是它线性扫描输入并线性划分输入。这意味着我们在读取每个加载到缓存中的数字之前,可以充分利用我们所做的每个缓存加载,然后再将该缓存交换为另一个。特别是,该算法不考虑缓存,可以为每个缓存级别提供良好的缓存性能,这是另一个优势。

O(nBlogMB(nB))Mk

Quicksort通常比Mergesort快

这种比较完全是关于恒定因素的(如果我们考虑典型情况)。特别是,选择是在Quicksort枢轴的次优选择与Mergesort的整个输入的副本之间的选择(或避免这种复制所需的算法复杂性)之间。事实证明,前者效率更高:这背后没有理论依据,只是碰巧更快。

nO(logn)O(n)

最后,请注意,Quicksort对恰好按正确顺序输入的内容稍有敏感,在这种情况下,它可以跳过某些交换。Mergesort没有任何此类优化,与Mergesort相比,它还使Quicksort更快。

使用适合您需求的分类

结论:没有排序算法总是最佳的。选择适合您需求的任何一种。如果您需要一种在大多数情况下最快的算法,并且不介意在极少数情况下它可能会变得有点慢,并且不需要稳定的排序,请使用Quicksort。否则,请使用更适合您需求的算法。


3
您的最后一句话特别有价值。我的一位同事目前正在分析不同输入分配下的Quicksort实施。例如,其中一些会分解为许多重复项。
拉斐尔

4
O(n2)

8
“这背后没有理论依据,只是碰巧更快。” 从科学的角度来看,这种说法是非常不令人满意的。想象牛顿说:“蝴蝶飞起来,苹果掉下来:这背后没有理论依据,苹果正好掉下来。”
David Richerby,2014年

2
@Alex十Brink,“特别是该算法对缓存不敏感 ” 是什么意思?
Hibou57 2014年

4
@戴维·里希比(David Richerby),“从科学的角度来看,这一说法非常不令人满意”:他可能只是目睹一个事实,却不假装我们对此感到满意。一些算法家族缺乏完整的形式化;哈希函数就是一个例子。
Hibou57 2014年

45

在我大学的一本编程教程中,我们要求学生比较quicksort,mergesort,insert sort和Python内置的list.sort(称为Timsort)的性能。实验结果使我深感惊讶,因为内置list.sort的性能比其他排序算法要好得多,即使在实例很容易使quicksort,mergesort崩溃的情况下也是如此。因此,断定通常的快速排序实施是最佳实践为时尚早。但是我敢肯定,Quicksort或它的某些混合版本可以更好地实现。

这是David R. MacIver撰写的一篇不错的博客文章,解释了Timsort是自适应mergesort的一种形式。


17
@Raphael简而言之,Timsort是用于渐近的合并排序,是用于短输入的插入排序,还有一些启发式方法,可以有效地处理偶尔已经分类的突发数据(在实践中经常发生)。戴:除了算法之外,list.sort它还受益于由专业人员优化的内置功能。更公平的比较将使所有功能以相同的语言以相同的努力程度进行编写。
吉尔斯2012年

1
@Dai:您至少可以描述在哪种情况下(低RAM,是否实现了一种实现并行化,...)的输入(分别是它们的分布)。
拉斐尔

7
我们在随机数列表上进行了测试,并进行了部分排序,完全排序和反向排序。这是一年级的入门课程,因此并不是深入的实证研究。但是,现在它已正式用于Java SE 7和Android平台中的数组排序这一事实确实具有一定意义。
2012年

3
此处也对此进行了讨论:cstheory.stackexchange.com/a/927/74
Jukka Suomela 2012年

34

我认为与其他排序算法相比,QuickSort如此之快的主要原因之一是因为它对缓存友好。QS处理数组的某个片段时,它将访问该片段的开始和结尾处的元素,然后移向该片段的中心。

因此,当您开始时,您访问数组中的第一个元素,并将一块内存(“位置”)加载到缓存中。当您尝试访问第二个元素时,它很可能已经在缓存中,因此非常快。

其他算法(例如堆排序)不能像这样工作,它们会在数组中跳很多,这会使它们变慢。


5
这是一个有争议的解释:mergesort也对缓存友好。
Dmytro Korduban 2012年

2
我认为这个答案基本上是正确的,但这是youtube.com/watch?v=aMnn0Jq0J-E
rgrig

3
快速排序的平均情况下时间复杂度的乘法常数可能也更好(与您提到的缓存因子无关)。
卡夫

1
与快速排序的其他良好属性相比,您提到的要点并不那么重要。
MMS

1
@Kaveh:“快速排序的平均情况下时间复杂度的乘法常数也更好”,您对此有任何数据吗?
乔治

29

其他人已经说过,Quicksort 的渐近平均运行时间(以常数表示)比其他排序算法(在某些设置下)更好。

O(nlogn)

注意,Quicksort有许多变体(例如,参见Sedgewick的论文)。它们在不同的输入分布上(统一,几乎排序,几乎反向排序,许多重复...)表现不同,其他算法可能对某些算法更好。

k10


20

O(nlgn)

ps:确切地说,优于其他算法取决于任务。对于某些任务,最好使用其他排序算法。

也可以看看:


3
@Janoma这取决于您使用哪种语言和编译器。几乎所有功能语言(ML,Lisp,Haskell)都可以进行优化,以防止堆栈增长,而更智能的命令式编译器也可以做到这一点(GCC,G ++,我相信MSVC都可以做到这一点)。值得注意的例外是Java,它将永远不会进行此优化,因此在Java中将递归重写为迭代是有意义的。
拉菲·凯特勒

4
@JD,您不能将尾部调用优化与quicksort配合使用(至少不能完全使用),因为它会自我调用两次。您可以优化掉第二个电话,但不能优化第一个电话。
svick 2012年

1
@Janoma,您实际上不需要递归实现。例如,如果您查看C语言中qsort函数的实现,它不会使用递归调用,因此实现会变得更快。
卡夫

1
Heapsort也就位,为什么QS通常更快?
凯文

6
23240

16

Θ(n2)Θ(nlogn)

第二个原因是它可以执行in-place排序并在虚拟内存环境中很好地工作。

更新::(在Janoma和Svick的评论之后)

为了更好地说明这一点,让我举一个使用“合并排序”的示例(因为我认为“合并排序”是继快速排序之后的下一个被广泛采用的排序算法),并告诉您额外的常量来自何处(据我所知)以及为什么我认为快速排序更好):

考虑以下顺序:

12,30,21,8,6,9,1,7. The merge sort algorithm works as follows:

(a) 12,30,21,8    6,9,1,7  //divide stage
(b) 12,30   21,8   6,9   1,7   //divide stage
(c) 12   30   21   8   6   9   1   7   //Final divide stage
(d) 12,30   8,21   6,9   1,7   //Merge Stage
(e) 8,12,21,30   .....     // Analyze this stage

如果您仔细查看最后一个阶段是如何发生的,则将前12个与8相比较,然后将8个较小,因此优先。现在12又是21,接下来是12,依此类推。如果您进行最终合并,即4个元素与其他4个元素,则它将产生大量的EXTRA比较作为常量,这在Quick Sort中不会发生。这就是为什么首选快速排序的原因。


1
但是,什么使常数如此之小呢?
svick 2012年

1
@svick因为已排序in-place,所以不需要额外的内存。
2012年

Θ(nlgn)

15

我在处理现实世界数据时的经验是,快速排序是一个糟糕的选择。Quicksort适用于随机数据,但现实世界中的数据通常不是随机数据。

早在2008年,我就跟踪了一个由于使用quicksort导致的软件漏洞。过了一会儿,我写了插入排序,快速排序,堆排序和合并排序的简单方法,并对它们进行了测试。在处理大型数据集时,我的合并排序优于其他所有排序。

从那时起,合并排序就是我选择的排序算法。优雅 实施起来很简单。这是一个稳定的排序。它不会像quicksort那样退化为二次行为。我改用插入排序对小数组进行排序。

在很多情况下,我发现自己的想法是,给定的实现对快速排序的效果出奇地好,只是发现它实际上不是快速排序。有时,实现会在快速排序和其他算法之间切换,有时甚至根本不使用快速排序。例如,GLibc的qsort()函数实际上使用合并排序。仅当分配工作空间失败时,它才会退回到就地快速排序中,代码注释将其称为“较慢的算法”

编辑:诸如Java,Python和Perl之类的编程语言也使用合并排序,或更确切地说,使用派生类(例如Timsort或大集合的合并排序,小集合的插入排序)。(Java还使用双轴快速排序,这比普通快速排序要快。)


我看到了类似的东西,因为我们碰巧不断地追加/重新排序,以将其插入一批已经排序的数据中。您可以通过使用随机快速排序来平均解决此问题(并因稀有和随机的极其缓慢的排序而感到惊讶),或者可以忍受始终较慢的排序,而无需花费大量时间来完成。有时您也需要排序稳定性。Java已从使用合并排序转变为快速排序变体。
罗布(Rob)

@Rob这是不正确的。到目前为止,Java仍使用mergesort(Timsort)的变体。它也确实使用了快速排序的变体(双轴快速排序)。
Erwan Legrand 2015年

14

1-快速分类就位(不需要固定的数量,也不需要额外的内存。)

2-快速排序比其他有效的排序算法更易于实现。

3-与其他有效的排序算法相比,快速排序在运行时间上具有较小的恒定因子。

更新:对于合并排序,您需要执行一些“合并”,在合并之前需要额外的数组来存储数据。但是很快,您就不会。这就是为什么可以进行快速排序。对于合并还进行了一些额外的比较,这些比较增加了合并排序中的常量因子。


3
您是否看到过高级的就地迭代式Quicksort实现?他们有很多东西,但并非“容易”。
拉斐尔

2
2号并没有回答我的问题都和数字1和3需要适当的理由,在我看来。
Janoma

@Raphael:他们很容易。使用数组而不是指针就地实现快速排序要容易得多。并且就地进行迭代无需重复。
MMS

合并的数组还不错。将一件物品从源堆积物移动到目标堆积物后,就不再需要它了。如果使用动态数组,则合并时会存在恒定的内存开销。
Oskar Skog

@ 1 Mergesort也可以就位。@ 2什么定义了效率?我喜欢合并排序,因为我认为合并排序非常简单而且有效。@ 3与排序大量数据无关,并且需要有效地实现该算法。
Oskar Skog

11

在什么条件下特定的排序算法实际上是最快的算法?

Θ(log(n)2)Θ(nlog(n)2)

Θ(nk)Θ(nm)k=2#number_of_Possible_valuesm=#maximum_length_of_keys

3)基础数据结构是否由链接的元素组成?是的->始终使用就地合并排序。链接数据结构易于实现固定大小或自适应的(即自然的)自下而上的归并合并各种不同的域,因为它们从不需要在每个步骤中复制整个数据,并且也不需要递归,所以它们都是比任何其他基于比较的常规排序都快,甚至比快速排序快。

Θ(n)

5)底层数据的大小可以绑定到小到中等大小吗?例如n是否小于10,000 ... 100,000,000(取决于基础架构和数据结构)?是->使用bionic排序或Batcher奇偶归并排序。转到1)

Θ(n)Θ(n2)Θ(nlog(n)2)已知最坏的运行时间,或者尝试进行梳理排序。我不确定shell排序还是comb排序在实践中都不会表现得很好。

Θ(log(n))Θ(n)Θ(n)Θ(log(n))Θ(n2)Θ(n)Θ(n)Θ(log(n))Θ(nlog(n))

Θ(nlog(n))

quicksort的实现提示:

Θ(n)Θ(log(n))Θ(nlogk(k1))

2)存在快速排序的自下而上的迭代变体,但是AFAIK具有与自上而下相同的渐近空间和时间边界,并且其他方面也难以实现(例如,显式管理队列)。我的经验是,出于任何实际目的,这些都不值得考虑。

mergesort的实现提示:

1)自上而下的mergesort总是比自上而下的mergesort更快,因为它不需要递归调用。

2)可以通过使用双缓冲区并切换缓冲区来加快非常幼稚的mergesort的速度,而不是在每个步骤之后都将数据从时态数组中复制回来。

3)对于许多实际数据,自适应合并排序比固定大小的合并排序要快得多。

Θ(k)Θ(log(k))Θ(1)Θ(n)

从我写的内容来看,很明显,快速排序通常不是最快的算法,除非同时满足以下条件:

1)可能的值不止一个

2)基础数据结构未链接

3)我们不需要稳定的订单

4)数据足够大,以至于双音分类器或Batcher奇偶合并合并的轻微次优渐近运行时间开始

5)数据几乎没有排序,并且不包含更大的已排序部分

6)我们可以从多个位置同时访问数据序列

Θ(log(n))Θ(n)

ps:有人需要帮助我格式化文本。


(5):Apple的sort实现首先检查数组开头和结尾处的升序或降序运行。如果没有太多这样的元素,这将非常快,如果它们中的n / ln n个以上,则可以非常有效地处理这些元素。连接两个已排序的数组并对结果进行排序,您将得到合并
gnasher729

8

大多数排序方法都必须在短时间内移动数据(例如,合并排序在本地进行更改,然后合并这一小块数据,然后合并一个更大的数据..)。因此,如果数据离其目的地很远,则需要进行许多数据移动。

ab


5
您关于快速排序与合并排序的争论没有成功。Quicksort首先进行大动作,然后进行越来越小的动作(每步动作的大小大约一半)。合并排序从一小步开始,然后进行越来越大的步伐(每一步大约两倍)。这并不意味着一个比另一个更有效。
吉尔斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.