【问题标题】:When to use merge sort and when to use quick sort?何时使用归并排序,何时使用快速排序?
【发布时间】:2011-12-14 07:02:05
【问题描述】:

wikipedia article for merge sort

wikipedia article for quick sort

两篇文章都具有出色的可视化效果。

两者都有 n*log(n) 复杂度。

很明显,数据的分布会影响排序的速度。我的猜测是,由于比较可以快速比较任意两个值,因此无论它们的分布如何,数据值的范围都无关紧要。

更重要的是,应该考虑横向分布(x 方向)相对于排序(去除大小)。

如果测试数据有某种程度的排序......

【问题讨论】:

  • 我可以告诉你什么时候使用std::sort...总是:)
  • std:sort 实现了什么......哪个算法?
  • 克里斯,标准没有规定实现。但是,您的标准库可能会根据序列中元素的类型和数量使用这两种算法的组合。
  • @ChrisAaker:标准库的 GCC 实现使用 introsort,它是快速排序的一种变体,如果它感觉会回退到合并排序达到最坏情况的复杂性(O(N^2) 用于快速排序)
  • 用户一直在谈论快速排序失败或遇到最坏的情况..但是这是怎么发生的呢?一个简单的例子或测试用例?

标签: c++ sorting


【解决方案1】:

它通常取决于所涉及的数据结构。快速排序是 通常是最快的,但不能保证 O(n*log(n));有 退化的情况,它变成 O(n^2)。堆排序是常见的 选择;它保证 O(n*log(n)),无论初始顺序如何, 但它的常数因子要高得多。它通常在你使用 需要一个硬性的时间上限。一些更新的算法 使用快速排序,但尝试识别它何时开始退化, 然后切换到堆排序。合并排序用于数据 结构不支持随机访问,因为它适用于纯 顺序访问(前向迭代器,而不是随机访问 迭代器)。例如,它用于std::list<>::sort。这也是 广泛用于外部排序,其中随机访问可以非常非常 与顺序访问相比,成本更高。 (当对一个文件进行排序时 不适合内存,你可以把它分成适合的块 内存,使用快速排序对这些进行排序,将每个写入文件,然后 对生成的文件进行合并排序。)

【讨论】:

  • 令人着迷......这是一个采访任务......公司文件系统主要使用顺序访问(没有 - 仅重写附加)......我现在可以理解他们为什么会问这个问题以及它是如何关联的。
  • 你是说merge_sort是在数据结构不支持随机访问时使用的......你的意思是你必须迭代才能得到你想要的值?
  • @ChrisAaker 我的意思是你不能去数据集中的任意位置,至少不便宜。合并排序最初设计用于在磁带上工作。即使在磁盘上的文件上,与顺序读取相比,寻找文件中的随机位置也相对昂贵。当然,在标准库中,正向和双向迭代器不支持加法;你需要一个随机访问迭代器。 (而且std::list 有双向迭代器,所以std::sort 不能使用它。)
  • 这应该是公认的答案。我会考虑一些关于稳定性的考虑(快速排序通常不是有效的实现),但你发布了一个很好的概述。 +1
  • 如果您还需要对链表进行排序,合并排序也是一个不错的选择。
【解决方案2】:

在处理链表时,合并排序更快。这是因为在合并列表时可以轻松更改指针。它只需要通过列表一次 (O(n))。

Quicksort 的就地算法需要移动(交换)数据。虽然这对于内存数据集非常有效,但如果您的数据集不适合内存,它可能会更加昂贵。结果将是大量的 I/O。

如今,发生了很多并行化。并行化合并排序比快速排序(就地)更简单。如果不使用in-place算法,那么快速排序的空间复杂度为O(n),与归并排序相同。

因此,概括地说,快速排序可能对适合内存的数据集更有效。对于更大的东西,最好使用归并排序。

另一个使用合并排序而不是快速排序的一般情况是数据非常相似(即,不接近统一)。快速排序依赖于使用枢轴。在所有值都相似的情况下,快速排序会遇到 O(n^2) 的最坏情况。如果数据的值非常相似,则更有可能选择较差的枢轴,从而导致分区非常不平衡,从而导致 O(n^2) 运行时。最直接的例子是如果列表中的所有值都相同。

