排序搜索之快速排序算法
一:快速排序算法
快速排序算法由C.A.R.Hoare在1960年提出,是应用最为广泛的排序算法。快速排序具有一些理想特征,如原位排序(只使用一个小的辅助栈),平均排序时间复杂度为nlgn,并且内部循环很小(使得它比其他的nlgn排序算法要快)。它的缺点是不够稳定,最坏情况下时间复杂度会退化到n2。
1.1 基本算法
快速排序算法是一个分治排序算法。它重排数组,将数组分为满足下面三个条件的两个部分,然后分别对两个部分进行排序。
1:对于某个i,a[i]在数组的最终位置上;
2:a[i]之前的元素都比a[i]小;
3:a[i]之后的元素都比a[i]大;
快速排序算法通过划分来完成排序,然后递归的应用该方法处理子数组。
1 int partition(int a[], int start, int end); 2 void quicksort(int a[], int start, int end) 3 { 4 int i = 0; 5 6 /* 递归结束条件 */ 7 if(start >= end) 8 { 9 return; 10 } 11 12 /* 划分数组 */ 13 i = partition(a, start, end); 14 15 /* 递归排序左右子数组 */ 16 quicksort(a, start, i - 1); 17 quicksort(a, i + 1, end); 18 }
重排数组,也就是上面的partition函数,是快速排序的核心。我们先考虑一种简单的策略(策略一)。首先,我们取a[start]作为划分元素,这个元素划分后将在最终的位置上;然后,从a[start + 1]开始向数组的右端扫描,直到找到一个大于a[start]的元素;同时从a[end]开始向数组的左端开始扫描,直到找到一个小于a[start]的元素。交换这两个元素,继续扫描过程,这样我们就能保证数组中位于左侧指针左侧的元素都比a[start]小,位于右侧指针右侧的元素都比a[start]大。当两个指针相遇时,我们交换a[start]和使扫描终止的元素,则a[start]左边的元素均小于等于a[start],a[start]右边的元素均大于a[start],从而结束了划分过程。
1 int partition(int a[], int start, int end) 2 { 3 int i, j; 4 5 for(i = start + 1, j = end; i < j; ) 6 { 7 /* i可能会越界 */ 8 while (a[i] <= a[start]) 9 { 10 i++; 11 } 12 13 /* j不可能越界 */ 14 while (a[j] > a[start]) 15 { 16 j--; 17 } 18 19 /* 左右两侧指针位置判断 */ 20 if (i < j) 21 { 22 /* 左右指针尚未相遇,交换元素 */ 23 EXCH(a[i], a[j]); 24 } 25 else 26 { 27 /* 左右指针相遇,扫描完成,交换划分元素,返回划分元素位置 */ 28 EXCH(a[start], a[j]); 29 return j; 30 } 31 } 32 }
除此以外,我们还有别的策略(策略二)。首先,我们同样取a[start]作为划分元素,这个元素划分后将在最终的位置上;然后,我们从a[start + 1]开始向右扫描,直到直到找到一个大于a[start]的元素:此时,我们将此元素与a[end]交换,然后执行end--;继续扫描直到到达end。那么此时a[end]之后的元素全都大于a[start],a[end]之前的元素(含a[end])均小于等于a[start]。交换a[start]和a[end],划分过程完成。
1 int partition(int a[], int start, int end) 2 { 3 int i = start + 1, j = end; 4 5 while(i <= j) 6 { 7 if(a[i] > a[start]) 8 { 9 /* 大于a[start]的元素交换到数组右端 */ 10 EXCH(a[i], a[j]); /* 见附录1,下同 */ 11 j--; 12 continue; 13 } 14 15 i++; 16 } 17 18 EXCH(a[start], a[j]); 19 return j; 20 }
1.2 性能和优化
快速排序算法的平均时间复杂度为NlgN,最坏的情况下退化为N2。快速排序算法不是一个稳定的算法,因此在实际应用中需要采取一定的措施来降低它退化的可能性并优化它的效率。首先,快速排序是一个递归算法,消除尾递归会带来很多好处。我们只需要将quicksort中的
/* 递归终止条件 */ if(start >= end) { return; }
修改为
/* 使用插入排序消除尾递归 */ if (start + SORT_RECURSION_MIN_DEPTH >= end) /* 见附录2 */ { insertion(a, start, end); /* 见附录3 */ return; }
前人研究表明,SORT_RECURSION_MIN_DEPTH定义为5-20之间比较高效。
其次,我们一直采用a[start]做为划分元素,这对于已经有序或基本有序的数组将会导致快速排序算法退化。根据实际情形选择一个合理的划分元素显然可以避免最坏的情况发生。使用数组中的一个随机元素做为划分元素,这样最坏情况发生的可能性就会相对很小。但是由于随机数生成器会使快速排序的内循环效率降低,一些简单的选择反而会更实用一些,例如三数取中法。
/* 三数取中法 */ mid = (start + end) >> 1; if(a[start] > a[mid]) { EXCH(a[start], a[mid]); } if(a[mid] > a[end]) { EXCH(a[mid], a[end]); } EXCH(a[start], a[mid]);
我们将中间值和a[start]交换,下面的划分处理完全不需要改动。
第三,当数组中包含大量的重复元素时,上面的实现将相等的元素全部划分到了划分元素的左侧,这也会导致算法退化。一个直接的想法就是将数组分为三个部分:一部分比划分元素小的,一部分等于划分元素的,一部分大于划分元素的。这在第二种策略中很容易实现。
while(i <= j) { if(a[i] > a[start]) { /* 大于a[start]的元素交换到数组右端 */ EXCH(a[i], a[j]); j--; continue; } else if(a[i] == a[start]) { /* 等于a[start]的元素交换到数组左端,并记录下个数 */ EXCH(a[i], a[k]); k++; } i++; } /* 将数组左端所有等于a[start]的元素交换到数组中间 */ for(i = j; k-- > start; i--) { EXCH(a[k], a[i]); }
由于此时划分过程需要返回两个参数,partition函数原型不再适用,可直接代入quicksort里,如下
1 void quicksort(int a[], int start, int end) 2 { 3 int i = start + 1, j = end, k = start + 1, mid; 4 5 /* 递归终止条件 */ 6 if (start + SORT_RECURSION_MIN_DEPTH >= end) 7 { 8 insertion(a, start, end); 9 return; 10 } 11 12 /* 三数取中法 */ 13 mid = (start + end) >> 1; 14 if(a[start] > a[mid]) 15 { 16 EXCH(a[start], a[mid]); 17 } 18 if(a[mid] > a[end]) 19 { 20 EXCH(a[mid], a[end]); 21 } 22 EXCH(a[start], a[mid]); 23 24 while(i <= j) 25 { 26 if(a[i] > a[start]) 27 { 28 /* 大于a[start]的元素交换到数组右端 */ 29 EXCH(a[i], a[j]); 30 j--; 31 continue; 32 } 33 else if(a[i] == a[start]) 34 { 35 /* 等于a[start]的元素交换到数组左端 */ 36 EXCH(a[i], a[k]); 37 k++; 38 } 39 40 i++; 41 } 42 43 /* 将数组左端所有等于a[start]的元素交换到数组中间 */ 44 for(i = j; k-- > start; i--) 45 { 46 EXCH(a[k], a[i]); 47 } 48 49 /* 递归排序左右子数组 */ 50 quicksort(a, start, i); 51 quicksort(a, j + 1, end); 52 }
第四,靠大家去发现了~~~
1.3 附录
1:
#define EXCH(v1, v2) do{\ int ltemp = v1;\ v1 = v2;\ v2 = ltemp;\ }while(0);
2: #define SORT_RECURSION_MIN_DEPTH 10
3:
/* 插入排序 */ void insertion(int a[], int start, int end) { int i, j; for(i = start + 1; i <= end; i++) { for(j = i; j > start; j--) { if(a[j] < a[j - 1]) { EXCH(a[j], a[j - 1]) } else { break; } } } }