Javascript 快速排序算法失败,出现stackoverflow错误

Javascript 快速排序算法失败,出现stackoverflow错误,javascript,node.js,algorithm,sorting,quicksort,Javascript,Node.js,Algorithm,Sorting,Quicksort,我试图实现快速排序算法 托马斯·H·科门,查尔斯·E·莱瑟森,罗纳德·L·里维斯特,克利福德·斯坦——算法导论,第三版——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) {

我试图实现快速排序算法 托马斯·H·科门,查尔斯·E·莱瑟森,罗纳德·L·里维斯特,克利福德·斯坦——算法导论,第三版——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的已排序数组,它将失败,并出现stackoverflow错误

如果我传递一个带有完全随机整数的数组,那么即使有50000个元素,一切都会正常工作。
因此,我无法理解是我的代码有问题,还是我的节点无法处理如此大的调用堆栈以实现最坏情况下的快速排序使用。

使用足够大的数组,您最终将达到最大堆栈大小并获得该异常。您之所以在前面的排序数组中看到它,是因为您选择轴心的方式

您已经将pivot作为数组的最后一个元素来实现它,这恰好意味着当您得到一个排序数组时,最坏的情况就会发生。如果按相反方向排序,则轴心值始终是最大值或最小值,因此每个元素都小于轴心,没有大于轴心的元素。因此,您没有将阵列拆分为两个大致相等的子阵列,而是将其拆分为一个子阵列,该子阵列的元素数仅比开始时少一个

选择轴以避免这种情况的一种方法是随机拾取轴。这使得它不太可能,但并非不可能达到最坏的情况,因此平均而言,它将运行良好。它确实有一个漏洞,如果有人知道你正在使用的随机数算法和种子,那么他们可以制作一个数组,迫使你进入最坏的情况


理想的轴心是中值。因此,一种方法是试图找到中间值或接近中间值。例如,可以对数组中的3个随机元素进行采样,并使用这3个元素的中间值作为轴心。这不太可能是中位数,但在大多数情况下已经足够好了。你可以进行更多的采样,以获得更好的中位数近似值,但是你要花更多的时间寻找轴心点,而不仅仅是继续使用算法;这有点折衷。

对于足够大的数组,您最终将达到最大堆栈大小并获得该异常。您之所以在前面的排序数组中看到它,是因为您选择轴心的方式

您已经将pivot作为数组的最后一个元素来实现它,这恰好意味着当您得到一个排序数组时,最坏的情况就会发生。如果按相反方向排序,则轴心值始终是最大值或最小值,因此每个元素都小于轴心,没有大于轴心的元素。因此,您没有将阵列拆分为两个大致相等的子阵列,而是将其拆分为一个子阵列,该子阵列的元素数仅比开始时少一个

选择轴以避免这种情况的一种方法是随机拾取轴。这使得它不太可能,但并非不可能达到最坏的情况,因此平均而言,它将运行良好。它确实有一个漏洞,如果有人知道你正在使用的随机数算法和种子,那么他们可以制作一个数组,迫使你进入最坏的情况

理想的轴心是中值。因此,一种方法是试图找到中间值或接近中间值。例如,可以对数组中的3个随机元素进行采样,并使用这3个元素的中间值作为轴心。这不太可能是中位数,但在大多数情况下已经足够好了。你可以进行更多的采样,以获得更好的中位数近似值,但是你要花更多的时间寻找轴心点,而不仅仅是继续使用算法;这有点折衷

如果我通过长度>10000的已排序数组,则会失败,并出现stackoverflow错误

与快速排序一样,问题在于。您使用的是pivot=arr[right],因此在已排序的数组中,分区函数将范围划分为[left,right-1]和[right,right]。递归调用的二叉树退化为一个列表,10000个递归调用对于堆栈来说太多了

可供选择的方法是随机选择枢轴指数,该指数不太可能(但并非不可能)失败或找到中值

如果我通过长度>10000的已排序数组,则会失败,并出现stackoverflow错误

与快速排序一样,问题在于。您使用的是pivot=arr[right],因此在已排序的数组中,分区函数将范围划分为[left,right-1]和[right,right]。递归调用的二叉树退化为一个列表,10000个递归调用对于堆栈来说太多了


备选方案是随机选择轴索引,这不太可能失败,但也不是不可能找到中值。

无论选择哪个轴,堆栈总是有溢出的可能性。为了避免这种情况,这里有一个 示例C++快速排序函数,在较小的部分使用递归,然后循环回较大的部分。这将堆栈空间限制为Ologn,但最坏情况下的时间复杂度保持在^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;
        }
    }
}

无论您选择哪个支点,总有可能出现堆栈溢出。为了避免这一点,这里有一个C++快速排序函数,它在较小的部分使用递归,然后循环回较大的部分。这将堆栈空间限制为Ologn,但最坏情况下的时间复杂度保持在^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;
        }
    }
}

对于幼稚的quicksort版本来说,已经排序的数组可能是一个问题。该算法通常在日志n上,但最坏情况下的性能是在堆栈深度为On的^2上。对于初始版本的快速排序,已经排序的数组可能会有问题。该算法通常在logn上,但最坏情况下的性能是在^2上,堆栈深度为On。谢谢,现在我明白了。我认为快速排序是最好的选择,但现在我明白了这个算法的问题所在。即使我选择pivot索引作为随机值,在极少数情况下也可能失败。为了避免这个问题,我需要在进行实际排序之前以某种方式分析数组。这就是为什么大多数编程语言使用mergesort变体作为默认排序实现的原因。谢谢,现在我明白了。我认为快速排序是最好的选择,但现在我明白了这个算法的问题所在。即使我选择pivot索引作为随机值,在极少数情况下也可能失败。为了避免这个问题,我需要在进行实际排序之前以某种方式分析数组。这就是为什么大多数编程语言使用mergesort变体作为默认排序实现的原因。