快速排序与堆排序


Answers:


60

本文进行了一些分析。

另外,来自维基百科:

快速排序最直接的竞争对手是堆排序。堆排序通常比快速排序慢一些,但最坏的运行时间始终是Θ(nlogn)。Quicksort通常更快,但是除了introsort变体之外,还有可能出现最坏情况的性能,当introsort变体在检测到不良情况时切换到堆排序。如果事先知道有必要使用堆排序,那么直接使用它比等待内向排序切换到它要快。


12
需要注意的是,在典型的实现中,快速排序和堆排序都不是稳定的排序。
MjrKusanagi 2014年

@DVK,根据您的链接cs.auckland.ac.nz/~jmor159/PLDS210/qsort3.html,堆排序在n = 100时需要2,842个比较,但在n = 500时需要53,113个比较。这意味着n = 500和n = 100之间的比率是18倍,并且与O(N logN)复杂度的堆排序算法不匹配。我猜很可能他们的堆排序实现内部存在某种错误。
杜加恩

@DUJiaen-请记住,O()与大N时的渐近行为有关,并且可能有乘数
DVK

这与乘数无关。如果算法的复杂度为O(N log N),则应该遵循时间(N)= C1 * N * log(N)的趋势。如果您使用Time(500)/ Time(100),很明显C1将消失,结果应接近(500 log500)/(100 log100)= 6.7,但是从您的链接来看,它是18,即规模过大。
杜加恩

2
这个链接是死的
PlsWork

123

Heapsort是O(N log N)保证的,这比Quicksort中最差的情况要好得多。Heapsort不需要更多的内存来放置另一个数组,就可以像Mergesort那样放置有序数据。那么,为什么商业应用程序坚持使用Quicksort?与其他实现相比,Quicksort有什么特别之处?

我自己测试了算法,并且发现Quicksort确实有一些特别之处。它运行速度快,比堆和合并算法快得多。

Quicksort的秘密是:它几乎不执行不必要的元素交换。交换非常耗时。

使用Heapsort,即使您已对所有数据进行了排序,您也将交换100%的元素来对数组进行排序。

使用Mergesort,情况甚至更糟。您将要在另一个数组中写入100%的元素,然后将其写回到原始数组中,即使已经订购了数据。

使用Quicksort,您无需交换已订购的产品。如果您的数据已完全订购,则几乎不交换任何信息!尽管对于最坏的情况有很多麻烦,但对支点的选择稍作改进,除获得数组的第一个或最后一个元素外,都可以避免。如果从中间元素在第一个,最后一个和中间元素之间获取轴心,则足以避免发生最坏情况。

Quicksort的优势不是最坏的情况,而是最好的情况!在最佳情况下,您可以进行相同数量的比较,好的,但是您几乎不交换任何内容。通常,您交换部分元素,但不是全部元素,如Heapsort和Mergesort。这就是给Quicksort最好的时间。更少的交换,更快的速度。

下面的C#在我的计算机上以释放模式运行的实现在运行中比Array.Sort在中间枢轴下要慢3秒,在改进枢轴下要慢2秒(是的,要获得一个好的枢轴会产生开销)。

