所有元素都相同时的快速排序复杂度?
Posted
技术标签:
【中文标题】所有元素都相同时的快速排序复杂度?【英文标题】:Quicksort complexity when all the elements are same? 【发布时间】:2011-02-26 11:23:13 【问题描述】:我有一个包含 N 个相同数字的数组。我正在对其应用快速排序。 这种情况下排序的时间复杂度应该是多少。
我仔细研究了这个问题,但没有得到确切的解释。
任何帮助将不胜感激。
【问题讨论】:
【参考方案1】:这取决于快速排序的实现。划分为 2 个(<
和 >=
)部分的传统实现将在相同的输入上具有 O(n*n)
。虽然不一定会发生 交换,但它会导致进行 n
递归调用 - 每个调用都需要与枢轴和 n-recursionDepth
元素进行比较。即O(n*n)
需要进行比较
但是有一个简单的变体,它分为 3 个集合(<
、=
和 >
)。在这种情况下,此变体具有 O(n)
性能 - 而不是选择枢轴,交换然后在 0
到 pivotIndex-1
和 pivotIndex+1
到 n
上递归,它将交换所有等于枢轴的事物到“中间”分区(在所有相同输入的情况下总是意味着与自身交换,即无操作)意味着调用堆栈在这种特殊情况下只有 1 深 n 比较并且不会发生交换。我相信这个变种至少已经进入了 linux 上的标准库。
【讨论】:
我认为 Hoare 的原始分区创建了<=
和 >=
部分,在它们之间平均分配相等的值。这在平均情况下(不同数据)没有成本,并且在数据相等的情况下保证 O(N log N) 时间
现在第二种方法是否包含在 linux 库以外的其他库中?【参考方案2】:
快速排序的性能取决于枢轴选择。选择的枢轴越接近中值元素,快速排序的性能就越好。
在这种特定情况下,您很幸运 - 您选择的枢轴将始终是 a 中位数,因为所有值都相同。因此,快速排序的分区步骤将永远不必交换元素,并且两个指针将恰好在中间相遇。因此,这两个子问题的大小正好是一半——给你一个完美的O(n log n)
。
更具体地说,这取决于分区步骤的实施情况。循环不变式只需要确保较小的元素在左侧子问题中,而较大的元素在右侧子问题中。不能保证分区实现永远不会交换相等的元素。但这总是不必要的工作,所以没有聪明的实现应该这样做:left
和 right
指针永远不会检测到相应枢轴的反转(即你永远不会遇到*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 是的,我认为这里没有人用漂亮而简单的英语给出答案。所以,我试了一下。我是新手,我可以删除这个答案吗?以上是关于所有元素都相同时的快速排序复杂度?的主要内容,如果未能解决你的问题,请参考以下文章