【讨论】:

    【解决方案3】:

    有一种现实世界的排序算法——称为Timsort——确实利用了野外遇到的数据通常是部分排序的想法。

    该算法源自归并排序和插入排序,用于CPython、Java 7和Android。

    有关详细信息,请参阅Wikipedia article

    【讨论】:

    • +1 用于 timsort。尽管与快速排序(n/2 vs. log n)相比,它在最坏的情况下确实使用了更多的内存。我不清楚为什么有人会使用合并排序,尽管 timsort 可用。
    • 我可能猜到 merge_sort 更适合预排序的数据。
    【解决方案4】:

    Java 6 及更早版本使用归并排序作为排序算法,而 C# 使用 QuickSort 作为排序算法。

    QuickSort 比归并排序执行得更好,即使它们都是 O(nlogn)。 QuickSort 的常数比归并排序小。

    【讨论】:

    • 常数更小...即在大 O 表示中删除的常数?
    • 除非在极少数情况下没有。合并排序和堆排序总是 O(nlog(n))。快速排序是 O(nlog(n)) 最好的情况,但 O(n^2) 最坏的情况。
    • 好吧,我理解数字上的区别..谢谢..但是是什么导致了这种最坏的情况......关于数据“传播”
    • 如果数据被排序,最坏的情况是逆序。这会导致 O(n**2) 运行时间。但通常不是。并且大多数情况下,除非它没有反向排序,否则快速排序比合并排序执行得更好。
    • 你的意思是“除非它是反向排序的”......基本上当数据以反向排序时......快速排序落在最坏的情况下大 O(n*n)。
    【解决方案5】:

    在这两者中,当您需要稳定排序时,请使用归并排序。您可以在不使用的情况下使用修改后的快速排序(例如 introsort),因为它往往更快并且占用的内存更少。

    Hoare 所描述的普通旧 Quicksort 对导致性能下降的特殊情况非常敏感,使其成为Theta(n^2),因此您通常确实需要修改版本。这就是数据分布的用武之地,因为合并排序没有坏情况。一旦你开始修改快速排序,你就可以继续进行各种不同的调整,而 introsort 是更有效的调整之一。它会即时检测它是否处于杀手级,如果是则切换到堆排序。

    事实上,Hoare 最基本的快速排序对于已经排序的数据最失败,所以你的“好的测试用例”带有某种程度的排序会在某种程度上杀死它。不过,这个事实只是出于好奇,因为只需要进行很小的调整即可避免这种情况,没有什么比一直进行自我介绍更复杂的了。因此,甚至费心分析被排序数据杀死的版本也很简单。

    在实践中,在 C++ 中,您通常会使用 std::stable_sortstd::sort,而不必过多担心确切的算法。

    【讨论】:

    • 你有什么杀性能特例的例子吗?
    • 我记得读过一些注意事项,如果用户可以提供专门制作的数据,快速排序将表现出最差的性能,基本上会导致 DOS 攻击。虽然我还没有看到任何情况下这将是一个真正的问题。 @ChrisAaker iirc 原始版本的快速排序使用第一个元素作为枢轴,因此创建起来非常简单。对于更复杂的变体(第一个、最后一个、中间),它几乎不可能偶然发生。
    • Voo 说的是,我认为有些论文构建了至少三个中值枢轴选择的杀手,甚至可能用于比这更好的枢轴选择。但是,如果您非常关心 DOS 攻击,那么随机枢轴选择会破坏所有精心设计的输入,并且对于任何大到足以让您关心自己是否处于错误状态的任何输入数据,出现错误情况的概率都可以忽略不计情况与否。从“你更有可能遭受宇宙射线比特翻转或地球自发爆炸”的意义上来说,这“可以忽略不计”。
    【解决方案6】:

    请记住,在实践中,除非您有一个非常大的数据集和/或多次执行排序,否则这可能根本不重要。话虽如此,快速排序通常被认为是“最快的” n*log(n) 排序器。看到这个问题已经问过:Quick Sort Vs Merge Sort

    【讨论】:

      猜你喜欢
      • 2015-05-26
      • 1970-01-01
      • 2020-04-22
      • 1970-01-01
      • 2021-11-28
      • 2019-11-16
      • 2011-01-13
      • 1970-01-01
      • 2020-08-14
      相关资源
      最近更新 更多