【问题标题】:Quicksort complexity when all the elements are same?所有元素都相同时的快速排序复杂度?
【发布时间】:2011-02-26 11:23:13
【问题描述】:

我有一个包含 N 个相同数字的数组。我正在对其应用快速排序。 这种情况下排序的时间复杂度应该是多少。

我仔细研究了这个问题,但没有得到确切的解释。

任何帮助将不胜感激。

【问题讨论】:

    标签: c algorithm complexity-theory


    【解决方案1】:

    这取决于快速排序的实现。划分为 2 个(<>=)部分的传统实现将在相同的输入上具有 O(n*n)。虽然不一定会发生 交换,但它会导致进行 n 递归调用 - 每个调用都需要与枢轴和 n-recursionDepth 元素进行比较。即O(n*n)需要进行比较

    但是有一个简单的变体,它分为​​ 3 个集合(<=>)。在这种情况下,此变体具有 O(n) 性能 - 而不是选择枢轴,交换然后在 0pivotIndex-1pivotIndex+1n 上递归,它将交换所有等于枢轴的事物到“中间”分区(在所有相同输入的情况下总是意味着与自身交换,即无操作)意味着调用堆栈在这种特殊情况下只有 1 深 n 比较并且不会发生交换。我相信这个变种至少已经进入了 linux 上的标准库。

    【讨论】:

    • 我认为 Hoare 的原始分区创建了 <=>= 部分,在它们之间平均分配相等的值。这在平均情况下(不同数据)没有成本,并且在数据相等的情况下保证 O(N log N) 时间
    • 现在第二种方法是否包含在 linux 库以外的其他库中?
    【解决方案2】:

    快速排序的性能取决于枢轴选择。选择的枢轴越接近中值元素,快速排序的性能就越好。

    在这种特定情况下,您很幸运 - 您选择的枢轴将始终是 a 中位数,因为所有值都相同。因此,快速排序的分区步骤将永远不必交换元素,并且两个指针将恰好在中间相遇。因此,这两个子问题的大小正好是一半——给你一个完美的O(n log n)

    更具体地说,这取决于分区步骤的实施情​​况。循环不变式只需要确保较小的元素在左侧子问题中,而较大的元素在右侧子问题中。不能保证分区实现永远不会交换相等的元素。但这总是不必要的工作,所以没有聪明的实现应该这样做:leftright 指针永远不会检测到相应枢轴的反转(即你永远不会遇到*left > pivot && *right < pivot 的情况),所以@987654325 @指针会递增,right指针会每一步递减,最终在中间相遇,产生大小为n/2的子问题。

    【讨论】:

    • 因为在他所说的特定情况下,大多数 QuickSort 实现实际上都具有 n*n 性能 - 所有元素都相同。
    • 因为它们通常基于<>= 进行分区,所以虽然不会发生交换,但它会递归n*n 次,并且每次都递归,但仍然会导致n*n 的性能
    • @tobyodavies:我相信在正确实施快速排序时不会。您必须向我展示一个没有的流行实现。例如,快速排序的 VS2010 实现(它是用于 std::sort 的 introsort 的“一部分”)甚至为它选择的枢轴建立了一个“相等的范围”,并且在这种特定情况下会给出线性复杂度。
    • 我认为真正的快速排序没有单一的严格定义。它更像是一个算法模板。它基本上是:选择枢轴,分区,递归子问题。如果您正确实施所有步骤(从某种意义上说,复杂性不会不必要地上升),那么这种情况下的复杂性将不是二次的(理论上 实际上)。例如,如果你只实现<>=,你甚至可以在等于元素的情况下获得无限递归。 (所有元素将始终在右侧,子问题永远不会缩小)。
    • @ltjax,2-partition 变体总是缩小,因为实际的支点在分区后保证在正确的位置,所以分区总是每次调用至少缩小 1。还有 Hoare 所描述的原始版本,在我看过的所有教科书之一中都教授过,它没有执行此优化。
    【解决方案3】:

    这取决于特定的实现。

    如果只有一种比较(≤或n em>2) 性能,因为每一步问题大小只会减少 1。

    算法listed here 是一个示例(所附插图适用于不同的算法)。

    如果有两种比较,例如 用于右侧元素,就像双指针实现中的情况一样,逐步移动指针,那么您可能会获得完美的 O(n log n) 性能,因为一半相等的元素将在两个分区中平均分割。

    上面链接中的插图使用了一种不会逐步移动指针的算法,因此您的性能仍然很差(查看“少数独特”的情况)。

    所以这取决于你在实现算法时是否考虑到这种特殊情况。

    实际的实现通常处理更广泛的特殊情况:如果在分区步骤中没有交换,他们假设数据几乎是排序的,并使用插入排序,这给出了更好的 O(n) 在所有相等元素的情况下。

    【讨论】:

    • 如果你使用双指针方法,在这种情况下你会得到 O(n) 而不是 O(n log n) - 每个指针都会递增直到结束,最大 n 比较
    • @tobyodavies - 我想您通常仍会递归到每个分区。
    • 第一次调用后只有一个分区 - = 分区
    • @tobydavies – 我指的是双向比较快速排序( 比较在不同元素上进行:分别位于左右指针下方)。在您描述的 three-way-comparing 排序中,当然是 O(n)。
    • 我不确定我是否可以看到检查 <>(而不是 <>=)的 QS 不会分成 3 - 如果你正在做<>=s 必须去某个地方...
    【解决方案4】:

    tobyodavies 提供了正确的解决方案。当所有键都相等时,它确实会处理这种情况并在 O(n) 时间内完成。 这和我们在荷兰国旗问题中所做的划分是一样的

    http://en.wikipedia.org/wiki/Dutch_national_flag_problem

    分享普林斯顿的代码

    http://algs4.cs.princeton.edu/23quicksort/Quick3way.java.html

    【讨论】:

      【解决方案5】:

      如果您实施 2 路分区算法,那么在每一步中,数组都会减半。这是因为当遇到相同的键时,扫描会停止。因此,在每个步骤中,分区元素将位于子数组的中心,从而在每个后续递归调用中将数组减半。现在,这种情况类似于使用 ~N lg N 比较对 N 个元素的数组进行排序的合并排序情况。因此,对于重复键,Quicksort 的传统 2 路分区算法使用 ~N lg N 比较,因此遵循线性方法。

      【讨论】:

        【解决方案6】:

        快速排序代码是使用“分区”和“快速排序”函数完成的。

        基本上,有两种实现快速排序的最佳方法。

        这两者的区别只是“分区”功能,

        1.洛穆托

        2.霍尔

        使用诸如上述 Lomuto 分区方案的分区算法(即使是选择好的主元值),快速排序对于包含许多重复元素的输入表现出较差的性能。当所有输入元素都相等时,问题就很明显了:在每次递归时,左分区为空(没有输入值小于枢轴),而右分区仅减少了一个元素(枢轴被移除)。因此,Lomuto 分区方案需要二次时间来对相等值的数组进行排序。

        因此,使用 Lomuto 分区算法需要 O(n^2) 时间。

        通过使用 Hoare 分区算法,我们得到了所有数组元素相等的最佳情况。时间复杂度为 O(n)。

        参考:https://en.wikipedia.org/wiki/Quicksort

        【讨论】:

        • 您没有错,但您的努力将更有效地用于解决尚未有好的答案的新问题。
        • @Blastfurnace 是的,我认为这里没有人用漂亮而简单的英语给出答案。所以,我试了一下。我是新手,我可以删除这个答案吗?
        猜你喜欢
        • 2012-07-06
        • 1970-01-01
        • 2016-07-23
        • 1970-01-01
        • 2013-04-26
        • 2020-08-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多