快速排序法(QuickSort)是一种非常快的对比排序方法。它也Divide-And-Conquer思想的实现之一。自从其产生以来,快速排序理论得到了极大的改进,然而在实际中却十分难以编程出正确健壮的代码。本文将对快速排序算法的基本理论和编程实践方面做作一个全面的讲解。在本文讲解中,将忽略很多细枝末节,试图给读者形成一个非常具体的快速排序形象。

1.快速排序---基本理论

因为该算法是Divide-And-Conquer思想的一个实现,所以本文将以Divide-And-Conquer思想对其进行分析。首先,假设所要排序的数字存储在数组S中,则该算法的操作可以拆分为两部分:

  • 在S中选出一个元素v;
  • 将S数组分为三个子数组。其中v这个元素单独形成子数组1,比v小的元素形成子数组2,比v大的元素形成自数组3.
  • 分别对子数组2和子数组3进行前两步操作,实现递归排序;
  • 返回时,依次返回S1,V,S2;

该程序具有平均运行时间T(n) = O(nlgn), 最差运行时间T(n) = O(n^2);

下面给出一个简单的排序实例对以上算法进行简单说明:

初始数组为--------------> S: 6,10,13,5,8,3,2,11

将第一个元素赋值给v----->v = 6;

以v为标准将S进行拆分--->[2,5,3],[6],[8,13,10,11] <----------将得到的数组命名为S1, S2;

同样对子数组S1进行拆分->[ ], [2], [ 5, 3] <--------------------拆分之后,第一个子数组为空。将得到的数组命名为S12;

对子数组S2进行拆分----->[ ], [8], [13, 10, 11]<---------------将得到的数组命名为S22;

此时的数组S为---------->2,5,3,6,8,13,10,11

对子数组S12进行拆分---->[3], [5],[ ];

对自数组S22进行拆分---->[10,11],[13],[]<--------------------将得到的数组命名为S221

此时的数组S为----------->2,3,5,6,8,10,11,13

对子数组S221进行拆分--->[ ], [11], [13]

对后得到的数组为-------->2,3,5,6,8,10,11,13;

根据以上分析,编写快速排序算法程序,得到的程序如下:

1 #include <string>
2 #include <iostream>
3
4  using namespace::std;
5
6  int Partition( int A[], int p, int q )
7 {
8 int key = A[p];
9 int i = p;
10 for(int j = p + 1 ;j < q; j++ )
11 {
12 if( A[j] <= key )
13 {
14 i++;
15 swap<int>(A[i], A[j]);
16 }
17 }
18 swap<int>(A[p], A[i]);
19 return i;
20 }
21
22  void QuickSort( int A[], int p, int q )
23 {
24 if( p < q )
25 {
26 int r = Partition(A, p, q);
27 QuickSort(A,p,r-1);
28 QuickSort(A,r+1,q);
29 }
30 }
31
32  int main()
33 {
34 int A[10] = {8,1,4,9,0,3,5,2,7,6};
35 QuickSort(A,0,9);
36 for( int k = 0; k < 10; k++ )
37 cout << A[k] << " ";
38 cout << endl;
39 }

计算结果如图:

看似结果很好,但是很遗憾,在实际中,我们却并不采用这样的程序。为什么呢?因为该程序还有几点需要进行改进:

  • 当我们输入的数组S是已经排序好的一列数,那么这个程序的运行时间将是O(n^2),这个效率是插入排序的效率,所以是很低很低的。(可以利用递归树进行具体分析)
  • 为了提高效率,可以使得i和j分别从左边和右边进行搜索,将值分别与v进行对比,当S[i]>v而S[j]<v的时候,再进行交换,这样可以提高交换的效率(也即降低交换的次数)
  • 快速排序算法在数组很小的时候的效率是十分低下的,其速度并没有插入排序算法的速度快,因而在数组的大小小于一定的值之后,应该采用插入排序完成排序。

