快速排序

快速排序由于排序效率在同为O(N*logN)的几种排序方法中效率较高,因此经常被采用,再加上快速排序思想----分治法也确实实用,因此很多软件公司的笔试面试,包括像腾讯,微软等知名IT公司都喜欢考这个,还有大大小的程序方面的考试如软考,考研中也常常出现快速排序的身影。

总的说来,要直接默写出快速排序还是有一定难度的,因为本人就自己的理解对快速排序作了下白话解释,希望对大家理解有帮助,达到快速排序,快速搞定

 

快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。

该方法的基本思想是:

1.先从数列中取出一个数作为基准数。

2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。

3.再对左右区间重复第二步,直到各区间只有一个数。

 

虽然快速排序称为分治法,但分治法这三个字显然无法很好的概括快速排序的全部步骤。因此我的对快速排序作了进一步的说明:挖坑填数+分治法:

先来看实例吧,定义下面再给出(最好能用自己的话来总结定义,这样对实现代码会有帮助)。

 

以一个数组作为示例,取区间第一个数为基准数。

0

1

2

3

4

5

6

7

8

9

72

6

57

88

60

42

83

73

48

85

初始时,i = 0;  j = 9;   X = a[i] = 72

由于已经将a[0]中的数保存到X中,可以理解成在数组a[0]上挖了个坑,可以将其它数据填充到这来。

从j开始向前找一个比X小或等于X的数。当j=8,符合条件,将a[8]挖出再填到上一个坑a[0]中。a[0]=a[8]; i++;  这样一个坑a[0]就被搞定了,但又形成了一个新坑a[8],这怎么办了?简单,再找数字来填a[8]这个坑。这次从i开始向后找一个大于X的数,当i=3,符合条件,将a[3]挖出再填到上一个坑中a[8]=a[3]; j--;

 

数组变为:

0

1

2

3

4

5

6

7

8

9

48

6

57

88

60

42

83

73

88

85

 i = 3;   j = 7;   X=72

再重复上面的步骤,先从后向前找,再从前向后找

从j开始向前找,当j=5,符合条件,将a[5]挖出填到上一个坑中,a[3] = a[5]; i++;

从i开始向后找,当i=5时,由于i==j退出。

此时,i = j = 5,而a[5]刚好又是上次挖的坑,因此将X填入a[5]。

 

数组变为:

0

1

2

3

4

5

6

7

8

9

48

6

57

42

60

72

83

73

88

85

可以看出a[5]前面的数字都小于它,a[5]后面的数字都大于它。因此再对a[0…4]和a[6…9]这二个子区间重复上述步骤就可以了。

 

 

对挖坑填数进行总结

1.i =L; j = R; 将基准数挖出形成第一个坑a[i]。

2.j--由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。

3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。

4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。

照着这个总结很容易实现挖坑填数的代码:

  1. int AdjustArray(int s[], int l, int r) //返回调整后基准数的位置  
  2. {  
  3.     int i = l, j = r;  
  4.     int x = s[l]; //s[l]即s[i]就是第一个坑  
  5.     while (i < j)  
  6.     {  
  7.         // 从右向左找小于x的数来填s[i]  
  8.         while(i < j && s[j] >= x)   
  9.             j--;    
  10.         if(i < j)   
  11.         {  
  12.             s[i] = s[j]; //将s[j]填到s[i]中,s[j]就形成了一个新的坑  
  13.             i++;  
  14.         }  
  15.   
  16.         // 从左向右找大于或等于x的数来填s[j]  
  17.         while(i < j && s[i] < x)  
  18.             i++;    
  19.         if(i < j)   
  20.         {  
  21.             s[j] = s[i]; //将s[i]填到s[j]中,s[i]就形成了一个新的坑  
  22.             j--;  
  23.         }  
  24.     }  
  25.     //退出时,i等于j。将x填到这个坑中。  
  26.     s[i] = x;  
  27.   
  28.     return i;  
  29. }  
int AdjustArray(int s[], int l, int r) //返回调整后基准数的位置
{
	int i = l, j = r;
	int x = s[l]; //s[l]即s[i]就是第一个坑
	while (i < j)
	{
		// 从右向左找小于x的数来填s[i]
		while(i < j && s[j] >= x) 
			j--;  
		if(i < j) 
		{
			s[i] = s[j]; //将s[j]填到s[i]中,s[j]就形成了一个新的坑
			i++;
		}

		// 从左向右找大于或等于x的数来填s[j]
		while(i < j && s[i] < x)
			i++;  
		if(i < j) 
		{
			s[j] = s[i]; //将s[i]填到s[j]中,s[i]就形成了一个新的坑
			j--;
		}
	}
	//退出时,i等于j。将x填到这个坑中。
	s[i] = x;

	return i;
}

