使用快速排序对大型列表进行排序时,总是会发生堆栈溢出

Stack overflow always happens when sorting large lists with quicksort

提问人:Joe Cronin 提问时间:11/13/2023 最后编辑:user207421Joe Cronin 更新时间:11/13/2023 访问量:101

问:

我正在对作业进行快速排序,但在对大型列表(>100.000 个项目)进行排序时,它总是会引发堆栈溢出。堆栈溢出应该表明停止条件不正确,但我找不到问题。我真的很感激一些帮助。感谢您的阅读。 我的快速排序:

private void qSort(List<E> items, int low, int high, Comparator<E> comparator) {
    // base case
    if (low >= high) {
        return;
    }

    // set the pivot (last element)
    E pivot = items.get(high);

    int leftPointer = low;
    int rightPointer = high;

    while (leftPointer < rightPointer) {
        // we move the left pointer until we find a value that is bigger than the pivot
        while (leftPointer < rightPointer && comparator.compare(items.get(leftPointer), pivot) <= 0) {
            leftPointer++;
        }

        // we move the right pointer until we find a value that is smaller than the pivot
        while (leftPointer < rightPointer && comparator.compare(items.get(rightPointer), pivot) >= 0) {
            rightPointer--;
        }

        // swap the values at the pointers
        swap(items, leftPointer, rightPointer);

    }
    // at this point, leftPointer and rightPointer are equal and we need to swap the item at the pointers with the pivot
    swap(items, leftPointer, high);

    // now we have the pivot is in the correct position
    // we continue recursively sorting the left side of the pivot, (not including the pivot, because its already in its final position)
    qSort(items, low, leftPointer - 1, comparator);
    // and the right side of the pivot
    qSort(items, leftPointer + 1, high, comparator);

}

我尝试了各种不同的实现,但没有一个有效。我试图增加堆栈大小。

Java 算法 快速排序

评论

1赞 Turing85 11/13/2023
"堆栈溢出应该表明停止条件不正确,但我找不到问题。- 没有尾部调用优化 (stackoverflow.com 的语言的每次递归最终都可能遇到 .我们可以尝试通过 JVM 参数 -Xssdocs.oracle.com 来增加堆栈大小StacKOverflowError
0赞 user207421 11/13/2023
首先对较小的分区进行排序,然后对另一个分区进行排序。这样可以最大程度地减少堆栈使用。
0赞 WJS 11/13/2023
不完全是你的问题,但与它有关。查看 stackoverflow.com/questions/65134616/...

答:

2赞 btilly 11/13/2023 #1

如果数据包含已排序的游程,则快速排序的项目将需要堆栈帧。即使只有一些排序的运行,这也可能是一个问题。nO(n)

有几种解决方案。最简单的方法是在使用之前简单地将枢轴与随机元素交换。现在,您将完成生成伪随机选择的工作。但现在几乎不可能触发快速排序的潜在病理行为。O(n)O(n)

如今最常用的解决方案是使用另一种排序算法。Timsort 是一个流行的选择,因为它的最坏情况行为是 ,但它可以在许多常见的现实世界场景中找到并利用排序序列来获取时间。O(n log(n))O(n)

2赞 Luatic 11/13/2023 #2

不久前,我在我的博客上写了关于实施Quicksort的陷阱。查看您的实现:

  • 您似乎正在使用 Lomuto 分区方案(双向分区)。这意味着具有许多重复项的数组将触发快速排序的最坏情况,其中一个分区是微不足道的(单个元素/无元素),而另一个分区包含所有剩余的元素。然后,这可能需要阵列长度的线性堆栈大小,从而导致堆栈溢出。我建议切换到三向分区(“荷兰国旗排序”),划分为比枢轴更小、相等和更大的元素;然后,您只需要递归地对“较小”和“较大”的分区进行排序。
  • 您总是选择最后一个元素作为枢轴。这意味着对于排序数组,将发生最坏的情况:一个分区是微不足道的,另一个分区包含除一个之外的所有元素。正如其他人已经指出的那样,一个简单的解决方案是随机选择枢轴;这样,您可以实现预期的 O(n log n) 运行时。
  • 此外,您可以(并且应该)实现一个简单的技巧来绑定堆栈使用:首先对较小的分区进行排序,然后通过尾部调用对另一个分区进行排序,不使用堆栈空间。不幸的是,Java没有适当的尾部调用,所以你必须选择迭代实现:使用一堆“作业”,其中你的类只是一个数组“slice”/range(两个整数:slice的开始和结束索引)。最初,推送作业 (0, n),其中 n 是数组长度,然后通过推送作业 (from, to) 来执行“递归调用”。迭代实现将完全避免堆栈溢出,但您可能仍希望限制辅助空间使用量 - 为此,请先推送更大的作业。这背后的基本原理是,我们希望较小的作业位于顶部,因为我们可以使用更少的辅助空间来完成它们;可以看出,如果你这样做,你的作业堆栈最多会有 O(log n) 的大小。Job

如果您可以自由地实现另一种排序算法,我会推荐合并排序,我认为这是最简单的有效排序算法(假设您被允许使用线性辅助空间)或堆排序(它是就地的,也实现了 O(n log n) 时间复杂度,但仍然非常简单)。