快速排序分区:Hoare与Lomuto


82

Cormen中提到了两种快速排序分区方法:

Hoare-Partition(A, p, r)
x = A[p]
i = p - 1
j = r + 1
while true
    repeat
        j = j - 1
    until A[j] <= x
    repeat
        i = i + 1
    until A[i] >= x
    if i < j
        swap( A[i], A[j] )
    else
        return j

和:

Lomuto-Partition(A, p, r)
x = A[r]
i = p - 1
for j = p to r - 1
    if A[j] <= x
        i = i + 1
        swap( A[i], A[j] )
swap( A[i +1], A[r] )
return i + 1

不管选择枢轴的方法如何,在哪种情况下都比另一种更好?我知道,例如,当重复值的百分比很高时(即数组表示相同值的2/3以上),Lomuto的预成型效果相对较差,因为Hoare在这种情况下的表现还不错。

还有哪些其他特殊情况使一种分区方法比另一种更好?


2
我想不出任何在Lomuto比霍尔更好的局面。似乎Lomuto随时执行额外的交换A[i+1] <= x。在排序数组中(并在合理选择的枢轴下),Hoare几乎不进行交换,而Lomuto进行一吨处理(一旦j变小,则所有A[j] <= x。),我会缺少什么?
Wandering Logic

2
@WanderingLogic我不确定,但是似乎Cormen决定在他的书中使用Lomuto分区可能是教学上的-似乎有一个相当简单的循环不变式。
罗伯特·巴恩斯

2
请注意,这两种算法的功能不同。在Hoare算法的最后,关键点不在最后。您可以swap(A[p], A[j])在Hoare的末尾添加a ,以使两者的行为相同。
的Aurelien奥姆斯

您还应该检查Hoare分区i < j的2个重复循环。
的Aurelien奥姆斯

@AurélienOoms代码直接从书中复制。
罗伯特·S·巴恩斯

Answers:


92

教学维度

由于其简单性,Lomuto的分区方法可能更易于实现。乔恩·本特利(Jon Bentley)的《编程珍珠》中有一个很好的轶事:

“ Quicksort的大多数讨论都使用基于两个接近索引[即Hoare的]的分区方案。尽管该方案的基本思想很简单,但我总是发现细节很棘手-我曾经花了两天的大部分时间来寻找隐藏在短分区循环中的错误。初稿的读者抱怨说,标准的两索引方法实际上比Lomuto的方法更简单,并草绘了一些代码来阐明他的观点。我发现两个错误后就停止了寻找。”

性能维度

对于实际使用,为了效率可能会牺牲易于实现的程度。从理论上讲,我们可以确定元素比较的数量并交换以比较性能。此外,实际的运行时间将受到其他因素的影响,例如缓存性能和分支预测错误。

如下所示,除交换次数外,算法在随机排列上的行为非常相似。Lomuto需要的Hoare是Hoare的三倍

比较数

两种方法都可以使用比较来实现,以对长度为的数组进行分区。这本质上是最佳的,因为我们需要将每个元素与枢轴进行比较以决定放置位置。nn1n

掉期数

两种算法的交换次数都是随机的,具体取决于数组中的元素。如果我们假设随机排列,即所有元素都是不同的,并且元素的每个排列均具有相同的可能性,则可以分析预期的交换次数。

仅作为相对顺序计数,我们假设元素是数字。由于元素的等级与其值一致,因此下面的讨论更加容易。1,,n

洛穆托法

索引变量扫描整个数组,并且每当找到小于数据透视点的元素,便进行交换。在元素,恰好个元素小于,因此,如果枢轴是,我们将得到交换。A [ j ] x 1 n x 1 x x 1 xjA[j]x1,,nx1xx1x

然后,通过对所有支点取平均值来得出总体期望。每个值都同样有可能成为枢轴(即,概率为),因此我们有1{1,,n}1n

1nx=1n(x1)=n212.