再写分治法的代码:

  1. void quick_sort1(int s[], int l, int r)  
  2. {  
  3.     if (l < r)  
  4.     {  
  5.         int i = AdjustArray(s, l, r);//先成挖坑填数法调整s[]  
  6.         quick_sort1(s, l, i - 1); // 递归调用   
  7.         quick_sort1(s, i + 1, r);  
  8.     }  
  9. }  
void quick_sort1(int s[], int l, int r)
{
	if (l < r)
    {
		int i = AdjustArray(s, l, r);//先成挖坑填数法调整s[]
		quick_sort1(s, l, i - 1); // 递归调用 
		quick_sort1(s, i + 1, r);
	}
}

这样的代码显然不够简洁,对其组合整理下:

  1. //快速排序  
  2. void quick_sort(int s[], int l, int r)  
  3. {  
  4.     if (l < r)  
  5.     {  
  6.         //Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换 参见注1  
  7.         int i = l, j = r, x = s[l];  
  8.         while (i < j)  
  9.         {  
  10.             while(i < j && s[j] >= x) // 从右向左找第一个小于x的数  
  11.                 j--;    
  12.             if(i < j)   
  13.                 s[i++] = s[j];  
  14.               
  15.             while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数  
  16.                 i++;    
  17.             if(i < j)   
  18.                 s[j--] = s[i];  
  19.         }  
  20.         s[i] = x;  
  21.         quick_sort(s, l, i - 1); // 递归调用   
  22.         quick_sort(s, i + 1, r);  
  23.     }  
  24. }  
//快速排序
void quick_sort(int s[], int l, int r)
{
    if (l < r)
    {
		//Swap(s[l], s[(l + r) / 2]); //将中间的这个数和第一个数交换 参见注1
        int i = l, j = r, x = s[l];
        while (i < j)
        {
            while(i < j && s[j] >= x) // 从右向左找第一个小于x的数
				j--;  
            if(i < j) 
				s[i++] = s[j];
			
            while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数
				i++;  
            if(i < j) 
				s[j--] = s[i];
        }
        s[i] = x;
        quick_sort(s, l, i - 1); // 递归调用 
        quick_sort(s, i + 1, r);
    }
}

快速排序还有很多改进版本,如随机选择基准数,区间内数据较少时直接用另的方法排序以减小递归深度。有兴趣的筒子可以再深入的研究下。

 

注1,有的书上是以中间的数作为基准数的,要实现这个方便非常方便,直接将中间的数和第一个数进行交换就可以了。

 

 

 

 

 

 

 

 

排序

设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动。

一趟快速排序的算法是:
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[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。

2排序演示编辑

假设用户输入了如下数组:
下标
0
1
2
3
4
5
数据
6
2
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
2
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
2
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<H])++[H]++
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
#include<iostream>
 
usingnamespacestd;
 
