快速排序和合并顺序数据在内存中适合的性能与访问磁盘上的顺序数据的速度慢

2022-09-04 03:29:42

以下引文来自维基百科合并排序页面的“与其他排序算法的比较”部分

在典型的现代体系结构上,高效的快速排序实现通常优于合并排序,以便对基于 RAM 的阵列进行排序。[需要引用]另一方面,合并排序是一种稳定的排序,在处理访问速度慢的顺序媒体时效率更高。

我的问题:

  1. 为什么当要排序的数据都可以放入内存时,Quicksort 的性能优于 Mergesort?如果所需的所有数据都已缓存或在内存中,那么快速排序和合并排序的访问速度不是很快吗?

  2. 为什么 Mergesort 在处理访问速度慢的顺序数据(例如,在要排序的数据不能全部放入内存的情况下从磁盘)方面更有效率?

  3. (从我下面的评论移到这里)在 n 个元素的基元数组(数据是连续的)中。必须在 MergeSort 中读取和比较的元素对是 和(在最终合并中发生)。现在认为必须在 QuickSort 中读取和比较的一对元素是 和 (发生在第一个分区中,假设我们将随机选择的透视与第一个元素交换)。我们知道数据是以块的形式读取并加载到缓存中,或者磁盘到内存(如果我错了,请纠正我),那么在使用MergeSort时,所需的数据是否更有可能加载到一个块中?在我看来,MergeSort总是占上风,因为它可能会比较更紧密的元素。我知道这是假的(见下图),因为QuickSort显然更快......我知道MergeSort没有到位,需要额外的内存,这可能会减慢速度。除此之外,我在分析中遗漏了哪些部分?arrarr[0]arr[n/2]arr[1]arr[n]

enter image description here

图像来自普林斯顿CS MergeSort和QuickSort幻灯片


我的动机:

我想了解上述这些概念,因为它们是为什么mergeSort在对LinkedList进行排序时是首选的主要原因之一,或者在对数组或顺序数据进行排序时没有顺序数据和quickSort是首选的主要原因之一。为什么mergeSort用于对Java中的对象进行排序,而quickSort用于对Java中的基元类型进行排序。

更新:Java 7 API实际上使用TimSort对Object进行排序,Object是MergeSort和InjectSort的混合体。对于基元,双透视快速排序。这些更改是从 Java SE 7 开始实现的。这与排序算法的稳定性有关。为什么Java的Arrays.sort方法对不同类型的使用两种不同的排序算法?


编辑:

我将不胜感激解决以下方面的回答:

  • 我知道这两种排序算法在移动次数、读取次数和比较次数上有所不同。如果这些是导致我在问题中列出的行为的原因(我怀疑它),那么对排序算法的步骤和过程如何导致它具有从磁盘或内存中寻求数据的优点或缺点的彻底解释将不胜感激。
  • 欢迎举例说明。我通过例子学得更好。

注意:如果您正在阅读@rcgldr的答案。看看我们在聊天室的对话,它有很多很好的解释和细节。https://chat.stackoverflow.com/rooms/161554/discussion-between-rcgldr-and-oliver-koo


答案 1

主要区别在于合并排序执行更多的移动,但比快速排序的比较更少。即使在对本机类型数组进行排序的情况下,快速排序也只快15%左右,至少当我在伪随机64位无符号整数的大型数组上对其进行测试时,这应该是快速排序的最佳情况,在我的系统上(英特尔3770K 3.5ghz,Windows 7 Pro 64位,Visual Studio 2015,排序1600万伪随机64位无符号整数, 快速排序为 1.32 秒,合并排序为 1.55 秒,1.32/1.55 ~= 0.85,因此快速排序比合并排序快 15%左右)。我的测试是使用快速排序,没有检查来避免最坏情况O(n^2)时间或O(n)空间。由于将检查添加到快速排序以减少或防止最坏情况行为(例如,如果递归变得太深,则回退到堆排序),速度优势降至小于10%(这是我在VS2015的std::sort(修改的快速排序)实现与std::stable_sort(修改合并排序)之间的区别)。

如果对“字符串”进行排序,则更有可能是要排序的是指向这些字符串的指针(或引用)数组。这是合并排序速度更快的地方,因为移动涉及指针,而比较涉及字符串的间接寻址和比较级别。

选择快速排序而不是合并排序的主要原因不是速度,而是空间要求。合并排序通常使用与原始数组大小相同的第二个数组。快速排序和自上而下的合并排序还需要 log(n) 堆栈帧进行递归,并且对于快速排序,将堆栈空间限制为 log(n) 堆栈帧是通过仅在较小的分区上递归并循环回来处理较大的分区来完成的。

在缓存问题方面,最新的处理器具有 4 路或 8 路关联缓存。对于合并排序,在合并期间,两个输入运行将最终位于 2 个缓存行中,一个输出运行在第 3 个缓存行中。快速排序在进行交换之前扫描数据,因此扫描的数据将在缓存中,尽管如果被比较/交换的两个元素彼此相距足够远,则以单独的行形式存在。


对于外部排序,使用自下而上合并排序的某些变体。这是因为合并排序合并操作是顺序的(在启动一对新运行时发生唯一的随机访问),这在硬盘驱动器的情况下或传统情况下的磁带驱动器(至少需要 3 个磁带驱动器)中速度很快。每次读取或写入可以用于非常大的数据块,从而减少硬盘驱动器中每个元素的平均访问时间,因为每个I / O一次读取或写入大量元素。

还应该注意的是,库中的大多数合并排序也是自下而上的合并排序的一些变体。自上而下的合并排序主要是教学环境实现。


如果在具有 16 个寄存器的处理器上对本机类型数组进行排序,例如 64 位模式下的 X86,其中 8 个寄存器用作 4 次运行的开始 + 结束指针(或引用),则 4 路合并排序通常与快速排序大致相同或快一点,前提是编译器优化了基于寄存器的指针或引用。这是一个类似的权衡,就像快速排序一样,4向合并排序做更多的比较(1.5 x比较),但移动(0.5 x移动)比传统的双向合并排序少。


应该注意的是,这些排序是 CPU 绑定的,而不是内存绑定的。我做了一个自下而上的合并排序的多线程版本,在使用4个线程的情况下,排序速度快了3倍。链接到使用 4 个线程的 Windows 示例代码:

https://codereview.stackexchange.com/questions/148025/multithreaded-bottom-up-merge-sort


答案 2