【发布时间】:2011-01-07 12:34:27
【问题描述】:
处理之前快速排序中的重复元素
我找到了一种在快速排序中更有效地处理重复元素的方法,我想知道是否有人以前见过这种方法。
这种方法大大减少了检查重复元素所涉及的开销,从而提高了有无重复元素的性能。通常,重复的元素以几种不同的方式处理,我将首先列举这些方式。
首先,有一种荷兰国旗方法,可以像[ < pivot | == pivot | unsorted | > pivot] 那样对数组进行排序。
其次,有一种方法,在排序过程中将相等的元素放在最左边,然后将它们移动到中心排序为[ == pivot | < pivot | unsorted | > pivot],然后在排序后将==元素移动到中心。
第三,Bentley-McIlroy 分区将== 元素放在两侧,因此排序为[ == pivot | < pivot | unsorted | > pivot | == pivot],然后== 元素移到中间。
最后两种方法是为了减少开销。
我的方法
现在,让我解释一下我的方法如何通过减少比较次数来改进快速排序。 我一起使用两个快速排序功能,而不仅仅是一个。
我将调用q1 的第一个函数,它将数组排序为[ < pivot | unsorted | >= pivot]。
我将调用q2 的第二个函数,它将数组排序为[ <= pivot | unsorted | > pivot]。
现在让我们一起看看它们的用法,以改进对重复元素的处理。
首先,我们调用q1 对整个数组进行排序。它选择一个我们将进一步称为pivot1 的枢轴,然后围绕pivot1 进行排序。因此,我们的数组被排序为[ < pivot1 | >= pivot1 ]。
然后,对于[ < pivot1]分区,我们再次发送到q1,这部分是相当正常的,所以我们先对另一个分区进行排序。
对于[ >= pivot1] 分区,我们将其发送到q2。 q2 选择一个枢轴,我们将在此子数组中将其称为pivot2,并将其排序为[ <= pivot2 | > pivot2]。
如果我们现在查看整个数组,我们的排序看起来像[ < pivot1 | >= pivot1 and <= pivot2 | > pivot2]。这看起来很像双轴快速排序。
现在,让我们回到q2 ([ <= pivot2 | > pivot2]) 内部的子数组。
对于[ > pivot2] 分区,我们只是将其发送回q1,这不是很有趣。
对于[ <= pivot2]分区,我们首先检查是否pivot1 == pivot2。如果它们相等,则该分区已经排序,因为它们都是相等的元素!如果枢轴不相等,那么我们只需将此分区再次发送到q2,它会选择一个枢轴(进一步pivot3),排序,如果pivot3 == pivot1,那么它不必对[ <= pivot 3]和很快。
希望你现在明白了。这种技术的改进在于处理相等的元素,而不必检查每个元素是否也等于枢轴。换句话说,它使用的比较更少。
与其总子数组的大小,然后在这种情况下对重复元素进行更标准的检查(上面列出的方法之一)。
源代码
这里有两个非常简化的qs1 和qs2 函数。他们使用 Sedgewick 聚合指针排序方法。它们显然可以非常优化(例如,它们选择枢轴非常差),但这只是为了展示这个想法。我自己的实现更长、更快、更难阅读,所以让我们从这个开始:
// qs sorts into [ < p | >= p ]
void qs1(int a[], long left, long right){
// Pick a pivot and set up some indicies
int pivot = a[right], temp;
long i = left - 1, j = right;
// do the sort
for(;;){
while(a[++i] < pivot);
while(a[--j] >= pivot) if(i == j) break;
if(i >= j) break;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
// Put the pivot in the correct spot
temp = a[i];
a[i] = a[right];
a[right] = temp;
// send the [ < p ] partition to qs1
if(left < i - 1)
qs1(a, left, i - 1);
// send the [ >= p] partition to qs2
if( right > i + 1)
qs2(a, i + 1, right);
}
void qs2(int a[], long left, long right){
// Pick a pivot and set up some indicies
int pivot = a[left], temp;
long i = left, j = right + 1;
// do the sort
for(;;){
while(a[--j] > pivot);
while(a[++i] <= pivot) if(i == j) break;
if(i >= j) break;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
// Put the pivot in the correct spot
temp = a[j];
a[j] = a[left];
a[left] = temp;
// Send the [ > p ] partition to qs1
if( right > j + 1)
qs1(a, j + 1, right);
// Here is where we check the pivots.
// a[left-1] is the other pivot we need to compare with.
// This handles the repeated elements.
if(pivot != a[left-1])
// since the pivots don't match, we pass [ <= p ] on to qs2
if(left < j - 1)
qs2(a, left, j - 1);
}
我知道这是一个相当简单的想法,但是当我添加标准快速排序改进(3 中值枢轴选择和小数组的插入排序开始)时,它在运行时提供了相当显着的改进。如果您要使用此代码进行测试,请仅对随机数据执行此操作,因为枢轴选择不佳(或改进枢轴选择)。要使用这种类型,您可以调用:
qs1(array,0,indexofendofarray);
一些基准
如果您想知道它的速度有多快,这里有一些数据供初学者参考。这使用了我的优化版本,而不是上面给出的版本。但是,上面给出的时间仍然比std::sort 时间更接近双轴快速排序。
在具有 2,000,000 个元素的高度随机数据上,我得到了这些时间(通过对几个连续的数据集进行排序):
std::sort - 1.609 seconds
dual-pivot quicksort - 1.25 seconds
qs1/qs2 - 1.172 seconds
std::sort 是 C++ 标准库排序,双轴快速排序是几个月前由 Vladimir Yaroslavskiy 提出的,qs1/qs2 是我的快速排序实现。
在更少的随机数据上。有 2,000,000 个元素并使用 rand() % 1000 生成(这意味着每个元素大约有 2000 个副本)时间是:
std::sort - 0.468 seconds
dual-pivot quicksort - 0.438 seconds
qs1/qs2 - 0.407 seconds
在某些情况下双轴快速排序胜出,我确实意识到双轴快速排序可以进行更多优化,但我的快速排序可以安全地说明这一点。
有人见过这个吗?
我知道这是一个很长的问题/解释,但是你们中的任何人以前见过这种改进吗?如果是这样,那为什么不使用它?
【问题讨论】:
-
你要做的是n学术文献搜索。 R Samuel Klatchko 在上一个关于这个主题的问题中为您提供了最佳期刊的链接,theMick 告诉您如果您不知道自己在做什么,如何开始。
-
我的格式现在好点了吗?你有什么建议吗?有没有更好的网站可以发布这个?
-
无论你做什么,根据 Allen Weiss 书中给出的决策树模型证明(C++ 中的数据结构和算法),排序将在平均情况下进行 O(NlogN) 比较。
-
是的,这仍然是平均 O(N log N) 排序。该系数仅比处理重复元素的其他系数小。
-
@Raccha:不熟悉那本书,但我认为 O(Nlog N) 下限假定只有 1 个比较函数可用。当然,如果
<=和>=神奇地使渐近更快的算法成为可能,我会感到非常惊讶!
标签: c++ algorithm sorting quicksort