快速排序算法何时需要O(n ^ 2)时间?
答案 0 :(得分:21)
Quicksort通过一个枢轴工作,然后将所有元素放在一侧低于该枢轴而另一侧放置所有较高元素;然后它以相同的方式递归地对两个子组进行排序(一直向下直到所有内容都被排序。)现在,如果你每次选择最差的数据(列表中的最高或最低元素),你将只有一个组排序,除了您选择的原始枢轴之外,该组中的所有内容。这实际上为您提供了n个组,每个组需要迭代n次,因此O(n ^ 2)复杂度。
发生这种情况的最常见原因是,如果将枢轴选择为快速排序实现中列表中的第一个或最后一个元素。对于未排序的列表,这与任何其他列表一样有效,但是对于已排序或接近排序的列表(实际上很常见),这很可能会给您最坏的情况。这就是为什么所有半正式的实现都倾向于从列表中心转移。
对标准快速排序算法进行了修改以避免出现这种边缘情况 - 一个例子是dual-pivot quicksort that was integrated into Java 7。
答案 1 :(得分:11)
总之,用于排序数组最低元素的Quicksort首先如下所示:
通常你会使用一个pivot元素,在两个同样长的子序列中对序列进行分区。
现在有不同的方案可供选择枢轴元素。 Early version只占用了最左边的元素。然而,在最坏的情况下,枢轴元件将永远是w.l.o.g.最低的元素。
在这种情况下,可以很容易地想到,最坏的情况是已经增加的排序数组:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
同样,在选择最右边的元素时,最坏的情况将是递减的序列。
20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
一种可能的补救措施是使用中心元素(如果序列长度均匀,则使用中心稍微左侧)。在这种情况下,最糟糕的情况将是更加异国情调。它可以通过修改Quicksort算法来构造,以将对应于当前选择的枢轴元素的数组元素设置为单调递增值。即我们知道第一个枢轴是中心,因此中心必须是最低值,例如0.接下来它被交换到最左边,即最左边的值现在在中心,并且将是下一个pivot元素,所以它必须是1,所以我们已经猜到了,数组看起来像:
1 ? ? 0 ? ? ?
我们可以让改进的Quicksort完成其余的工作。这是C ++代码:
// g++ -std=c++11 worstCaseQuicksort.cpp && ./a.out
#include <algorithm> // swap
#include <iostream>
#include <vector>
#include <numeric> // iota
int main( void )
{
std::vector<int> v(20); /**< will hold the worst case later */
/* p basically saves the indices of what was the initial position of the
* elements of v. As they get swapped around by Quicksort p becomes a
* permutation */
auto p = v;
std::iota( p.begin(), p.end(), 0 );
/* in the worst case we need to work on v.size( sequences, because
* the initial sequence is always split after the first element */
for ( auto i = 0u; i < v.size(); ++i )
{
/* i can be interpreted as:
* - subsequence starting index
* - current minimum value, if we start at 0 */
/* note thate in the last step iPivot == v.size()-1 */
auto const iPivot = ( v.size()-1 + i )/2;
v[ p[ iPivot ] ] = i;
std::swap( p[ iPivot ], p[i] );
}
for ( auto x : v ) std::cout << " " << x;
}
我们得到了这些最坏的情况序列:
0
0 1
1 0 2
2 0 1 3
1 3 0 2 4
4 2 0 1 3 5
1 5 3 0 2 4 6
4 2 6 0 1 3 5 7
1 5 3 7 0 2 4 6 8
8 2 6 4 0 1 3 5 7 9
1 9 3 7 5 0 2 4 6 8 10
6 2 10 4 8 0 1 3 5 7 9 11
1 7 3 11 5 9 0 2 4 6 8 10 12
10 2 8 4 12 6 0 1 3 5 7 9 11 13
1 11 3 9 5 13 7 0 2 4 6 8 10 12 14
8 2 12 4 10 6 14 0 1 3 5 7 9 11 13 15
1 9 3 13 5 11 7 15 0 2 4 6 8 10 12 14 16
16 2 10 4 14 6 12 8 0 1 3 5 7 9 11 13 15 17
1 17 3 11 5 15 7 13 9 0 2 4 6 8 10 12 14 16 18
10 2 18 4 12 6 16 8 14 0 1 3 5 7 9 11 13 15 17 19
1 11 3 19 5 13 7 17 9 15 0 2 4 6 8 10 12 14 16 18 20
16 2 12 4 20 6 14 8 18 10 0 1 3 5 7 9 11 13 15 17 19 21
1 17 3 13 5 21 7 15 9 19 11 0 2 4 6 8 10 12 14 16 18 20 22
12 2 18 4 14 6 22 8 16 10 20 0 1 3 5 7 9 11 13 15 17 19 21 23
1 13 3 19 5 15 7 23 9 17 11 21 0 2 4 6 8 10 12 14 16 18 20 22 24
这有秩序。右边只是从零开始的两个增量。左侧也有订单。让我们使用Ascii艺术很好地格式化73元素长最坏情况序列的左侧:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
------------------------------------------------------------------------------------------------------------
1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35
37 39 41 43 45 47 49 51 53
55 57 59 61 63
65 67
69
71
标题是元素索引。在第一行中,从1开始并且增加2的数字被给予每个第2个元素。在第二行中,对每个第4个元素进行相同的操作,在第3行中,将数字分配给每个第8个元素,依此类推。在这种情况下,要写入第i行的第一个值是索引2 ^ i-1,但是对于某些长度,这看起来有点不同。
结果结构让人想起倒置的二叉树,其节点从叶子开始自下而上标记。
另一种方法是使用最左边,中间和最右边元素的中值。在这种情况下,最坏的情况只能是,w.l.o.g。左子序列的长度为2(不仅仅是上面例子中的长度1)。我们还假设最右边的值始终是三个中值的最高值。这也意味着它是所有价值中最高的。在上面的程序中进行调整,我们现在有了这个:
auto p = v;
std::iota( p.begin(), p.end(), 0 );
auto i = 0u;
for ( ; i < v.size(); i+=2 )
{
auto const iPivot0 = i;
auto const iPivot1 = ( i + v.size()-1 )/2;
v[ p[ iPivot1 ] ] = i+1;
v[ p[ iPivot0 ] ] = i;
std::swap( p[ iPivot1 ], p[i+1] );
}
if ( v.size() > 0 && i == v.size() )
v[ v.size()-1 ] = i-1;
生成的序列是:
0
0 1
0 1 2
0 1 2 3
0 2 1 3 4
0 2 1 3 4 5
0 4 2 1 3 5 6
0 4 2 1 3 5 6 7
0 4 2 6 1 3 5 7 8
0 4 2 6 1 3 5 7 8 9
0 8 2 6 4 1 3 5 7 9 10
0 8 2 6 4 1 3 5 7 9 10 11
0 6 2 10 4 8 1 3 5 7 9 11 12
0 6 2 10 4 8 1 3 5 7 9 11 12 13
0 10 2 8 4 12 6 1 3 5 7 9 11 13 14
0 10 2 8 4 12 6 1 3 5 7 9 11 13 14 15
0 8 2 12 4 10 6 14 1 3 5 7 9 11 13 15 16
0 8 2 12 4 10 6 14 1 3 5 7 9 11 13 15 16 17
0 16 2 10 4 14 6 12 8 1 3 5 7 9 11 13 15 17 18
0 16 2 10 4 14 6 12 8 1 3 5 7 9 11 13 15 17 18 19
0 10 2 18 4 12 6 16 8 14 1 3 5 7 9 11 13 15 17 19 20
0 10 2 18 4 12 6 16 8 14 1 3 5 7 9 11 13 15 17 19 20 21
0 16 2 12 4 20 6 14 8 18 10 1 3 5 7 9 11 13 15 17 19 21 22
0 16 2 12 4 20 6 14 8 18 10 1 3 5 7 9 11 13 15 17 19 21 22 23
0 12 2 18 4 14 6 22 8 16 10 20 1 3 5 7 9 11 13 15 17 19 21 23 24
中心元素和三个中值的最坏情况序列看起来已经非常随机,但为了使Quicksort更加健壮,可以随机选择枢轴元素。如果在每次Quicksort运行中使用的随机序列至少可重现,那么我们也可以为此构建最坏情况序列。我们只需调整第一个程序中的iPivot =
行,例如到:
srand(0); // you shouldn't use 0 as a seed
for ( auto i = 0u; i < v.size(); ++i )
{
auto const iPivot = i + rand() % ( v.size() - i );
生成的序列是:
0
1 0
1 0 2
2 3 1 0
1 4 2 0 3
5 0 1 2 3 4
6 0 5 4 2 1 3
7 2 4 3 6 1 5 0
4 0 3 6 2 8 7 1 5
2 3 6 0 8 5 9 7 1 4
3 6 2 5 7 4 0 1 8 10 9
8 11 7 6 10 4 9 0 5 2 3 1
0 12 3 10 6 8 11 7 2 4 9 1 5
9 0 8 10 11 3 12 4 6 7 1 2 5 13
2 4 14 5 9 1 12 6 13 8 3 7 10 0 11
3 15 1 13 5 8 9 0 10 4 7 2 6 11 12 14
11 16 8 9 10 4 6 1 3 7 0 12 5 14 2 15 13
6 0 15 7 11 4 5 14 13 17 9 2 10 3 12 16 1 8
8 14 0 12 18 13 3 7 5 17 9 2 4 15 11 10 16 1 6
3 6 16 0 11 4 15 9 13 19 7 2 10 17 12 5 1 8 18 14
6 0 14 9 15 2 8 1 11 7 3 19 18 16 20 17 13 12 10 4 5
14 16 7 9 8 1 3 21 5 4 12 17 10 19 18 15 6 0 11 2 13 20
1 2 22 11 16 9 10 14 12 6 17 0 5 20 4 21 19 8 3 7 18 15 13
22 1 15 18 8 19 13 0 14 23 9 12 10 5 11 21 6 4 17 2 16 7 3 20
2 19 17 6 10 13 11 8 0 16 12 22 4 18 15 20 3 24 21 7 5 14 9 1 23
答案 2 :(得分:10)
获得一个等于最低或最高数字的枢轴,也应该触发O(n 2 )的最坏情况。
答案 3 :(得分:4)
quicksort的不同实现具有不同的数据集,以使其具有最差的运行时。这取决于算法选择它的pivot-element的位置。
而且正如Ghpst所说,选择最大或最小的数字会给你一个最坏的情况。
如果我没记错的话,快速排序通常会使用随机元素进行转轴,以尽量减少出现最坏情况的可能性。
答案 4 :(得分:1)
我认为如果数组的顺序相反,那么转动该数组的最后一个元素将是最糟糕的情况
答案 5 :(得分:0)
造成快速排序最坏情况的因素如下:
n-1
个元素时。换句话说,快速排序的最坏情况运行时间发生在Quicksort接受排序数组(按递减顺序)时,是O(n ^ 2)的时间复杂度。