我试图从中实现QuickSort算法 Thomas H. Cormen,Charles E. Leiserson,Ronald L.Rivest,Clifford Stein-算法简介,第三版-2009 *第7.1节
在JavaScript中。
这是我的实现:
function swap(arr, i, j) {
const temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
function partition(arr, left = 0, right = arr.length - 1) {
let pivot = arr[right];
let i = left - 1;
let j;
for (j = left; j < right; j++) {
if (arr[j] < pivot) {
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, right);
return i + 1;
}
function quickSort(arr, left = 0, right = arr.length - 1) {
if (left >= right) return arr;
const p = partition(arr, left, right);
quickSort(arr, left, p - 1);
quickSort(arr, p + 1, right);
return arr;
}
如果我通过长度超过10000的已排序数组,则此代码的问题将导致堆栈溢出错误而失败
如果我传递带有完全随机整数的数组-即使有5万个元素,一切也按预期工作。 所以我不明白我的代码是否有问题,或者我的节点无法处理如此大的调用堆栈以应对最坏的情况下快速排序。
答案 0 :(得分:4)
有了足够大的数组,您最终将达到最大堆栈大小并得到该异常。之所以会在排序数组上看到它,是因为您选择数据透视表的方式。
您已经使用数据透视表作为数组的最后一个元素来实现它,这恰好意味着当您获得排序数组时,最坏的情况就会发生。您的枢轴值始终是最大值(如果沿相反方向排序,则始终是最小值),因此每个元素都小于枢轴,没有更大的值。因此,与其将数组分为两个大致相等的子数组,不如将其拆分为一个仅包含比开始时少一个元素的子数组。
一种选择避免这种情况的枢轴的方法是随机选择枢轴。这使得不可能(但不是不可能)击中最坏的情况,因此平均而言效果很好。它确实存在一个漏洞,即如果有人知道您正在使用的随机数算法和种子,那么他们可以制作一个数组,这将迫使您进入最坏的情况。
理想的枢轴是中间值。因此,一种方法是尝试找到中位数或接近中位数。例如,您可以对数组的3个随机元素进行采样,并将这3个元素的中位数用作轴。这不太可能是准确的中位数,但在大多数情况下都足够好。您可以进行更多采样以获得更好的中位数近似值,但是随后您将花费更多的时间来尝试找到支点,而不仅仅是继续使用算法。这有点折衷。
答案 1 :(得分:3)
如果我通过长度> 10000的已排序数组,则会失败并出现stackoverflow错误
与快速排序一样,问题是choice of the pivot element。您正在使用ItemSource
,因此在已经排序的数组中,ItemSource
函数会将范围分为pivot = arr[right]
和partition
。您的递归调用的二叉树会退化为一个列表,而10000个递归调用对于堆栈来说太多了。
替代方案是随机选择枢轴索引(这不太可能,但并非不可能失败),或寻找中位数。
答案 2 :(得分:0)
无论您选择哪种枢轴,总是有堆栈溢出的机会。为了避免这种情况,下面是一个示例C ++快速排序函数,该函数在较小的部分上使用递归,然后为较大的部分循环返回。这将堆栈空间限制为O(log(n)),但最坏的情况下时间复杂度仍为O(n ^ 2)。
void QuickSort(uint64_t a[], int lo, int hi)
{
while (lo < hi){
uint64_t p = a[hi];
int i = lo;
for (int j = lo; j < hi; ++j){
if (a[j] < p){
std::swap(a[j], a[i]);
++i;
}
}
std::swap(a[i], a[hi]);
if(i - lo <= hi - i){
QuickSort(a, lo, i-1);
lo = i+1;
} else {
QuickSort(a, i+1, hi);
hi = i-1;
}
}
}