1算法介绍编辑
![快排图 快排图](http://img.e-com-net.com/image/info8/bd8ea0adee4f4ac1aaeea3b4e008d30e.gif)
快排图
一趟快速排序的算法是:
1)设置两个 变量i、j, 排序开始的时候:i=0,j=N-1;
2)以第一个 数组元素作为关键数据,赋值给
key,即
key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于
key的值A[j],将A[j]赋给A[i];
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于
key的A[i],将A[i]赋给A[j];
5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于
key,4中A[j]不大于
key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
2排序演示编辑
假设用户输入了如下数组:
下标
|
0
|
1
|
2
|
3
|
4
|
5
|
数据
|
6
|
5
|
7
|
3
|
8
|
9
|
创建 变量i=0(指向第一个数据), j=5(指向最后一个数据), k=6( 赋值为第一个数据的值)。
我们取走了下标0的数据,于是,我们需要找到一个数字来替换他。由于我们要把所有比6小的数移动到左面,所以我们可以开始寻找比6小的数并从右往左找。别急,我们要按顺序找哦。不断递减j的值,我们发现下标3的数据比6小,于是把3移到下标0(实际是i指向的位置。代码中要用i,因为后面还会循环这个步骤,不用i的话第二次循环就会出问题。),数组和变量变成了以下状态:
下标
|
0
|
1
|
2
|
3
|
4
|
5
|
数据
|
3
|
5
|
7
|
6
|
8
|
9
|
i=0 j=3 k=6
由于变量k已经储存了下标0的数据,所以我们可以放心的把下标0覆盖了。如此一来,下标3虽然有数据,但是相当于没有了,因为数据已经复制到别的地方了。于是我们再找一个数据来替换他。这次要变成找比k大的了,而且要从前往后找了。递加变量i,发现下标2是第一个比k大的,于是用下标2的数据7替换j指向的下标3的数据,数据状态变成下表:
下标
|
0
|
1
|
2
|
3
|
4
|
5
|
数据
|
3
|
5
|
6
|
7
|
8
|
9
|
i=2 j=3 k=6
重复上面的步骤,递减变量j。这时,我们发现i和j“碰头”了:他们都指向了下标2。于是,循环结束,把k填回下标2里,即得到结果。
如果i和j没有碰头的话,就递加i找大的,还没有,就再递减j找小的,如此反复,不断循环。注意判断和寻找是同时进行的。
注意:这里的数据给的不太好,实际上这样排序以后不会得到最终结果,只会把比k大和比k小的数分到k的两边。(你可以想象一下i和j是两个机器人,数据就是大小不一的石头,先取走i前面的石头留出回旋的空间,然后他们轮流分别挑选比k大和比k小的石头扔给对面,最后在他们中间把取走的那块石头放回去,于是比这块石头大的全扔给了j那一边,小的全扔给了i那一边。只是这次运气好,扔完一次刚好排整齐。)为了得到最后结果,需要再次对下标2两边的数组分别执行此步骤,然后再分解数组,直到数组不能再分解为止(只有一个数据),才能得到正确结果。
3示例代码编辑
Erlang语言
1
2
3
4
5
6
|
超简短实现:
q_sort([])->
[];
q_sort([H|R])->
q_sort([X||X<-R,X q_sort([X||X<-R,X>=H]).
|
C++语言
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
#include
usingnamespacestd;
//从小到大
intpartition(inta[],intp,intr){
intx=a[r];
//通常,拿最后一个值,作为预期的中间值
intmiddle=p;
//记录“较小的一段数据”的最大下标。通常这个值在p和r的中间,故起名middle
for
(intj=p;j if
(a[j] {
if
(j!=middle)
{
inttemp=a[middle];
a[middle]=a[j];
a[j]=temp;
}
middle++;
}
}
inttemp=a[r];
a[r]=a[middle];
a[middle]=temp;
returnmiddle;
}
voidQuickSort(inta[],intp,intr){
if
(p intq=partition(a,p,r);
QuickSort(a,p,q-1);
QuickSort(a,q+1,r);
}
}
intmain(){
intarray[]={0,-2,11,-4,13,-5,14,-43};
QuickSort(array,0,7);
for
(inti=0;i<=7;i++)
cout< cout< return0;
}
C语言版本
voidQuickSort(inta[],intnumsize)
//a是整形数组,numsize是元素个数
{
inti=0,j=numsize-1;
intval=a[0];
//指定参考值val大小
if
(numsize>1)
//确保数组长度至少为2,否则无需排序
{
while
(i {
for
(;j>i;j--)
//从后向前搜索比val小的元素,找到后填到a[i]中并跳出循环
if
(a[j] {
a[i]=a[j];
break
;
}
for
(;i if
(a[i]>val)
{
a[j]=a[i];
break
;
}
}
a[i]=val;
//将保存在val中的数放到a[i]中
QuickSort(a,i);
//递归,对前i个数排序
QuickSort(a+i+1,numsize-1-i);
//对i+1到numsize这numsize-1-i个数排序
}
}
|
Java
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
publicstaticvoidquickSort(inta[],intstart,intend){
inti,j;
i=start;
j=end;
if
((a==
null
)||(a.length==
0
))
return
;
while
(i while
(i j--;
}
if
(i inttemp=a[i];
a[i]=a[j];
a[j]=temp;
}
while
(i i++;
}
if
(i inttemp=a[i];
a[i]=a[j];
a[j]=temp;
}
}
if
(i-start>
1
){
//递归调用,把key前面的完成排序
quickSort(a,start,i-
1
);
}
if
(end-i>
1
){
quickSort(a,i+
1
,end);
//递归调用,把key后面的完成排序
}
}
////////////////////////////方式二////////////////////////////////
更有效率点的代码:
public
intstart,intend){
inti=start+
1
,j=end;
Tkey=targetArr[start];
SortUtil
if
(start>=end){
returntargetArr;
}
/*从i++和j--两个方向搜索不满足条件的值并交换*
*条件为:i++方向小于key,j--方向大于key*/
while
(
true
){
while
(targetArr[j].compareTo(key)>
0
){
j--;
}
while
(targetArr[i].compareTo(key)<
0
&&i i++;
}
if
(i>=j){
break
;
}
sUtil.swap(targetArr,i,j);
if
(targetArr[i]==key){
j--;
}
else
{
i++;
}
}
/*关键数据放到‘中间’*/
sUtil.swap(targetArr,start,j);
if
(start this
.quickSort(targetArr,start,i-
1
);
}
if
(j+
1
this
.quickSort(targetArr,j+
1
,end);
}
returntargetArr;
}
////////////////方式三:减少交换次数,提高效率/////////////////////
private
intstart,intend){
inti=start,j=end;
Tkey=targetArr[start];
while
(i //按j--方向遍历目标数组,直到比key小的值为止
while
(j>i&&targetArr[j].compareTo(key)>=
0
){
j--;
}
if
(i //targetArr[i]已经保存在key中,可将后面的数填入
targetArr[i]=targetArr[j];
}
//按i++方向遍历目标数组,直到比key大的值为止
while
(i i++;
}
if
(i //targetArr[j]已保存在targetArr[i]中,可将前面的值填入
targetArr[j]=targetArr[i];
}
}
//此时i==j
targetArr[i]=key;
if
(i-start>
1
){
//递归调用,把key前面的完成排序
this
.quickSort(targetArr,start,i-
1
);
}
if
(end-j>
1
){
//递归调用,把key后面的完成排序
this
.quickSort(targetArr,j+
1
,end);
}
}
|
C#
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
36
37
38
39
40
41
42
|
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Text;
namespacetest
{
classProgram
{
staticvoidMain(
string
[]args)
{
int
[]array={49,38,65,97,76,13,27};
sort(array,0,array.Length-1);
Console.ReadLine();
}
/**一次排序单元,完成此方法,key左边都比key小,key右边都比key大。
*@paramarray排序数组
*@paramlow排序起始位置
*@paramhigh排序结束位置
*@return单元排序后的数组*/
privatestaticintsortUnit(
int
[]array,intlow,inthigh)
{
intkey=array[low];
while
(low {
//从后向前搜索比key小的值
while
(array[high]>=key&&high>low)
--high;
//比key小的放左边
array[low]=array[high];
//从前向后搜索比key大的值,比key大的放右边
while
(array[low]<=key&&high>low)
++low;
//比key大的放右边
array[high]=array[low];
}
//左边都比key小,右边都比key大。//将key放在游标当前位置。//此时low等于high
array[low]=key;
Console.WriteLine(
string
.Join(
","
,array));
returnhigh;
}
/**快速排序*@paramarry*@return*/
publicstaticvoidsort(
int
[]array,intlow,inthigh)
{
if
(low>=high)
return
;
//完成一次单元排序
intindex=sortUnit(array,low,high);
//对左边单元进行排序
sort(array,low,index-1);
//对右边单元进行排序
sort(array,index+1,high);}
}
}
|
运行结果:27 38 13 49 76 97 65快速排序就是 递归调用此过程——在以49为中点分割这个数据序列,分别对前面一部分和后面一部分进行类似的快速排序,从而完成全部数据序列的快速排序,最后把此数据序列变成一个有序的序列,根据这种思想对于上述 数组A的快速排序的全过程如图6所示:初始状态 {49 38 65 97 76 13 27} 进行一次快速排序之后划分为 {27 38 13} 49 {76 97 65} 分别对前后两部分进行快速排序{27 38 13} 经第三步和第四步交换后变成 {13 27 38} 完成排序。{76 97 65} 经第三步和第四步交换后变成 {65 76 97} 完成排序。图示
PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
functionquickSort(
$arr
){
if
(
count
(
$arr
)>1){
$k
=
$arr
[0];
$x
=
array
();
$y
=
array
();
$_size
=
count
(
$arr
);
for
(
$i
=1;
$i
<
$_size
;
$i
++){
if
(
$arr
[
$i
]<=
$k
){
$x
[]=
$arr
[
$i
];
}
else
{
$y
[]=
$arr
[
$i
];
}
}
$x
=quickSort(
$x
);
$y
=quickSort(
$y
);
returnarray_merge(
$x
,
array
(
$k
),
$y
);
}
else
{
return
$arr
;
}
}
|
pascal
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
procedureqsort(l,h:integer);
//假设被排序的数组是a,且快排后按升序排列)
vari,j,t,m:integer;
begin
i:=l;
j:=h;
//(l,h表示快排的左右区间)
m:=a[(i+j)div2];
//注意:本句不能写成:m:=(i+j)div2;
repeat
whilea[i] whilem
ifi<=jthen
//注意,是’<=';
begin
t:=a[i];
a[i]:=a[j];
a[j]:=t;
inc(i);
dec(j);
end;
untili>j;
//注意,是大于号,不是‘>=’;
ifj>lthenqsort(l,j);
ifi end;
javascript:
varsort=(
function
(){
varquickSort={
partition:
function
(array,low,high){
if
(low>=high){
return
;
}
varkey=array[high];
varmiddle=low;
for
(vari=low;i if
(array[i] if
(i!=middle){
vartmp=array[middle];
array[middle]=array[i];
array[i]=tmp;
}
middle++;
}
}
vartmp=array[high];
array[high]=array[middle];
array[middle]=tmp;
returnmiddle;
},
sort:
function
(array,low,high){
if
(low varmiddle=quickSort.partition(array,low,high);
quickSort.sort(array,low,middle-1);
quickSort.sort(array,middle+1,high);
}
}
}
return
{
quickSort:quickSort.sort
};
})();
vararray=[0,2,7,4,3,5,1,6];
sort.quickSort(array,0,8);
alert(array.toString());
|
变种算法
Python递归
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
defpartition(inlist,start_index,end_index):
flag
=
inlist[end_index]
i
=
start_index
-
1
forjinrange(start_index,end_index):
ifinlist[j]>flag:
pass
else
:
i
+
=
1
tmp
=
inlist[i]
inlist[i]
=
inlist[j]
inlist[j]
=
tmp
tmp
=
inlist[end_index]
inlist[end_index]
=
inlist[i
+
1
]
inlist[i
+
1
]
=
tmp
returni
+
1
defquickSort(inlist,start_index,end_index):
ifstart_index>
=
end_index:
returnmiddle
=
partition(inlist,start_index,end_index)
quickSort(inlist,start_index,middle
-
1
)
quickSort(inlist,middle
+
1
,end_index)
returninlistprintquickSort([
49
,
27
,
38
,
1
,
13
,
76
,
97
,
65
],
0
,
len
([
49
,
27
,
38
,
1
,
13
,
76
,
97
,
65
])
-
1
)
C语言
#include
intfun(inta[],inti,intj)
{
a[
0
]
=
a[i];
while
(i {
while
(i if
(i while
(i if
(i }
a[i]
=
a[
0
];
returni;
}
voidQuick_Sort(inta[],ints,intt)
{
inti;
if
(s {
i
=
fun(a,s,t);
Quick_Sort(a,s,i
-
1
);
Quick_Sort(a,s
+
1
,t);
}
}
voidput(inta[],intn)
{
inti;
for
(i
=
1
;i printf(
"%d\t"
,a[i]);
printf(
"\n"
);
}
voidmain()
{
inta[
10
]
=
{
0
,
9
,
8
,
7
,
6
,
5
,
4
,
3
,
2
,
1
};
put(a,
10
);
Quick_Sort(a,
1
,
10
);
put(a,
10
);
}
|
变种随机化快排
快速排序的最坏情况基于每次划分对主元的选择。基本的快速排序选取第一个元素作为主元。这样在 数组已经有序的情况下,每次划分将得到最坏的结果。一种比较常见的优化方法是随机化算法,即随机选取一个元素作为主元。这种情况下虽然最坏情况仍然是O(n^2),但最坏情况不再依赖于输入数据,而是由于 随机函数取值不佳。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望 时间复杂度。一位前辈做出了一个精辟的总结:“随机化快速排序可以满足一个人一辈子的人品需求。”
随机化快速排序的唯一缺点在于,一旦输入数据中有很多的相同数据,随机化的效果将直接减弱。对于极限情况,即对于n个相同的数排序,随机化快速排序的 时间复杂度将毫无疑问的降低到O(n^2)。解决方法是用一种方法进行扫描,使没有交换的情况下主元保留在原位置。
平衡快排
(Balanced Quicksort):每次尽可能地选择一个能够代表中值的元素作为关键数据,然后遵循普通快排的原则进行比较、替换和 递归。通常来说,选择这个数据的方法是取开头、结尾、中间3个数据,通过比较选出其中的中值。取这3个值的好处是在实际问题中,出现近似顺序数据或逆序数据的概率较大,此时中间数据必然成为中值,而也是事实上的近似中值。万一遇到正好中间大两边小(或反之)的数据,取的值都接近最值,那么由于至少能将两部分分开,实际效率也会有2倍左右的增加,而且利于将数据略微打乱,破坏退化的结构。
外部快排
(External Quicksort):与普通快排不同的是,关键数据是一段buffer,首先将之前和之后的M/2个元素读入buffer并对该buffer中的这些元素进行排序,然后从被 排序 数组的开头(或者结尾)读入下一个元素,假如这个元素小于buffer中最小的元素,把它写到最开头的空位上;假如这个元素大于buffer中最大的元素,则写到最后的空位上;否则把buffer中最大或者最小的元素写入数组,并把这个元素放在buffer里。保持最大值低于这些关键数据,最小值高于这些关键数据,从而避免对已经有序的中间的数据进行重排。完成后, 数组的中间空位必然空出,把这个buffer写入数组中间空位。然后 递归地对外部更小的部分,循环地对其他部分进行排序。
三路基数快排
(Three-way Radix Quicksort,也称作Multikey Quicksort、Multi-key Quicksort):结合了 基数排序(radix sort,如一般的 字符串比较排序就是基数排序)和快排的特点,是字符串排序中比较高效的算法。该算法被排序 数组的元素具有一个特点,即multikey,如一个 字符串,每个字母可以看作是一个key。算法每次在被排序 数组中任意选择一个元素作为关键数据,首先仅考虑这个元素的第一个key(字母),然后把其他元素通过key的比较分成小于、等于、大于关键数据的三个部分。然后 递归地基于这一个key位置对“小于”和“大于”部分进行排序,基于下一个key对“等于”部分进行排序。
伪代码非随机
QUICKSORT(
A,
p,
r)
1
if
p<
r
2
then
q ←PARTITION(
A,
p,
r)
3 QUICKSORT(
A,
p,
q-1)
4 QUICKSORT(
A,
q+1,
r)
为排序一个完整的 数组
A,最初的调用是QUICKSORT(
A,
1,
length[
A])。
快速 排序算法的关键是PARTITION过程,它对子 数组A[p..r]进行就地重排:
PARTITION(
A,
p,
r)
1
x←
A[
r]
2
i←
p-1
3
for
j←
p
to
r-1
4
do if
A[
j]≤
x
5
then
i←
i+1
6 exchange
A[
i]←→
A[
j]
7 exchange
A[
i+1]←→
A[
r]
8
return
i+1
[1]
随机
对PARTITION和QUICKSORT所作的改动比较小。在新的划分过程中,我们在真正进行划分之前实现交换:
(其中PARTITION过程同
快速 排序 伪代码(非随机))
RANDOMIZED-PARTITION(
A,
p,
r)
1
i← RANDOM(
p,
r)
2 exchange
A[
r]←→
A[
i]
3
return PARTITION(
A,
p,
r)
新的快速排序过程不再调用PARTITION,而是调用RANDOMIZED-PARTITION。
RANDOMIZED-QUICKSORT(
A,
p,
r)
1
if
p<
r
2
then
q← RANDOMIZED-PARTITION(
A,
p,
r)
3 RANDOMIZED-QUICKSORT(
A,
p,
q-1)
4 RANDOMIZED-QUICKSORT(
A,
q+1,
r)
[1]
函数
在c++中可以用函数qsort()可以直接为 数组进行排序。
用 法:
void qsort(void *base, int nelem, int width, int (*fcmp)(const void *,const void *));
参数:
1 待 排序 数组首地址
2 数组中待排序元素数量
3 各元素的占用空间大小
4 指向函数的 指针,用于确定排序的顺序
1 待 排序 数组首地址
2 数组中待排序元素数量
3 各元素的占用空间大小
4 指向函数的 指针,用于确定排序的顺序
性能分析注意
这里为方便起见,我们假设算法Quick_Sort的范围阈值为1(即一直将线性表分解到只剩一个元素),这对该 算法复杂性的分析没有本质的影响。
我们先分析函数 partition的性能,该函数对于确定的输入复杂性是确定的。观察该函数,我们发现,对于有n个元素的确定输入L[p..r],该函数运行时间显然为θ(n)。
最坏情况
无论适用哪一种方法来选择pivot,由于我们不知道各个元素间的相对大小关系(若知道就已经排好序了),所以我们无法确定pivot的选择对划分造成的影响。因此对各种pivot 选择法而言,最坏情况和最好情况都是相同的。
我们从直觉上可以判断出最坏情况发生在每次划分过程产生的两个区间分别包含n-1个元素和1个元素的时候(设输入的表有n个元素)。下面我们暂时认为该猜测正确,在后文我们再详细证明该猜测。
对于有n个元素的表L[p..r],由于函数Partition的计算时间为θ(n),所以快速排序在序坏情况下的复杂性有 递归式如下:
T(1)=θ(1),T(n)=T(n-1)+T(1)+θ(n) (1)
用迭代法可以解出上式的解为T(n)=θ(n
2)。
这个最坏情况运行时间与 插入排序是一样的。
下面我们来证明这种每次划分过程产生的两个区间分别包含n-1个元素和1个元素的情况就是最坏情况。
设T(n)是过程Quick_Sort作用于规模为n的输入上的最坏情况的时间,则
T(n)=max(T(q)+T(n-q))+θ(n),其中1≤q≤n-1 (2)
我们假设对于任何k
将归纳假设代入(2),得到:
T(n)≤max(cq
2+c(n-q)
2)+θ(n)=c*max(q
2+(n-q)
2)+θ(n)
因为在[1,n-1]上q2+(n-q)2关于q递减,所以当q=1时q
2+(n-q)
2有最大值n
2-2(n-1)。于是有:
T(n)≤cn
2-2c(n-1)+θ(n)≤cn
2
只要c足够大,上面的第二个小于等于号就可以成立。于是对于所有的n都有T(n)≤cn。
这样, 排序算法的最坏情况运行时间为θ(n
2),且最坏情况发生在每次划分过程产生的两个区间分别包含n-1个元素和1个元素的时候。
时间复杂度为o(n
2)。
最好情况
如果每次划分过程产生的区间大小都为n/2,则快速排序法运行就快得多了。这时有:
T(n)=2T(n/2)+θ(n),T(1)=θ(1) (3)
解得:T(n)=θ(nlogn)
快速排序法最佳情况下执行过程的递归树如下图所示,图中lgn表示以2位底的对数,而本文中用logn表示以2位底的对数.
图2快速排序法最佳情况下执行过程的 递归树
由于快速排序法也是基于比较的排序法,其运行时间为Ω(nlogn),所以如果每次划分过程产生的区间大小都为n/2,则运行时间θ(nlogn)就是最好情况运行时间。
但是,是否一定要每次平均划分才能达到最好情况呢?要理解这一点就必须理解对称性是如何在描述运行时间的 递归式中反映的。我们假设每次划分过程都产生9:1的划分,乍一看该划分很不对称。我们可以得到 递归式:
T(n)=T(n/10)+T(9n/10)+θ(n),T(1)=θ(1) (4)
这个 递归式对应的 递归树如下图所示:
图3(4)式对应的递归树
请注意该树的每一层都有代价n,直到在深度log10n=θ(logn)处达到边界条件,以后各层代价至多为n。 递归于深度log10/9n=θ(logn)处结束。这样,快速排序的总时间代价为T(n)=θ(nlogn),从渐进意义上看就和划分是在中间进行的一样。事实上,即使是99:1的划分时间代价也为θ(nlogn)。其原因在于,任何一种按常数比例进行划分所产生的 递归树的深度都为θ(nlogn),其中每一层的代价为
O(n),因而不管常数比例是什么,总的运行时间都为θ(nlogn),只不过其中隐含的常数因子有所不同。(关于 算法复杂性的渐进阶,请参阅 算法的复杂性)
平均情况
我们首先对平均情况下的性能作直觉上的分析。
要想对快速排序的平均情况有个较为清楚的概念,我们就要对遇到的各种输入作个假设。通常都假设输入数据的所有排列都是等可能的。后文中我们要讨论这个假设。
当我们对一个随机的输入 数组应用快速排序时,要想在每一层上都有同样的划分是不太可能的。我们所能期望的是某些划分较对称,另一些则很不对称。事实上,我们可以证明,如果选择L[p..r]的第一个元素作为支点元素,Partition所产生的划分80%以上都比9:1更对称,而另20%则比9:1差,这里证明从略。
平均情况下,Partition产生的划分中既有“好的”,又有“差的”。这时,与Partition执行过程对应的 递归树中,好、差划分是随机地分布在树的各层上的。为与我们的直觉相一致,假设好、差划分交替出现在树的各层上,且好的划分是最佳情况划分,而差的划分是最坏情况下的划分,图4(a)表示了递归树的连续两层上的划分情况。在根节点处,划分的代价为n,划分出来的两个子表的大小为n-1和1,即最坏情况。在根的下一层,大小为n-1的子表按最佳情况划分成大小各为(n-1)/2的两个子表。这儿我们假设含1个元素的子表的边界条件代价为1。
(a)
(b)
图4 快速排序的递归树划分中的两种情况
在一个差的划分后接一个好的划分后,产生出三个子表,大小各为1,(n-1)/2和(n-1)/2,代价共为2n-1=θ(n)。这与图4(b)中的情况差不多。该图中一层划分就产生出大小为(n-1)/2+1和(n-1)/2的两个子表,代价为n=θ(n)。这种划分差不多是完全对称的,比9:1的划分要好。从直觉上看,差的划分的代价θ(n)可被吸收到好的划分的代价θ(n)中去,结果是一个好的划分。这样,当好、差划分交替分布划分都是好的一样:仍是θ(nlogn),但θ记号中隐含的常数因子要略大一些。关于平均情况的严格分析将在后文给出。
在前文从直觉上探讨快速排序的平均性态过程中,我们已假定输入数据的所有排列都是等可能的。如果输入的分布满足这个假设时,快速排序是对足够大的输入的理想选择。但在实际应用中,这个假设就不会总是成立。