平均使用Lomuto方法交换长度为的数组。n

霍尔法

在这里,分析有些棘手:即使固定枢轴,交换的数量仍然是随机的。x

更准确地说:索引和彼此相向直到交叉为止,这总是在发生(通过Hoare分区算法的正确性!)。这有效地将数组分为两部分:左边的部分被扫描,右边的部分被扫描。Ĵijxij

现在,正好对每 “错位”的元素进行交换,即当前位于左侧的一个大元素(大于,因此属于右侧分区)和一个位于右侧的小元素。请注意,这种配对形成总是可行的,即,最初在右侧的小元素数量等于在左侧的大元素数量。x

可以证明,这些对的数量是超几何 分布的:对于大元素,我们随机绘制它们在数组中的位置,并在中有位置。左部分。因此,假设枢轴为,则预期的对数为。Hyp(n1,nx,x1)nxx1(nx)(x1)/(n1)x

最后,我们再次对所有枢轴值取平均,以获得Hoare分区的总交换预期数量:

1nx=1n(nx)(x1)n1=n613.

(更详细的描述可以在我的硕士论文第29页中找到。)

内存访问模式

两种算法都使用两个指针进入数组,并按顺序对其进行扫描。因此,两者的行为几乎都是最佳的wrt缓存。

相等元素和已排序列表

正如Wandering Logic所述,对于非随机排列的列表,算法的性能差异更大。

在已经排序的数组上,Hoare的方法从不交换,因为不存在错位的对(参见上文),而Lomuto的方法仍然进行大约交换!n/2

相等元素的存在在Quicksort中需要特别注意。(我自己闯入了这个陷阱;有关“过早优化的故事”,请参阅我的硕士论文,第36页)。考虑一个充满 s 的数组作为极端示例。在这样的数组上,Hoare的方法为对元素执行一次交换-这是Hoare分区的最坏情况-但和始终在数组中间相遇。因此,我们具有最佳的分区,总运行时间保持在。i j On log n 0ijO(nlogn)

Lomuto的方法在全数组上表现得更加愚蠢:比较将始终为真,因此我们对每个元素进行一次交换!但更糟糕的是:循环之后,我们总是有,因此我们观察到最坏的情况下的分区,从而使整体性能降低到!= Ñ Θ Ñ 20A[j] <= xi=nΘ(n2)

结论

Lomuto的方法简单且易于实现,但不应用于实现库排序方法。


16
哇,这是一个详细的答案。做得很好!
拉斐尔

必须同意Raphael,真的是一个很好的答案!
罗伯特·S·巴恩斯

1
我要澄清一点,即随着唯一元素与总元素的比率降低,Lomuto所做的比较次数的增长明显快于Hoare的比较。这可能是由于Lomuto的分区不佳和Hoare的平均分区良好所致。
罗伯特·S·巴恩斯

两种方法的绝佳解释!谢谢!
v kouk

您可以轻松地创建Lomuto方法的变体,该方法可以提取等于枢轴的所有元素,并使它们脱离递归,尽管我不确定这是否会帮助或阻碍平均情况。
JakubNarębski15

5

一些评论增加了塞巴斯蒂安的优秀答案。

我将通常讨论分区重排算法,而不是它对Quicksort的特殊使用。

稳定性

Lomuto的算法是半稳定的:不满足谓词的元素的相对顺序得以保留。Hoare的算法不稳定。

元素访问模式

Lomuto的算法可以与单链表或类似的仅转发数据结构一起使用。Hoare的算法需要双向性

比较数

Lomuto算法可以执行谓词的应用来划分长度为的序列。(Hoare也是如此)。n1nn

但是为了做到这一点,我们必须牺牲两个属性:

  1. 要分区的序列不能为空。
  2. 该算法无法返回分区点。

如果我们需要这两个属性中的任何一个,我们别无选择,只能通过进行比较来实现该算法。n

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.