为了解决第一个问题,很多专家学者进行了如下尝试:

  1. 选取最前面的两个不同的元素,取其中较大的一个赋值给v;但是这种做法和第一种做法有相同的弊端,读者可自行进行分析,在此不作赘述。
  2. 在诸多元素之中选取一个随机的元素作为v。这种做法可以避免O(n^2)的弊端,但是随机数的产生需要花费很多的时间,所以这种做法是正确的,但是却并不是高效的。
  3. 选取最左边,中间和最右边三个数中的中间值。比如左中右三个值分别是0、8、6,那么我们就选取6作为v值。这样做是高效而安全的。所以一般的快速排序算法就用这种策略。

下面给出以上分析之后的快速排序算法程序:

1 #include <string>
2 #include <iostream>
3 #include <algorithm>
4  using namespace::std;
5
6  int Median3(int A[], int p, int q )
7 {
8 int c = ( p + q ) / 2;
9 if( A[p] > A[c] )
10 swap<int>(A[p], A[c]);
11 if( A[p] > A[q] )
12 swap<int>(A[p], A[q]);
13 if( A[c] > A[q] )
14 swap<int>(A[c], A[q]);
15 swap<int>(A[c],A[q-1]);
16 return A[q-1];
17 }
18
19  int Partition( int A[], int p, int q )
20 {
21 int key = Median3( A, p, q );
22 int i = p;
23 int j = q-1;
24 while(1)
25 {
26 while( A[++i] < key ){}
27 while( A[--j] > key ){}
28 if( i < j )
29 swap<int>( A[i], A[j] );
30 else
31 break;
32 }
33 swap( A[i], A[q-1] );
34 return i;
35 }
36
37 void InsertionSort(int A[], int N)
38 {
39 int tmp;
40 int j;
41 int p;
42
43 for( p = 1; p < N; p++ )
44 {
45 tmp = A[p];
46 for( j = p; j > 0 && A[j -1] > tmp; j -- )
47 A[j] = A[j-1];
48 A[j] = tmp;
49 }
50 }
51
52 #define cutoff 5
53 void QuickSort(int A[], int p, int q)
54 {
55 if( p + cutoff <= q )
56 {
57 int r = Partition(A, p, q);
58 QuickSort( A, p, r - 1 );
59 QuickSort( A, r + 1, q );
60 }
61 else
62 InsertionSort(A + p, q - p + 1 );
63 }
64
65 int main()
66 {
67 int A[8] = {6,10,13,5,8,3,2,11};
68 QuickSort(A,0,7);
69 for( int k = 0; k < 8; k++ )
70 cout << A[k] << " ";
71 cout << endl;
72 }

排序结果如图所示:

该程序中,cutoff的值必须大于等与2!

因为若是cutoff = 1;也就是说,插入法排序的数字只有一个;那么递归的最内一层是两个数字。在这个时候就会出现问题,具体分析如下:

以上例中的数组A为例,在递归树的右侧,会出现对13,11的排序;此时,p = 6, q = 7;

设C, L, R分别代表了中间,左边和右边三个值,那么根据Median3函数算法的计算,最终得到L = 13, C = 13, R = 11; 于是:

L < C  => L和C不交换;

L > R => L和R交换,此时 C = 11, L = 11, R = 13;

C < R => C和R不交换;

所以最后得到的Key = 11, 经过Median3排序之后的顺序是11,13;

于是对其进行排序,完成时i = 7, 因此在执行33句时会交换A[7]和A[6],交换之后得到的顺序是 13, 11;

这个顺序就是最终的排序结果,因此在排序的最后导致了程序的排序结果错误;

产生这个错误的主要原因是:剩余了两个数,而在求meidian值的时候,对三个数进行了对比。

同时,若cutoff的值小于2还将产生一个错误,那就数:

--j的岗哨依赖于数组的元素A[P] < key,这样才使得,--j不会越过p值;而在上述情况中,A[p] = key值,为了提高程序的效率, 该程序在编写时设定,当A[j] = A[p]时,j会继续搜索,所以导致--j越过了A[p];

所以在设定cutoff的时候,cutoff的值至少为2,也就说InsertionSort至少要对两个数进行排序或者更多。

原创作品,转载请注明出处;

posted on 2011-05-11 22:09  cylee025  阅读(38601)  评论(7编辑  收藏  举报