【问题标题】:Quicksort recursion depth Stack space of O(n) doesnot cause stackoverflow?Quicksort递归深度O(n)的堆栈空间不会导致stackoverflow?
【发布时间】:2012-10-19 08:55:02
【问题描述】:

在最坏的情况下,快速排序递归深度需要 O(n) 的堆栈空间。为什么在最坏的情况下它不会导致大型集合的堆栈溢出? (倒序)

【问题讨论】:

  • 使用第一个元素作为枢轴的快速排序实现是一个糟糕的实现。请注意,更好的方法是随机取一个元素或选择中间元素作为枢轴。
  • @amit:将中间元素作为枢轴也很糟糕,只是不那么糟糕,因为它更难构建杀手级输入。
  • @SteveJessop:我不同意。请注意,随机数组与任何选择都具有完全相同的“最坏情况”变化 - 作为随机选择的元素。选择第一个元素的问题是 - 对现实生活中的数组进行排序(或几乎排序)的概率不是 1/n! (您见过多少次使用已经排序的数组的代码?太多.. .),虽然我怀疑选择中间元素时最坏情况数组的概率会比1/n!(如果有的话)大得多。也就是说——如果你害怕攻击——那么应该避免任意选择。
  • 可能我们之间的区别只是我认为默认情况下我应该害怕攻击,而您认为默认情况下不应该。我们甚至可能都是正确的,这仅取决于您正在编写的上下文,但我确实认为,对于像排序这样的通用实用程序操作,能够排除可能发生的攻击是非常罕见的。

标签: performance algorithm sorting data-structures quicksort


【解决方案1】:

如果您在枢轴的两侧进行递归,那么在最坏的情况下,对于足够大的数据,它确实会导致堆栈溢出。这就是为什么没有人在生产代码中使用天真的 QuickSort。

您可以对算法进行一个简单的更改,以防止Omega(n) 最坏情况下的堆栈使用。在每个分区之后,递归地对“小半部分”进行快速排序,然后迭代循环以完成“大半部分”。这需要O(log n) 额外的堆栈空间。如果你愿意,你可以通过再次修改它使其成为O(1) 堆栈空间和O(log n) 额外的非堆栈空间。将“大半部分”推到预先分配的数组(或您选择的其他后进先出数据结构)的末尾,循环执行“小半部分”,当您击中底部时弹出最后一个元素从数据结构中取出来做下一步。

您可以进行进一步的更改以避免Omega(n^2) 最坏情况运行时。但它不再是单纯的 QuickSort,而是 QuickSort-with-median-of-medians-pivot-selection,或 Introsort,或其他。

【讨论】:

  • QuickSort-with-median-of-medians-pivot-selection 仍然是快速排序,并且对于某些输入仍然表现出相同的最坏情况行为。
  • @Konrad:不是这样。中位数的中位数是一种真正的线性时间选择算法,它的枢轴选择算法(这是您在快速排序中使用的)在数据的第 30 个和第 70 个百分位数之间找到一个枢轴,这足以避免最坏的情况快速排序和快速选择。您是否在考虑“3 的中位数”,尽管需要一些棘手的构造,但它确实有致命的案例?
  • 你当然是对的。我将其解读为三中位数的中位数,这是一种不同的(恒定时间)方法。
  • @Konrad:我认为我第一次和最后一次编写实际使用的快速排序(c. 2001)时使用的参考文献中至少有一个称为伪中位数 9。也许是为了避免这种误读。
【解决方案2】:

那么为什么在最坏的情况下它不会导致大量有序数据的堆栈溢出

确实如此。为什么你会认为它没有?

(但请注意,快速排序的最坏情况输入取决于您选择的枢轴。它通常不是相反的序列 - 事实上,对于枢轴的幼稚选择,另一个最坏情况输入是已经排序的序列,它不会'不必颠倒。)

但是现在排序算法的库实现实际上很少是快速排序,正是因为这个原因。例如,C++ std::sort 使用 introsort 代替,这是一种修改过的快速排序,一旦递归太深就会切换到不同的排序算法。

【讨论】:

    【解决方案3】:

    在顺序相反的情况下(最坏的情况)可能会导致堆栈溢出。因为数组对于内部函数堆栈来说可能太大了。 (例如 99,000,000 个元素)您可以将递归替换为 stack 数据结构。你会循环while (stck.empty() == false)

    function quicksort(arr[0,1,...n-1])
         stack.push(pair(0, n - 1))
         while stack not empty
              interval = stack.pop()
              ...
              ...
              stack.push(interval(0, pivot - 1))
              stack.push(interval(pivot + 1, n - 1))
    

    【讨论】:

    • 这不是问题所在。虽然它阻止了堆栈溢出,但它并没有改善 O(n) 的空间消耗,这是一个严重的问题。
    • @delnanO(n)堆栈空间的问题高度依赖于排序情况。它在更好的情况和更坏的情况之间有所不同
    • @delnan:这正是的问题,这完全是关于堆栈溢出的问题。或许提问者也应该关心非堆栈的额外空间需求,但实际上并没有问到。
    • @SteveJessop 问题问“为什么在最坏的情况下它不会导致大型集合的堆栈溢出?(逆序)”。虽然这似乎是一个有缺陷的前提,但答案至少应该解决它。
    猜你喜欢
    • 2013-12-13
    • 2010-10-26
    • 2019-12-21
    • 1970-01-01
    • 2021-01-26
    • 1970-01-01
    • 1970-01-01
    • 2018-10-29
    • 1970-01-01
    相关资源
    最近更新 更多