static void Main(string[] args)
{
    int[] arrToSort = new int[100000000];
    var r = new Random();
    for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);

    Console.WriteLine("Press q to quick sort, s to Array.Sort");
    while (true)
    {
        var k = Console.ReadKey(true);
        if (k.KeyChar == 'q')
        {
            // quick sort
            Console.WriteLine("Beg quick sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
            QuickSort(arrToSort, 0, arrToSort.Length - 1);
            Console.WriteLine("End quick sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
            for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);
        }
        else if (k.KeyChar == 's')
        {
            Console.WriteLine("Beg Array.Sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
            Array.Sort(arrToSort);
            Console.WriteLine("End Array.Sort at " + DateTime.Now.ToString("HH:mm:ss.ffffff"));
            for (int i = 0; i < arrToSort.Length; i++) arrToSort[i] = r.Next(1, arrToSort.Length);
        }
    }
}

static public void QuickSort(int[] arr, int left, int right)
{
    int begin = left
        , end = right
        , pivot
        // get middle element pivot
        //= arr[(left + right) / 2]
        ;

    //improved pivot
    int middle = (left + right) / 2;
    int
        LM = arr[left].CompareTo(arr[middle])
        , MR = arr[middle].CompareTo(arr[right])
        , LR = arr[left].CompareTo(arr[right])
        ;
    if (-1 * LM == LR)
        pivot = arr[left];
    else
        if (MR == -1 * LR)
            pivot = arr[right];
        else
            pivot = arr[middle];
    do
    {
        while (arr[left] < pivot) left++;
        while (arr[right] > pivot) right--;

        if(left <= right)
        {
            int temp = arr[right];
            arr[right] = arr[left];
            arr[left] = temp;

            left++;
            right--;
        }
    } while (left <= right);

    if (left < end) QuickSort(arr, left, end);
    if (begin < right) QuickSort(arr, begin, right);
}

10
+1的考虑因素。排序算法所需的交换,读/写操作的数量
ycy 2015年

2
对于任何确定性,恒定时间的数据透视选择策略,您都可以找到一个产生O(n ^ 2)最坏情况的数组。仅消除最小值是不够的。您必须可靠地选择某个特定范围内的枢轴。

1
我很好奇这是否是您在手动编码的快速排序和C#内置Array.sort之间为模拟运行的确切代码?我测试了此代码,并且在所有测试中,最好手动编码的快速排序与Array.sort相同。我在测试中控制的一件事是制作随机数组的两个相同副本。毕竟,给定的随机化可能比另一个随机化更有利(倾向于最佳情况)。因此,我对每组进行相同的设置。Array.sort每次被捆绑或击败(释放构建btw)。
克里斯(Chris)

1
合并排序不必复制100%的元素,除非这是教科书中非常幼稚的实现。实现起来很简单,因此您只需要复制其中的50%(两个合并数组的左侧)。推迟复制直到您实际上必须“交换”两个元素也很简单,因此使用已排序的数据,您将不会有任何内存开销。因此,即使是50%实际上也是最坏的情况,您可以在0%和0%之间设置任何值。
ddekany

1
@MarquinhoPeli我的意思是说,与排序列表的大小相比,您只需要50%的可用内存,而不是100%,这似乎是一种常见的误解。所以我在谈论峰值内存使用情况。我无法提供链接,但是很容易看出您是否尝试将两个已经排序的数组的一半合并到位(只有左边的一半会覆盖您尚未使用的元素的问题)。在整个排序过程中需要执行多少内存复制是另一个问题,但是很显然,对于任何排序算法,最坏的情况都不能低于100%。
ddekany

15

在大多数情况下,快一点与快一点无关紧要……您根本不希望它偶尔变得缓慢。尽管您可以调整QuickSort以避免缓慢的情况,但您会失去基本QuickSort的优雅。因此,对于大多数事情,我实际上更喜欢HeapSort ...您可以完全简单的方式实现它,而永远不会变慢。

在大多数情况下,如果您确实想要最大速度,则QuickSort可能比HeapSort更为可取,但都不是正确的答案。对于紧急情况,值得仔细检查情况的细节。例如,在我的一些对速度有严格要求的代码中,数据已经被排序或接近排序是很常见的(它为多个相关字段建立索引,这些字段通常一起上下移动或彼此上下移动,因此,一旦您按一个排序,其他的就会被排序,反向排序或接近……其中任何一个都可以杀死QuickSort)。在那种情况下,我都没有实现...而是实现了Dijkstra的SmoothSort ... HeapSort变体,在已经排序或接近排序时为O(N)...它不是那么优雅,不太容易理解,但是很快...阅读如果您想要编写更具挑战性的代码,请访问http://www.cs.utexas.edu/users/EWD/ewd07xx/EWD796a.PDF


6

Quicksort-Heapsort就地混合也非常有趣,因为它们大多数在最坏的情况下只需要n * log n比较(就渐近的第一项而言,它们是最佳的,因此它们避免了最坏的情况O(log n)的额外空间,它们相对于已排序的数据集至少保留了Quicksort良好行为的“一半”。Dikert和Weiss在http://arxiv.org/pdf/1209.4214v1.pdf中提出了一种非常有趣的算法:

  • 选择一个枢轴p作为sqrt(n)元素的随机样本的中位数(这可以通过Tarjan&co的算法在最多24个sqrt(n)的比较中完成,或者通过复杂得多的蜘蛛程序在5个sqrt(n)的比较中完成) Schonhage的工厂算法);
  • 与Quicksort的第一步一样,将阵列分为两部分;
  • 堆放最小的部分,并使用O(log n)额外的位对堆进行编码,在堆中,每个左子节点的值都大于其同级对象;
  • 递归地提取堆的根,向下筛选由根留下的凹腔,直到到达堆的叶子,然后用从数组其他部分获取的适当元素填充该腔;
  • 在数组的其余无序部分上递归(如果选择p作为精确的中位数,则根本没有递归)。

2

比较 之间的quick sortmerge sort因为两者都是就地排序的类型,因此,对于快速排序而言,wrost案例运行时间与wrost案例运行时间之间是有区别的;O(n^2)对于堆排序而言,它仍然是这样O(n*log(n));对于平均数量的数据而言,快速排序更为有用。由于它是随机算法,因此获得正确ans的可能性。更少的时间取决于您选择的枢轴元素的位置。

所以

好电话: L和G的大小均小于3s / 4

错误通话: L和G中的一个大小大于3s / 4

对于少量,我们可以进行插入排序,对于大量数据,可以进行堆排序。


尽管可以通过就地排序实现合并排序,但是实现起来很复杂。AFAIK,大多数合并排序实现不是就地实现的,但是它们是稳定的。
MjrKusanagi 2014年

2

堆排序的好处是运行情况最糟的情况是O(n * log(n)),因此,在快速排序执行效果可能很差的情况下(通常是大多数排序的数据集),堆排序是更可取的。


4
如果选择了不正确的数据透视选择方法,则Quicksort只能对大多数排序的数据集执行不佳的操作。即,错误的枢轴选择方法将总是选择第一个或最后一个元素作为枢轴。如果每次都选择一个随机枢轴,并且使用了一种处理重复元素的好方法,那么最坏情况下的快速排序的机会就很小。
贾斯汀·皮尔

1
@Justin-的确是这样,我是在谈论一个幼稚的实现。
zellio 2010年

1
@贾斯汀:的确如此,但是无论经济增长多么缓慢,总有机会放缓。对于某些应用程序,我可能要确保O(n log n)行为,即使它的速度较慢。
David Thornley,2010年

2

好吧,如果您进入架构级别...我们在高速缓存中使用队列数据结构,因此队列中可用的内容都将被排序。作为快速排序,我们将数组划分为任何长度都没有问题...但是在堆中排序(通过使用数组)可能会导致父级可能不存在于缓存中可用的子数组中,然后必须将其带入缓存中……这很费时间。这是最好的快速排序!!😀


1

堆排序构建一个堆,然后重复提取最大项。最坏的情况是O(n log n)。

但是,如果您看到快速排序的最坏情况是O(n2),您将意识到快速排序对于大数据而言不是一个很好的选择。

因此,这使排序是一件有趣的事情;我相信今天有这么多排序算法存在的原因是因为它们在最佳位置都是“最佳”的。例如,如果对数据进行排序,气泡排序可以执行快速排序。或者,如果我们对要排序的项目有所了解,那么我们可能会做得更好。

可能不会直接回答您的问题,以为我要加两分钱。


1
切勿使用冒泡排序。如果您合理地认为您的数据将被排序,则可以使用插入排序,甚至可以测试数据以查看它们是否已排序。不要使用Bubblesort。
vy32 2014年

如果您有非常大的RANDOM数据集,那么最好的选择是quicksort。如果是部分订购的,则不是这样,但是,如果您开始使用庞大的数据集,则至少应该对它们有足够的了解。
Kobor42 2014年

1

当处理非常大的输入时,堆排序是一个安全的选择。渐近分析显示,最坏情况下Heapsort的增长顺序为Big-O(n logn),比Big-O(n^2)最坏情况下的Quicksort更好。但是,堆排序在大多数机器上在实践中都比快速实现的排序要慢一些。堆排序也不是稳定的排序算法。

实际上,堆排序比速排序更慢的原因是由于速排序中的引用元素(“ https://en.wikipedia.org/wiki/Locality_of_reference ”)的位置更好,数据元素位于相对较近的存储位置内。表现出强烈的参考性的系统是性能优化的理想选择。但是,堆排序具有更大的飞跃。这使得快速排序更适合较小的输入。


2
快速排序也不稳定。

1

对我来说,堆排序和快速排序之间有一个非常根本的区别:快速排序使用递归。在递归算法中,堆随着递归次数的增长而增长。如果n小,这无关紧要,但是现在我正在对两个矩阵进行排序,其中n = 10 ^ 9 !!。该程序占用了将近10 GB的内存,任何多余的内存都将使我的计算机开始交换到虚拟磁盘内存。我的磁盘是RAM磁盘,但是仍然交换到它会大大改变速度。因此,在用C ++编码的statpack中,它包括可调整的维数矩阵,程序员事先不知道大小,以及非参数统计排序,我更喜欢使用heapsort以避免对非常大的数据矩阵使用的延迟。


1
您平均只需要O(logn)内存。假设您对转轴不走运,那么递归开销是微不足道的,在这种情况下,您有更大的问题要担心。

-1

要回答原始问题并在此处解决其他一些评论:

我只是比较了选择,快速,合并和堆排序的实现,以了解它们如何相互叠加。答案是它们都有缺点。

TL; DR:Quick是最好的通用排序(合理地快速,稳定并且大部分就位),但我个人更喜欢堆排序,除非需要稳定排序。

选择-N ^ 2-实际上只有少于20个左右的元素才有用,然后表现不佳。除非您的数据已经排序,或者非常非常接近。N ^ 2变慢,变快。

根据我的经验,速度并不是一直都那么快。使用快速排序作为常规排序的好处是它相当快且稳定。它也是一个就地算法,但是由于通常是递归实现的,因此会占用额外的堆栈空间。它也落在O(n log n)和O(n ^ 2)之间。某种程度上的计时似乎可以证实这一点,尤其是当值落在狭窄范围内时。它比对10,000,000个项目的选择排序更快,但比合并或堆慢。

合并排序是有保证的O(n log n),因为其排序与数据无关。不管您赋予它什么值,它都会做它所做的事情。它也很稳定,但是如果您对实现不小心的话,很大的种类会炸毁您的堆栈。有一些复杂的就地合并排序实现,但是通常您需要在每个级别中使用另一个数组将值合并到其中。如果这些数组存在于堆栈中,则可能会遇到问题。

堆排序最大为O(n log n),但在许多情况下,速度更快,这取决于将值沿log n深堆向上移动的距离。堆可以很容易地在原始数组中就地实现,因此它不需要额外的内存,而且是迭代的,因此不必担心递归时的堆栈溢出。堆排序的一个巨大缺点是它不是一个稳定的排序,这意味着如果需要它是正确的。


快速排序不是稳定的排序。除此之外,这种性质的问题还鼓励基于意见的回应,并可能导致编辑战争和争论。SO指南明确建议不要提出基于意见的回答。回答者即使有丰富的经验和智慧,也应避免诱惑回答他们。标记他们是否要关闭或等待有足够声誉的人标记并关闭他们。此评论不是对您的知识或答案有效性的反映。
MikeC '16
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.