我正在努力找出一个有效的quicksort
算法。它工作正常,但是当元素数量很大时,需要很长时间才能运行,并且数组的某些部分是预先排序的。我正在quicksort
查找维基百科的文章,在那里我发现了这个:
为了确保最多使用O(log N)空间,首先递归到数组的较小一半,并使用尾调用递归到另一个。
使用插入排序,其具有较小的常数因子,因此在小阵列上更快,用于在这样的小阵列上的调用(即,长度小于实验确定的阈值t)。这可以通过将这些数组保持未排序并在末尾运行单个插入排序传递来实现,因为插入排序有效地处理几乎排序的数组。在识别每个小段时单独插入排序会增加启动和停止许多小排序的开销,但避免浪费在多个段边界上比较密钥的工作量,由于快速排序过程的工作原因,这些密钥将按顺序排列。它还可以改善缓存的使用。
我目前正在为两个分区递归。知道如何实现第一个提示吗?什么是首先递归到数组的较小一半,并使用尾调用来递归到另一个?其次,如何在快速排序中实现insertion-sort
?它是否总能提高效率,或者只有在阵列的某些部分进行预先排序时?如果是第二种情况,那么当然我无法知道何时会发生这种情况。那么我应该何时加入insertion-sort
?
答案 0 :(得分:12)
在快速排序中,您选择一个随机数据透视表,将数组分隔为两半,大部分机会可能更小,
e.g。数组大小为100,pivot将数组分隔为40/60,40为较小的大小。
让我们假设您决定使用插入排序为10的阈值大小, 你需要通过pivot继续递归地拆分数组,只要其中一半变小或等于10,就可以使用在小尺寸数组上行为类似于O(n)的插入排序。
考虑到如果您的数组反向排序(最坏的情况),插入排序会表现不佳。
关于递归的东西,你只需要修改快速排序递归的停止案例 - >数组大小< = 10停止递归并使用插入排序对所有数组(在此递归步骤中小得多)进行排序。
通过尾递归,它们意味着用前半部分完成所需的一切,然后作为最后一个方法调用较小的一半的插入排序,它用于节省空间。
Quick-sort()
choose a pivot
move the smaller elements from left
move the bigger elements from right
quick-sort on the bigger half of the array
if half is less then X
only then do an insertion sort on the other half <- this is a tail recursion insertion sort
else
quick sort on this half also
据我所知,第二个优化建议不要对每个递归步骤使用插入排序,但要记住为其做出约束的索引,然后在一个批处理中连接所有切片中的项目时调用插入排序,这将确保改进缓存使用,但实现起来稍微困难,
答案 1 :(得分:7)
有多种方法可以使标准的快速排序更有效率。要从帖子中实现第一个提示,您应该写一些类似的内容:
void quicksort(int * tab, int l, int r)
{
int q;
while(l < r)
{
q = partition(tab, l, r);
if(q - l < r - q) //recurse into the smaller half
{
quicksort(tab, l, q - 1);
l = q + 1;
} else
{
quicksort(tab, q + 1, r);
r = q - 1;
}
}
}
希望足够清楚。下一步是实现您自己的堆栈(或使用您使用的任何语言内置的一些内容),而不是使用递归调用。示例(伪)代码:
void quicksort2(int * tab, int l, int r)
{
int le, ri, q;
init stack;
push(l, r, stack);
while(!empty(stack))
{
//take the top pair of values from the stack and set them to le and ri
pop(le, ri, stack);
if(le >= ri)
continue;
q = partition(tab, le, ri);
if(q - le < ri - q) //smaller half goes first
{
push(le, q - 1, stack);
push(q + 1, ri, stack);
} else
{
push(q + 1, ri, stack);
push(le, q - 1, stack);
}
}
delete stack;
}
然后,您可以继续执行帖子中的其他提示。要做到这一点,你应该设置一些任意常量,让我们称之为CUT_OFF,到20左右。这将告诉你的算法什么时候应该切换到插入排序。它应该是相当容易的(添加一个if语句的问题)来改变前一个例子,以便在它达到CUT_OFF点后切换到插入排序,这样我就会离开你。
至于分区方法,我建议使用Lomuto分区而不是Hoare。
但是,如果您的数据已经预先排序,那么您可以考虑使用不同的算法。根据我的经验,如果您的数据是预先排序的,那么在链接列表上实现的自然系列合并排序是一个非常好的选择。
答案 2 :(得分:4)
我前段时间写过一个你可以在那里找到的基于快速排序的算法(实际上它是一种选择算法,但也可以用于排序算法):
我从这次经历中吸取的教训如下:
我希望这有帮助,劳伦特。
答案 3 :(得分:1)
你可以看一下TimSort,非完全随机数据比quicksort更好(它们具有相同的渐近复杂度,但TimSort具有较低的常量)
答案 4 :(得分:1)
我最近找到了this optimization。 它比std :: sort工作得更快。它使用小数组上的选择排序和3的中位数作为分区元素。
这是我的C ++实现:
const int CUTOFF = 8;
template<typename T>
bool less (T &v, T &w)
{
return (v < w);
}
template<typename T>
bool eq (T &v, T &w)
{
return w == v;
}
template <typename T>
void swap (T *a, T *b)
{
T t = *a;
*a = *b;
*b = t;
}
template<typename T>
void insertionSort (vector<T>& input, int lo, int hi)
{
for (int i = lo; i <= hi; ++i)
{
for (int j = i; j > lo && less(input[j], input[j-1]); --j)
{
swap(&input[j], &input[j-1]);
}
}
}
template<typename T>
int median3 (vector<T>& input, int indI, int indJ, int indK)
{
return (less(input[indI], input[indJ]) ?
(less(input[indJ], input[indK]) ? indJ : less(input[indI], input[indK]) ? indK : indI) :
(less(input[indK], input[indJ]) ? indJ : less(input[indK], input[indI]) ? indK : indI));
}
template <typename T>
void sort(vector<T>& input, int lo, int hi)
{
int lenN = hi - lo + 1;
// cutoff to insertion sort
if (lenN <= CUTOFF)
{
insertionSort(input, lo, hi);
return;
}
// use median-of-3 as partitioning element
else if (lenN <= 40)
{
int median = median3(input, lo, lo + lenN / 2, hi);
swap(&input[median], &input[lo]);
}
// use Tukey ninther as partitioning element
else
{
int eps = lenN / 8;
int mid = lo + lenN / 2;
int mFirst = median3(input, lo, lo + eps, lo + eps + eps);
int mMid = median3(input, mid - eps, mid, mid + eps);
int mLast = median3(input, hi - eps - eps, hi - eps, hi);
int ninther = median3(input, mFirst, mMid, mLast);
swap(&input[ninther], &input[lo]);
}
// Bentley-McIlroy 3-way partitioning
int iterI = lo, iterJ = hi + 1;
int iterP = lo, iterQ = hi + 1;
for (;; )
{
T v = input[lo];
while (less(input[++iterI], v))
{
if (iterI == hi)
break;
}
while (less(v, input[--iterJ]))
{
if (iterJ == lo)
break;
}
if (iterI >= iterJ)
break;
swap(&input[iterI], &input[iterJ]);
if (eq(input[iterI], v))
swap(&input[++iterP], &input[iterI]);
if (eq(input[iterJ], v))
swap(&input[--iterQ], &input[iterJ]);
}
swap(&input[lo], &input[iterJ]);
iterI = iterJ + 1;
iterJ = iterJ - 1;
for (int k = lo + 1; k <= iterP; ++k)
{
swap(&input[k], &input[iterJ--]);
}
for (int k = hi ; k >= iterQ; --k)
{
swap(&input[k], &input[iterI++]);
}
sort(input, lo, iterJ);
sort(input, iterI, hi);
}
答案 5 :(得分:0)
尾递归是将递归调用更改为循环。 对于QuickSort,它会像:
QuickSort(SortVar)
Granularity = 10
SortMax = Max(SortVar)
/* Put an element after the last with a higher key than all other elements
to avoid that the inner loop goes on forever */
SetMaxKey(SortVar, SortMax+1)
/* Push the whole interval to sort on stack */
Push 1 SortMax
while StackSize() > 0
/* Pop an interval to sort from stack */
Pop SortFrom SortTo
/* Tail recursion loop */
while SortTo - SortFrom >= Granularity
/* Find the pivot element using median of 3 */
Pivot = Median(SortVar, SortFrom, (SortFrom + SortTo) / 2, SortTo)
/* Put the pivot element in front */
if Pivot > SortFrom then Swap(SortVar, SortFrom, Pivot)
/* Place elements <=Key to the left and elements >Key to the right */
Key = GetKey(SortVar, SortFrom)
i = SortFrom + 1
j = SortTo
while i < j
while GetKey(SortVar, i) <= Key; i = i + 1; end
while GetKey(SortVar, j) > Key; j = j - 1; end
if i < j then Swap(SortVar, i, j)
end
/* Put the pivot element back */
if GetKey(SortVar, j) < Key then Swap(SortVar, SortFrom, j)
if j - SortFrom < SortTo - j then
/* The left part is smallest - put it on stack */
if j - SortFrom > Granularity then Push SortFrom j-1
/* and do tail recursion on the right part */
SortFrom = j + 1
end
else
/* The right part is smallest - put it on stack */
if SortTo - j > Granularity then Push j+1 SortTo
/* and do tail recursion on the left part */
SortTo = j - 1
end
end
end
/* Run insertionsort on the whole array to sort the small intervals */
InsertionSort(SortVar)
return
此外,没有理由在较小的时间间隔内调用InsertionSort,因为当QuickSort完成时,数组会被粗略地排序,这样只需要很小的间隔来排序。这只是InsertionSort的完美案例。
如果你没有堆栈,你可以使用递归代替 - 但保持尾递归:
QuickSort(SortVar, SortFrom, SortTo)
Granularity = 10
/* Tail recursion loop */
while SortTo - SortFrom >= Granularity
/* Find the pivot element using median of 3 */
Pivot = Median(SortVar, SortFrom, (SortFrom + SortTo) / 2, SortTo)
/* Put the pivot element in front */
if Pivot > SortFrom then Swap(SortVar, SortFrom, Pivot)
/* Place elements <=Key to the left and elements >Key to the right */
Key = GetKey(SortVar, SortFrom)
i = SortFrom + 1
j = SortTo
while i < j
while GetKey(SortVar, i) <= Key; i = i + 1; end
while GetKey(SortVar, j) > Key; j = j - 1; end
if i < j then Swap(SortVar, i, j)
end
/* Put the pivot element back */
if GetKey(j) < Key then Swap(SortVar, SortFrom, j)
if j - SortFrom < SortTo - j then
/* The left part is smallest - recursive call */
if j - SortFrom > Granularity then QuickSort(SortVar, SortFrom, j-1)
/* and do tail recursion on the right part */
SortFrom = j + 1
end
else
/* The right part is smallest - recursive call */
if SortTo - j > Granularity then QuickSort(SortVar, j+1, SortTo)
/* and do tail recursion on the left part */
SortTo = j - 1
end
end
/* Run insertionsort on the whole array to sort the small intervals */
InsertionSort(SortVar)
return