快速排序
前面講了插入排序、選擇排序、冒泡排序、歸併排序以及冒泡排序的改進版雞尾酒排序和插入排序的改進版希爾排序,下面來說一種很常用的排序方法:快速排序。
快速排序既然敢以快速命名,可以想見它的排序速度是很快的。事實也是如此,在實際應用中它的平均性能非常好,因此在一般情況下屬於應用中的首選排序方式。
原理
快速排序與歸併排序一樣用了分治的思想。把一個要排序的數組按某個元素值(一般稱作主元)進行劃分,分成兩邊,大於主元的放在一邊,小於的放在另一邊,主元放兩邊的中間;然後再分別排序兩邊那兩個子數組,這樣就完成了排序。而那兩個子數組排序的時候同樣可以用快速排序再分成兩個子數組進行分別排序。以此類推,可以一直分到分無可分,即數組中只有一個元素爲止。
從上面的過程可以看出,快速排序是可以在原址上排序的,所以不需要額外的空間進行歸併。其技巧在於如何按主元進行劃分子數組。
劃分的過程有多種,這裏我們選擇兩種常見的。
- 選取最左邊的元素爲主元,從右往左過一遍,將大於主元的保持在右邊區域,小於的放到左邊區域,最後把主元放到兩個區域的中間。右邊區域與左邊區域在最後一步之前始終是挨着的,左邊區域要增加元素直接增加即可,右邊區域要增加則需要從左邊區域的最右邊給騰出一個位置,放入該元素,並把左邊區域的最右邊元素移到左邊區域的最左邊。如此從右往左過一遍之後即可劃分好。
- 同樣選取最左邊的元素爲主元,採用兩頭往中間的方式過一遍所有元素,將大於主元的保持在右邊區域,小於的放到左邊區域,最後把主元放到兩個區域的中間。右邊區域從最右邊開始增長,左邊區域從最左邊開始增長,兩個區域直到最後才接觸到一起。具體方法是先從右往左找到第一個小於主元的元素,將其放到左邊區域的最右邊,然後從左往右找到第一個大於主元的元素,將其放到右邊區域的最左邊;如此循環直到所有元素一遍過完,最後將主元放到中間。
實現
按照以上原理我們來用代碼實現。
下面就是用C語言實現的代碼。分成三個函數來實現。
- 要排序的數組a有n個元素。
- quick_sort 函數進行封裝,調用 quick_sort_。
- quick_sort_ 調用 partition 函數進行子數組的切分,將數組a[low…high]切分成 a[low…pivot-1] 和 a[pivot+1…high] 兩個子數組,然後分別對兩個子數組遞歸調用 quick_sort_ 進行排序。
- partition 的實現分兩種,partition1 按第一種劃分方式,partition2 按第二種劃分方式。
void quick_sort(int a[], int n)
{
if (n<=0) return;
quick_sort_(a, 0, n-1);
}
void quick_sort_(int a[], int low, int high)
{
if (low >= high) return;
int pivot = 0;
pivot = partition1(a, low, high);
//pivot = partition2(a, low, high);
quick_sort_(a, low, pivot-1);
quick_sort_(a, pivot+1, high);
}
int partition1(int a[], int low, int high)
{
int x = a[low]; //取a[low]的值x作爲主元
/* 從右往左看,大於主元的保持在右邊區域,小於的放到左邊區域 */
int i = high + 1; //i指向右邊區域的最左邊
for (int j=high; j>low; j--) { //j指向左邊區域的最左邊
if (a[j] >= x) { //若有元素要放到右邊區域
i--; //從左邊區域的最右邊給騰出一個位置
swap(&a[i], &a[j]); //放入該元素,並把左邊區域的最右邊元素移到左邊
} // 區域的最左邊,此時j依然指向左邊區域的最左邊
}
swap(&a[low], &a[i-1]); //將主元放到中間
return i-1; //返回主元所在位置下標
}
int partition2(int a[], int low, int high)
{
int x = a[low]; //取a[low]的值x作爲主元
/* 把小於x的元素放到左邊區域,其餘放到右邊區域,x放到中間 */
while (low < high) {
while (low<high && a[high]>=x) high--; //從右往左找到第一個小於主元x的元素
a[low] = a[high]; //將其放到左邊區域的最右邊
while (low<high && a[low]<=x) low++; //從左往右找到第一個大於主元x的元素
a[high] = a[low]; //將其放到右邊區域的最左邊
}
a[low] = x; //將主元放到中間
return low; //返回主元所在位置下標
}
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
爲了驗證此函數的效果,加上了如下輔助代碼,對3個數組進行排序,運行結果在最後,可見排序成功。
#include <stdio.h>
#include <stdlib.h>
#define SIZE_ARRAY_1 5
#define SIZE_ARRAY_2 6
#define SIZE_ARRAY_3 20
void quick_sort(int a[], int n);
void show_array(int a[], int n);
void main()
{
int array1[SIZE_ARRAY_1]={1,4,2,-9,0};
int array2[SIZE_ARRAY_2]={10,5,2,1,9,2};
int array3[SIZE_ARRAY_3];
for(int i=0; i<SIZE_ARRAY_3; i++) {
array3[i] = (int)((40.0*rand())/(RAND_MAX+1.0)-20);
}
printf("Before sort, ");
show_array(array1, SIZE_ARRAY_1);
quick_sort(array1, SIZE_ARRAY_1);
printf("After sort, ");
show_array(array1, SIZE_ARRAY_1);
printf("Before sort, ");
show_array(array2, SIZE_ARRAY_2);
quick_sort(array2, SIZE_ARRAY_2);
printf("After sort, ");
show_array(array2, SIZE_ARRAY_2);
printf("Before sort, ");
show_array(array3, SIZE_ARRAY_3);
quick_sort(array3, SIZE_ARRAY_3);
printf("After sort, ");
show_array(array3, SIZE_ARRAY_3);
}
void show_array(int a[], int n)
{
if(n>0)
printf("This array has %d items: ", n);
else
printf("Error: array size should bigger than zero.\n");
for(int i=0; i<n; i++) {
printf("%d ", a[i]);
}
printf("\n");
}
運行結果:
Before sort, This array has 5 items: 1 4 2 -9 0
After sort, This array has 5 items: -9 0 1 2 4
Before sort, This array has 6 items: 10 5 2 1 9 2
After sort, This array has 6 items: 1 2 2 5 9 10
Before sort, This array has 20 items: 13 -4 11 11 16 -12 -6 10 -8 2 0 5 -5 0 18 16 5 8 -14 4
After sort, This array has 20 items: -14 -12 -8 -6 -5 -4 0 0 2 4 5 5 8 10 11 11 13 16 16 18
分析
時間複雜度
從代碼可見,partition的過程是遍歷一次 個元素,而遞歸調用 partition 的次數與劃分是否平衡有關。最好的情況是正好平衡,即每次劃分成一半一半,這種情況下partition 的次數類似於歸併排序中歸併的次數,即 ,所以快速排序的最佳時間複雜度爲 。
然而在最壞的情況下,劃分極度不平衡,每次有一個組只有一個元素,此時快速排序將類似於插入排序,劃分次數會達到 ,所以快速排序的最壞時間複雜度爲 。
所幸最壞的情況一般不會發生,一般來說快速排序的性能還是相當不錯的。如果期望更加靠譜一點,可以採用隨機化選取主元的方式。從代碼可見,我們前面的主元選取都是直接選了第一個元素,如果第一個元素正好就是最小的元素,則可能發生最壞的情況,而每次都隨機化選擇主元則可以避免這個情況。
空間複雜度
因爲快速排序可以原址進行,所以這裏需要的空間 的。但是快速排序有遞歸調用,而調用的深度又取決於劃分的情況,所以快速排序的最佳空間複雜度爲 ,最壞空間複雜度爲 。