快速排序算法什么时候需要 O(n^2) 的时间复杂度?
快速排序通过选取一个基准值,将所有比该基准值小的元素放在一边,将所有比该基准值大的元素放在另一边;然后以相同方式递归地对这两个子组进行排序(一直到全部排序完成)。如果每次选择最差的基准值(即列表中的最高或最低元素),那么你只需要对一个组进行排序,该组除了你选择的原始基准值之外的所有元素都在其中。这实质上让你得到n个需要遍历n次的组,因此具有O(n^2)复杂度。
导致这种情况发生的最常见原因是在快速排序实现中选择第一个或最后一个元素作为基准值。对于未排序的列表,这与其他任何方法一样有效,但对于已排序或近乎排序的列表(在实践中很常见),这很可能会给你带来最坏的情况。这就是为什么所有合格的实现通常会从列表中心选择基准值的原因。
有一些修改标准快速排序算法以避免这种情况的方法 - 其中一个例子是双轴快速排序,它被集成到Java 7中。
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
针对已排序数组的最坏情况,有一种可能的解决方案是使用中心元素(如果序列长度为偶数,则略微向左移动)。然后,最坏情况会变得更加奇特。它可以通过修改快速排序算法来构造,以将当前选定的枢轴元素对应的数组元素设置为单调递增的值。即,我们知道第一个枢轴是中心,所以中心必须是最低值,例如0。接下来,它被交换到最左边,即最左边的值现在位于中心,并且将成为下一个枢轴元素,因此它必须是1。现在,我们可以猜测数组将如下所示:
1 ? ? 0 ? ? ?
// 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个元素增加1个数字。在第二行中,对每4个元素执行相同的操作,在第三行中,将数字分配给每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
对于中间元素和三数中值,最坏情况下的序列已经相当随机了,但为了使快速排序更加健壮,可以随机选择枢轴元素。如果所使用的随机序列在每次快速排序运行时都是可重复的,那么我们也可以构造出针对该序列的最坏情况序列。我们只需要调整第一个程序中的行,例如:
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
当选择最小或最大数作为轴点时,会触发时间复杂度为O(n2)的最坏情况。
我认为如果数组是倒序的,那么选择最后一个元素作为枢轴将会是最坏情况。
导致快速排序最坏情况的因素如下:
n-1
个元素时,最坏情况会发生。换句话说,当快速排序接收到一个已排序的数组(按降序排列)时,最坏情况的运行时间为O(n^2)。