voidQsort(inta[],intlow,inthigh)
{
if(low>=high)
{
return;
}
intfirst=low;
intlast=high;
intkey=a[first];/*用字表的第一个记录作为枢轴*/
while(first<last)
{
while(first<last&&a[last]>=key)
--last;
a[first]=a[last];/*将比第一个小的移到低端*/
while(first<last&&a[first]<=key)
++first;
a[last]=a[first];/*将比第一个大的移到高端*/
}
a[first]=key;/*枢轴记录到位*/
Qsort(a,low,first-1);
Qsort(a,last+1,high);
}
intmain()
{
inta[]={57,68,59,52,72,28,96,33,24};
 
Qsort(a,0,sizeof(a)/sizeof(a[0])-1);/*这里原文第三个参数要减1否则内存泄露*/
 
for(inti=0;i<sizeof(a)/sizeof(a[0]);i++)
{
cout<<a[i]<<"";
}
 
return(0);
}/*参考数据结构p274(清华大学出版社,严蔚敏)*/

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
voidQuickSort(inta[],intnumsize)/*a是整形数组,numsize是元素个数*/
{
inti=0,j=numsize-1;
intval=a[0];/*指定参考值val大小*/
if(numsize>1)/*确保数组长度至少为2,否则无需排序*/
{
while(i<j)/*循环结束条件*/
{
/*从后向前搜索比val小的元素,找到后填到a[i]中并跳出循环*/
for(;j>i;j--)
if(a[j]<val)
{
a[i]=a[j];
break;
}
/*从前往后搜索比val大的元素,找到后填到a[j]中并跳出循环*/
for(;i<j;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-1这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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
classQuick
{
publicvoidsort(intarr[],intlow,inthigh)
{
intl=low;
inth=high;
intpovit=arr[low];
 
while(l<h)
{
while(l<h&&arr[h]>=povit)
{
h--;
}
 
if(l<h)
{
inttemp=arr[h];
arr[h]=arr[l];
arr[l]=temp;
l++;
 
}
 
while(l<h&&arr[l]<=povit)
{
l++;
 
}
 
if(l<h)
{
inttemp=arr[h];
arr[h]=arr[l];
arr[l]=temp;
h--;
 
}
}
print(arr);
System.out.print("l="+(l+1)+"h="+(h+1)+"povit="+povit+"\n");
if(l>low)sort(arr,low,h-1);
if(h<high)sort(arr,l+1,high);
 
}
}
 
 
/*//////////////////////////方式二////////////////////////////////*/
更效率点的代码:
public<TextendsComparable<?superT>>
T[]quickSort(T[]targetArr,intstart,intend)
{
inti=start+1,j=end;
Tkey=targetArr[start];
SortUtil<T>sUtil=newSortUtil<T>();
 
if(start>=end)
{
return(targetArr);
}
 
 
/*从i++和j--两个方向搜索不满足条件的值并交换
*
*条件为:i++方向小于key,j--方向大于key
*/
while(true)
{
while(targetArr[j].compareTo(key)>0)
{
j--;
}
while(targetArr[i].compareTo(key)<0&&i<j)
{
i++;
}
if(i>=j)
{
break;
}
sUtil.swap(targetArr,i,j);
if(targetArr[i]==key)
{
j--;
}else{
i++;
}
}
 
/*关键数据放到‘中间’*/
sUtil.swap(targetArr,start,j);
 
if(start<i-1)
{
this.quickSort(targetArr,start,i-1);
}
if(j+1<end)
{
this.quickSort(targetArr,j+1,end);
}
 
returntargetArr;
}
 
 
/*//////////////方式三:减少交换次数,提高效率/////////////////////*/
private<TextendsComparable<?superT>>
voidquickSort(T[]targetArr,intstart,intend)
{
inti=start,j=end;
Tkey=targetArr[start];
 
while(i<j)
{
/*按j--方向遍历目标数组,直到比key小的值为止*/
while(j>i&&targetArr[j].compareTo(key)>=0)
{
j--;
}
if(i<j)
{
/*targetArr[i]已经保存在key中,可将后面的数填入*/
targetArr[i]=targetArr[j];
}
/*按i++方向遍历目标数组,直到比key大的值为止*/
while(i<j&&targetArr[i].compareTo(key)<=0)
/*此处一定要小于等于零,假设数组之内有一亿个1,0交替出现的话,而key的值又恰巧是1的话,那么这个小于等于的作用就会使下面的if语句少执行一亿次。*/
{
i++;
}
if(i<j)
{
/*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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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<high)
{
/*从后向前搜索比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
21
22
23
24
<?php
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
procedureqsort(l,h:longint);//假设被排序的数组是a,且快排后按升序排列)
var
i,j,t,m:integer;
begin
i:=l;
j:=h;//(l,h表示快排的左右区间)
m:=a[(i+j)div2];//注意:本句不能写成:m:=(i+j)div2;
repeat
whilea[i]<mdoinc(i);
whilem<a[j]dodec(j);//降序把这个'<'换成‘>';
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<hthenqsort(i,h);//这两行是递归寻找;//【两句if不能互换】
end;
变种算法
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<stdio.h>
intfun(inta[],inti,intj)
{
a[0]=a[i];
while(i<j)
{
while(i<j&&a[0]<=a[j])j--;
if(i<j){a[i]=a[j];i++;}
while(i<j&&a[i]<a[0])i++;
if(i<j){a[j]=a[i],j--;}
}
a[i]=a[0];
returni;
}
voidQuick_Sort(inta[],ints,intt)
{
inti;
if(s<t)
{
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<n;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);
}

4变种编辑

随机化快排

快速排序的最坏情况基于每次划分对主元的选择。基本的快速排序选取第一个元素作为主元。这样在数组已经有序的情况下,每次划分将得到最坏的结果。一种比较常见的优化方法是随机化算法,即随机选取一个元素作为主元。这种情况下虽然最坏情况仍然是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对“等于”部分进行排序。

5伪代码编辑

非随机
QUICKSORT(Apr)
1 if p<r
2 then q ←PARTITION(Apr)
3 QUICKSORT(Apq-1)
4 QUICKSORT(Aq+1,r)
为排序一个完整的数组A,最初的调用是QUICKSORT(A1length[A])。
快速排序算法的关键是PARTITION过程,它对子数组A[p..r]进行就地重排:
PARTITION(Apr)
1 xA[r]
2 ip-1
3 for jp to r-1
4 do if A[j]≤x
5 then ii+1
6 exchange A[i]←→A[j]
7 exchange A[i+1]←→A[r]
8 return i+1[1] 
随机
对PARTITION和QUICKSORT所作的改动比较小。在新的划分过程中,我们在真正进行划分之前实现交换:
(其中PARTITION过程同快速排序伪代码(非随机))
RANDOMIZED-PARTITION(Apr)
1 i← RANDOM(pr)
2 exchange A[r]←→A[i]
3 return PARTITION(Apr)
新的快速排序过程不再调用PARTITION,而是调用RANDOMIZED-PARTITION。
RANDOMIZED-QUICKSORT(Apr)
1 if p<r
2 then q← RANDOMIZED-PARTITION(Apr)
3 RANDOMIZED-QUICKSORT(Apq-1)
4 RANDOMIZED-QUICKSORT(Aq+1,r)[1] 
函数
在c++中可以用函数qsort()可以直接为数组进行排序。
用 法:
void qsort(void *base, int nelem, int width, int (*fcmp)(const void *,const void *));
参数:   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)=θ(n2)。
这个最坏情况运行时间与插入排序是一样的。
下面我们来证明这种每次划分过程产生的两个区间分别包含n-1个元素和1个元素的情况就是最坏情况。
设T(n)是过程Quick_Sort作用于规模为n的输入上的最坏情况的时间,则
T(n)=max(T(q)+T(n-q))+θ(n),其中1≤q≤n-1 (2)
我们假设对于任何k<n,总有T(k)≤ck,其中c为常数;显然当k=1时是成立的。
将归纳假设代入(2),得到:
T(n)≤max(cq2+c(n-q)2)+θ(n)=c*max(q2+(n-q)2)+θ(n)
因为在[1,n-1]上q2+(n-q)2关于q递减,所以当q=1时q2+(n-q)2有最大值n2-2(n-1)。于是有:
T(n)≤cn2-2c(n-1)+θ(n)≤cn2
只要c足够大,上面的第二个小于等于号就可以成立。于是对于所有的n都有T(n)≤cn。
这样,排序算法的最坏情况运行时间为θ(n2),且最坏情况发生在每次划分过程产生的两个区间分别包含n-1个元素和1个元素的时候。
时间复杂度为o(n2)。
最好情况
如果每次划分过程产生的区间大小都为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),但θ记号中隐含的常数因子要略大一些。关于平均情况的严格分析将在后文给出。
在前文从直觉上探讨快速排序的平均性态过程中,我们已假定输入数据的所有排列都是等可能的。如果输入的分布满足这个假设时,快速排序是对足够大的输入的理想选择。但在实际应用中,这个假设就不会总是成立。
解决的方法是,利用随机化策略,能够克服分布的等可能性假设所带来的问题。
一种随机化策略是:与对输入的分布作“假设”不同的是对输入的分布作“规定”。具体地说,在排序输入的线性表前,对其元素加以随机排列,以强制的方法使每种排列满足等可能性。事实上,我们可以找到一个能在O(n)时间内对含n个元素的数组加以随机排列的算法。这种修改不改变算法的最坏情况运行时间,但它却使得运行时间能够独立于输入数据已排序的情况。
另一种随机化策略是:利用前文介绍的选择支点元素pivot的第四种方法,即随机地在L[p..r]中选择一个元素作为支点元素pivot。实际应用中通常采用这种方法。
快速排序的随机化版本有一个和其他随机化算法一样的有趣性质:没有一个特别的输入会导致最坏情况性态。这种算法的最坏情况性态是由随机数产生器决定的。你即使有意给出一个坏的输入也没用,因为随机化排列会使得输入数据的次序对算法不产生影响。只有在随机数产生器给出了一个很不巧的排列时,随机化算法的最坏情况性态才会出现。事实上可以证明几乎所有的排列都可使快速排序接近平均情况性态,只有非常少的几个排列才会导致算法的近最坏情况性态。
一般来说,当一个算法可按多条路子做下去,但又很难决定哪一条保证是好的选择时,随机化策略是很有用的。如果大部分选择都是好的,则随机地选一个就行了。通常,一个算法在其执行过程中要做很多选择。如果一个好的选择的获益大于坏的选择的代价,那么随机地做一个选择就能得到一个很有效的算法。我们在前文已经了解到,对快速排序来说,一组好坏相杂的划分仍能产生很好的运行时间。因此我们可以认为该算法的随机化版本也能具有较好的性态。
在前文我们从直觉上分析了快速排序在平均情况下的性能为θ(nlogn),我们将在下面定量地分析快速排序法在平均情况下的性能。为了满足输入的数据的所有排列都是等可能的这个假设,我们采用上面提到的随机选择pivot的方法,并且在Select_pivot函数中将选出的pivot与L[p]交换位置(这不是必需的,纯粹是为了下文分析的方便,这样L[p]就是支点元素pivot)。那种基于对输入数据加以随机排列的随机化算法的平均性态也很好,只是比这儿介绍的这个版本更难以分析。
我们先来看看Partition的执行过程。为简化分析,假设所有输入数据都是不同的。即使这个假设不满足,快速排序的平均情况运行时间仍为θ(nlogn),但这时的分析就要复杂一些。
由Partition返回的值q仅依赖于pivot在L[p..r]中的秩(rank),某个数在一个集合中的秩是指该集合中小于或等于该数的元素的个数。如果设n为L[p..r]的元素个数,将L[p]与L[p..r]中的一个随机元素pivot交换就得rank(pivot)=i(i=1,2,..,n)的概率为l/n。
下一步来计算划分过程不同结果的可能性。如果rank(pivot)=1,即pivot是L[p..r]中最小的元素,则Partition的循环结束时指针i停在i=p处,指针j停在k=p处。当返回q时,划分结果的"低区"中就含有唯一的元素L[p]=pivot。这个事件发生的概率为1/n,因为rank(pivot)=i的概率为1/n。
如果rank(pivot)≥2,则至少有一个元素小于L[p],故在外循环while循环的第一次执行中,指针i停于i=p处,指针j则在达到p之前就停住了。这时通过交换就可将L[p]置于划分结果的高区中。当Partition结束时,低区的rank(pivot)-1个元素中的每一个都严格小于pivot(因为假设输入的元素不重复)。这样,对每个i=1,2,..,n-1,当rank(pivot)≥2时,划分的低区中含i个元素的概率为 l/n。
把这两种情况综合起来,我们的结论为:划分的低区的大小为1的概率为2/n,低区大小为i的概率为1/n,i=2,3,..n-1。
让我们来对Quick_Sort的期望运行时间建立一个递归式。设T(n)表示排序个元素的表所需的平均时间,则:
(5)
其中T(1)=θ(1)。
q的分布基本上是均匀的,但是q=1的可能性是其他值的两倍。根据前面作的最坏情况的分析有:
T(1)=θ(1),T(n-1)=θ(n2),所以
这可被(5)式中的θ(n)所吸收,所以(5)式可简化为:
(6)
注意对k=1,2,..,n-1,和式中每一项T(k)为T(q)和T(n-q)的机会各有一次,把这两项迭起来有:
(7)
我们用代入法来解上述递归方程。归纳假设T(n)≤a*nlogn+b,其中a>0,b>0为待定常数。可以选择足够大的a,b使anlogn+b>T(1),对于n>1有:
(8)
下面我们来确定和式
(9)
的界。
因为和式中每一项至多是nlogn,则有界:
这是个比较紧的界,但是对于解递归式(8)来说还不够强。为解该递归式,我们希望有界:
为了得到这个界,可以将和式(9)分解为两部分,这时有:
等号右边的第一个和式中的logk可由log(n/2)=logn-1从上方限界。第二个和式中的logk可由logn从上方限界,这样,
对于n≥2成立。即:
(10)
将(10)代入(8)式得:
(11)
因为我们可以选择足够大的a使a*n/4能够决定θ(n)+b,所以快速排序的平均运行时间为θ(nlogn)。
(12)
posted @ 2014-08-19 21:14  李艳21  阅读(415)  评论(0编辑  收藏  举报