【问题标题】:Big O question for QuickSort with Javascript使用 Javascript 进行快速排序的大 O 问题
【发布时间】:2020-04-01 20:38:41
【问题描述】:

我正在使用 QuickSort,我在 LeetCode 上为this problem 测试了这两种算法。两种算法都有效,但一种似乎比另一种快得多,我不明白为什么。

这个超出了 LeetCode 的时限:

function quickSort(array) {
    quickSortHelper(array, 0, array.length - 1);
    return array;
}

function quickSortHelper(array, start, end) {
    // Edge case
    if(start >= end) return;
    // Always choose the pivot index at the beginning of the array.
    const pivot = start;
    // The left index is next to the pivot on the right.
    let left = start + 1;
    let right = end;

    while (right >= left) {
        if (array[left] > array[right] && array[right] < array[pivot])
            swap(left, right, array);
        if (array[left] < array[pivot]) left += 1;
        if (array[right] > array[pivot]) right -= 1;
    }

    swap(pivot, right, array);

    // Always sort the smaller sub-array first
    const leftIsSmaller = right - 1 - start < end - (right + 1);

    if (leftIsSmaller) {
        quickSort(array, start, right - 1);
        quickSort(array, right + 1, end);
    }
    else {
        quickSort(array, right + 1, end);
        quickSort(array, start, right - 1);
    }
}

function swap(a, b, array) {
    [array[a], array[b]] = [array[b], array[a]];
    return array;
}

大O:

  • 最差:时间 = O(n^2),因为如果主元不断被交换到未排序部分的末尾,那么我们每次都有 O(n - 1) ~ O(n) = O(n*n )
  • 最佳:当枢轴将数组分成两半时,时间 = O(nlog(n))。
  • 平均:时间 = O(nlog(n))
  • Space = O(log(n)),因为递归使用 2 个子数组并首先针对较小的子数组进行快速排序。

这个没有超过 LeetCode 的时间限制,并且比大多数提交都快得多。大 while 循环中的 2 个 while 循环(对我而言)似乎更慢,但实际上更快。

function quickSort(array) {
    sortHelper(array, 0, array.length - 1);
    return array;
}

function sortHelper(array, start, end) {
    if (start >= end) return;
    let i = start;
    let j = end;
    let base = array[i];

    while (i < j) {
        while (i < j && array[j] >= base) j -= 1; // This makes sense but why this while loop has to happen before the other while loop?
        array[i] = array[j]; // this line
        while (i < j && array[i] <= base) i += 1;
        array[j] = array[i]; // and this line. don't they make you lose access to the values in the array?
    }
    array[i] = base;
    sortHelper(array, start, i - 1);
    sortHelper(array, i + 1, end);
}

这是为什么呢?

【问题讨论】:

  • swap 在循环中创建 2 个堆对象,而代码 B 只是增加/减少数字。这可能会受到伤害,但我一目了然地看不到任何时间复杂度差异。你做过分析吗?
  • 如果您在第一个中重写swap() 以便它与临时局部变量进行简单交换,则性能可能会更接近。 (无论如何,运行时可能会有效地执行解构分配;我不确定,但我肯定会尝试它。)
  • @ggorlen 谢谢你的建议。我还没有完成分析,但我正在研究它。
  • @Pointy:我已经用let tmp = array[a]; array[a] = array[b]; array[b] = tmp; 重写了swap(),但是算法仍然超过了时间限制。
  • 虽然时间复杂度保持不变,但您可以通过将内部 while 循环减少为单个条件来进一步加快快速排序的恒定时间部分,如 wiki pseudo code 所示。

标签: javascript arrays algorithm performance quicksort


【解决方案1】:

时间复杂度没有区别,但第一个版本经常多次执行相同(或相反)的数据比较。这在第二个版本中没有发生。

例如,假设对于以下if,第一个表达式为真,第二个为假,并且在多次迭代中:

 if (array[left] > array[right] && array[right] < array[pivot])

...那么第一个表达式被检查多次确实是一种浪费,因为left 在第一个表达式为真时不会从一个迭代更改为下一个迭代。

以这个极端示例数组为例:[1, 6, 3, 2, 5, 4]

轴心为 1。

在所有迭代期间,right 索引将减小,直到到达左侧。每次迭代的比较次数为 4,因此 5 次迭代加起来为 20。

现在看看更快的算法。对于相同输入,第一个内部循环的比较次数为 5,第二个内部循环为 0(我只计算 data 比较)。

这是一个极端的例子(20 对 5),但在更多随机输入的情况下发生的程度也较小。

【讨论】:

  • 感谢@trincot 的回答。这就说得通了。我不太了解更快的算法。如果您不介意进一步解释一下:为什么在大的 while 循环中,j -= 1 的 while 循环必须是第一个?不要array[i] = array[j];array[j] = array[i]; 让您无法访问数组中的值?
  • 您可以写信给array[i],因为您在base 中拥有原始值的副本。之后您可以写信给array[j],因为您现在在array[i] 中有一份原件的副本。因为您从base = array[i] 开始,所以您必须首先通过执行第一个内部循环来分配给array[i] =
  • “总是先排序较小的子数组”可能会在对非常大的数组进行排序时降低堆栈溢出的风险...
猜你喜欢
  • 1970-01-01
  • 2019-03-10
  • 2018-01-31
  • 2022-11-13
  • 1970-01-01
  • 2017-06-19
  • 1970-01-01
  • 2019-07-05
  • 1970-01-01
相关资源
最近更新 更多