快速排序和霍尔分区
Posted
技术标签:
【中文标题】快速排序和霍尔分区【英文标题】:QuickSort and Hoare Partition 【发布时间】:2011-11-04 02:47:05 【问题描述】:我很难将带有 Hoare 分区的 QuickSort 转换为 C 代码,但不知道原因。我正在使用的代码如下所示:
void QuickSort(int a[],int start,int end)
int q=HoarePartition(a,start,end);
if (end<=start) return;
QuickSort(a,q+1,end);
QuickSort(a,start,q);
int HoarePartition (int a[],int p, int r)
int x=a[p],i=p-1,j=r;
while (1)
do j--; while (a[j] > x);
do i++; while (a[i] < x);
if (i < j)
swap(&a[i],&a[j]);
else
return j;
另外,我真的不明白为什么HoarePartition
有效。有人可以解释它为什么有效,或者至少将我链接到一篇有效的文章吗?
我已经看到了分区算法的逐步完成,但我对它没有直观的感觉。在我的代码中,它甚至似乎都不起作用。例如,给定数组
13 19 9 5 12 8 7 4 11 2 6 21
它将使用枢轴 13,但以数组结尾
6 2 9 5 12 8 7 4 11 19 13 21
并将返回j
,即a[j] = 11
。我认为从该点开始并继续前进的数组应该具有都大于枢轴的值,但这里不是这样,因为 11
这里是 Hoare 分区的伪代码(来自 CLRS,第二版),如果有用的话:
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
exchange A[i] ↔ A[j]
else return j
谢谢!
编辑:
解决这个问题的正确 C 代码最终会是:
void QuickSort(int a[],int start,int end)
int q;
if (end-start<2) return;
q=HoarePartition(a,start,end);
QuickSort(a,start,q);
QuickSort(a,q,end);
int HoarePartition (int a[],int p, int r)
int x=a[p],i=p-1,j=r;
while (1)
do j--; while (a[j] > x);
do i++; while (a[i] < x);
if (i < j)
swap(&a[i],&a[j]);
else
return j+1;
【问题讨论】:
是的,我有,已编辑原始答案 我认为您编辑了组织问题。 检查您的数据样本,您从 12 到 11 个元素(缺少 13 个)。那不可能。 即根据链接umiacs.umd.edu/~joseph/classes/enee641/assign5-solution.pdf 我不明白为什么它不适用于两个元素? if (end-start 【参考方案1】:我认为这段代码有两个问题。对于初学者,在您的快速排序功能中,我认为您想重新排序行
int q=HoarePartition(a,start,end);
if (end<=start) return;
这样你就有了它们:
if (end<=start) return;
int q=HoarePartition(a,start,end);
但是,您应该做的还不止这些;特别是这应该是
if (end - start < 2) return;
int q=HoarePartition(a,start,end);
原因是如果您尝试分区的范围大小为零或一,Hoare 分区将无法正常工作。在我的 CLRS 版本中,任何地方都没有提到这一点。我必须去 the book's errata page 才能找到这个。这几乎肯定是您遇到“访问超出范围”错误问题的原因,因为如果不变量被破坏,您可能会直接跑出数组!
至于对 Hoare 分区的分析,我建议从手动跟踪它开始。还有更详细的分析here。直观地说,它的工作原理是从范围的末端向彼此增加两个范围——一个在左侧包含小于枢轴的元素,一个在右侧包含大于枢轴的元素。这可以稍微修改以生成 Bentley-McIlroy 分区算法(在链接中引用),该算法可以很好地扩展以处理相等的键。
希望这会有所帮助!
【讨论】:
首先非常感谢,这确实有帮助,其次我仍然得到一个错误:运行时检查失败 #2 - 变量“a”周围的堆栈已损坏。j=r+1
上没有。从调用模式可以看出,这使用了真正的 C [inclusiveStart, ExclusiveEnd)
约定。
这解释了运行时错误,发生了一些奇怪的事情——只有 j 它停止排序一步 b4 完成,而 j+1 它对数组进行排序但超出范围......怎么了?
@Henk Holterman- 你确定吗?我在我的机器上对此进行了测试,看起来他肯定在使用包容性端点;如果你提供一个包含十个元素的数组并指定范围 [0, 9] 它会正确排序。
@Ofek Ron- 通过该更改,代码可以在我的机器上运行。您如何指定要排序的范围的端点?【参考方案2】:
你最后的 C 代码有效。但这并不直观。 幸运的是,现在我正在学习 CLRS。 在我看来,CLRS 的伪代码是错误的。(在 2e) 最后,我发现换个地方就好了。
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
exchange A[i] ↔ A[j]
else
exchnage A[r] ↔ A[i]
return i
是的,添加一个交换 A[r] ↔ A[i] 可以使它工作。 为什么? 因为 A[i] 现在大于 A[r] OR i == r。 所以我们必须交换来保证分区的特性。
【讨论】:
CLRS 提供 Lomuto 分区方案,而不是 Hoare。【参考方案3】:回答“为什么霍尔分区有效?”的问题:
让我们将数组中的值简化为三种:L 值(小于枢轴值),E 值(等于枢轴值) , 和 G 值(大于枢轴值的值)。
我们还将为数组中的一个位置指定一个特殊的名称;我们称这个位置为 s,它是过程结束时 j 指针所在的位置。我们是否提前知道 s 是哪个位置?不,但我们知道一些位置符合该描述。
使用这些术语,我们可以用稍微不同的术语来表达分区过程的目标:它将单个数组拆分为两个较小的子数组,这些子数组没有错误排序彼此。如果满足以下条件,则满足“未错误排序”的要求:
-
“低”子数组,从数组的左端到包括 s,不包含 G 值。
在 s 之后立即开始并一直到右端的“high”子数组不包含 L 值。
这就是我们真正需要做的。我们甚至不需要担心 E 值在任何给定的传递中结束。只要每次传递都使子数组彼此正确,以后的传递就会处理任何子数组中存在的任何混乱。
所以现在让我们从另一边来解决这个问题:分区过程如何确保在 s 或它的左侧没有 G 值,以及s 右侧没有 L 值?
嗯,“s 右侧的值集”与“j 指针在到达 之前移动的单元格集相同” s”。而“s左侧并包括s的值的集合”与“i指针在j之前移动过的值的集合”相同em> 达到s”。
这意味着任何 错位的值在循环的某些迭代中都将位于我们的两个指针之一之下。 (为方便起见,假设它是指向 L 值的 j 指针,尽管它对于指向 a 的 i 指针完全相同G 值。)当 j 指针位于错位的值上时,i 指针会在哪里?我们知道它会是:
-
在“低”子数组中的某个位置,L 值可以毫无问题地通过该位置;
指向的值可以是 E 或 G 值,可以轻松替换 j 下的 L 值 指针。 (如果它不在 E 或 G 值上,它就不会停在那里。)
请注意,有时 i 和 j 指针实际上都会停在 E 值上。发生这种情况时,值将被切换,即使不需要它。不过,这不会造成任何伤害;我们之前说过 E 值的放置不会导致子数组之间的错误排序。
所以,总而言之,Hoare 分区之所以有效,是因为:
-
它将一个数组分成更小的子数组,这些子数组彼此之间没有错误排序;
如果您继续这样做并对子数组进行递归排序,最终数组中将没有任何未排序的内容。
【讨论】:
当 j 指针位于错误位置时, i 指针会在哪里?我们知道它会是:不会有 i 穿越 j 的可能性吗? “我不会有可能穿越j吗?”是的;在某些时候,i 和 j 会交叉,或者它们会停在相同的值上。但是当这两种情况中的任何一种发生时,条件 (i 【参考方案4】:您的最终代码是错误的,因为j
的初始值应该是r + 1
而不是r
。否则你的分区函数总是忽略最后一个值。
实际上,HoarePartition 之所以有效,是因为对于任何包含至少 2 个元素(即 p < r
)的数组 A[p...r]
,当它终止时,A[p...j]
的每个元素都是 <=
A[j+1...r]
的每个元素。
所以接下来主要算法重复的两个部分是[start...q]
和[q+1...end]
所以正确的C代码如下:
void QuickSort(int a[],int start,int end)
if (end <= start) return;
int q=HoarePartition(a,start,end);
QuickSort(a,start,q);
QuickSort(a,q + 1,end);
int HoarePartition (int a[],int p, int r)
int x=a[p],i=p-1,j=r+1;
while (1)
do j--; while (a[j] > x);
do i++; while (a[i] < x);
if (i < j)
swap(&a[i],&a[j]);
else
return j;
更多说明:
分区部分只是伪代码的翻译。 (注意返回值为j
)
对于递归部分,请注意基本情况检查(end <= start
而不是end <= start + 1
否则您将跳过[2 1]
子数组)
【讨论】:
【参考方案5】:-
将枢轴移至第一个。 (例如,使用 3 的中位数。为小输入大小切换到插入排序。)
分区,
反复交换当前最左边的 1 和当前最右边的 0。
0 -- cmp(val, pivot) == true, 1 -- cmp(val, pivot) == false。
不左则停止
之后,将枢轴与最右边的 0 交换。
【讨论】:
【参考方案6】:首先你误解了Hoare的划分算法,从c中的翻译代码可以看出, 因为你认为枢轴是子数组的最左边的元素。
我会解释你将最左边的元素视为枢轴。
int HoarePartition (int a[],int p, int r)
这里 p 和 r 代表数组的下界和上界,它可以是一个更大的数组的一部分,也可以是要分区的子数组。
所以我们从最初指向数组端点前后的指针(标记)开始(只需 bcoz 使用 do while 循环)。因此,
i=p-1,
j=r+1; //here u made mistake
现在根据分区,我们希望枢轴左侧的每个元素都小于或等于枢轴并大于枢轴右侧的每个元素。
所以我们将移动“i”标记,直到我们得到大于或等于枢轴的元素。和类似的 'j' 标记,直到我们找到小于或等于枢轴的元素。
现在如果 i
do j--; while (a[j] <= x); //look at inequality sign
do i++; while (a[i] >= x);
if (i < j)
swap(&a[i],&a[j]);
现在如果 'i' 不小于 'j',这意味着现在中间没有元素可以交换,所以我们返回 'j' 位置。
所以现在分区下半部分后的数组是从'start到j'
上半部分是从'j+1到end'
所以代码看起来像
void QuickSort(int a[],int start,int end)
int q=HoarePartition(a,start,end);
if (end<=start) return;
QuickSort(a,start,q);
QuickSort(a,q+1,end);
【讨论】:
【参考方案7】:java 中的简单实现。
public class QuickSortWithHoarePartition
public static void sort(int[] array)
sortHelper(array, 0, array.length - 1);
private static void sortHelper(int[] array, int p, int r)
if (p < r)
int q = doHoarePartitioning(array, p, r);
sortHelper(array, p, q);
sortHelper(array, q + 1, r);
private static int doHoarePartitioning(int[] array, int p, int r)
int pivot = array[p];
int i = p - 1;
int j = r + 1;
while (true)
do
i++;
while (array[i] < pivot);
do
j--;
while (array[j] > pivot);
if (i < j)
swap(array, i, j);
else
return j;
private static void swap(int[] array, int i, int j)
int temp = array[i];
array[i] = array[j];
array[j] = temp;
【讨论】:
以上是关于快速排序和霍尔分区的主要内容,如果未能解决你的问题,请参考以下文章