1、序言
這是《漫談經典排序演算法系列》第三篇,先解析了冒泡排序,然後引出快速排序,給出了快速排序的兩種實現版本。
各種排序演算法的解析請參考如下:
《漫談經典排序演算法:一、從簡單選擇排序到堆排序的深度解析》
《漫談經典排序演算法:二、各種插入排序解析及效能比較》
《漫談經典排序演算法:三、冒泡排序 && 快速排序》
《漫談經典排序演算法:四、歸併排序》
《漫談經典排序演算法:五、線性時間排序(計數、基數、桶排序)》
《漫談經典排序演算法:六、各種排序演算法總結》
註:為了敘述方便,本文以及原始碼中均不考慮A[0],預設下標從1開始。
2、冒泡排序 2.1 引出
前面的兩篇部落格裡講的插入排序是基於“逐個記錄插入”,選擇排序是基於“選擇”,那麼冒泡排序其實是基於“交換”。每次從第一個記錄開始,一、二兩個記錄比較,大的往後放,二三兩個記錄比較...依次類推,這就是一趟冒泡排序。每一趟冒泡排序後,無序序列中值最大的記錄冒到序列末尾,所以稱之為冒泡排序。
2.2 代碼
//冒泡排序void bubbleSort(int *a,int n){int i,j;for(i=1;i<n;i++)for(j=1;j<n-i+1;j++){if(a[j+1]<a[j]){a[j]=a[j]+a[j+1];a[j+1]=a[j]-a[j+1];a[j]=a[j]-a[j+1];}}}
2.3 效率分析
相對於簡單選擇排序,冒泡排序交換次數明顯更多。它是通過不斷地交換把最大的數冒出來。冒泡排序平均時間和最壞情況下(逆序)時間為o(n^2)。最佳情況下雖然不用交換,但比較的次數沒有減少,時間複雜度仍為o(n^2)。此外冒泡排序是穩定的。
3、快速排序 3.1 引出
快速排序是冒泡排序的一種改進,冒泡排序排完一趟是最大值冒出來了,那麼可不可以先選定一個值,然後掃描待排序序列,把小於該值的記錄和大於該值的記錄分成兩個單獨的序列,然後分別對這兩個序列進行上述操作。這就是快速排序,我們把選定的那個值稱為樞紐值,如果樞紐值為序列中的最大值,那麼一趟快速排序就變成了一趟冒泡排序。
3.2 代碼
兩種版本,第一種是參考《資料結構》,在網上這種寫法很流行。第二種是參考《演算法導論》,實現起來較複雜。
//快速排序(兩端交替著向中間掃描)void quickSort1(int *a,int low,int high){int pivotkey=a[low];//以a[low]為樞紐值int i=low,j=high;if(low>=high)return;//一趟快速排序while(i<j){//雙向掃描while(i < j && a[j] >= pivotkey)j--;a[i]=a[j];while(i < j && a[i] <= pivotkey)i++;a[j]=a[i];}a[i]=pivotkey;//放置樞紐值//分別對左邊、右邊排序 quickSort1(a,low,i-1); quickSort1(a,i+1,high); }//快速排序(以最後一個記錄的值為樞紐值,單向掃描數組)void quickSort2(int *a,int low,int high){int pivotkey=a[high];//以a[high]為樞紐值 int i=low-1,temp,j;if(low>=high)return;//一趟快速排序for(j=low;j<high;j++){if(a[j]<=pivotkey){i++; temp=a[i];a[i]=a[j];a[j]=temp;}}i++;//放置樞紐值temp=a[i];a[i]=pivotkey;a[high]=temp;//分別對左邊、右邊排序 quickSort2(a,low,i-1); quickSort2(a,i+1,high); }
3.3 效率分析
快速排序時間與劃分是否對稱有關。快速排序的平均時間複雜度為o(n*logn),至於為什麼是o(n*logn),請參考《演算法導論》第7章,書中用遞迴樹的方法闡述了快速排序平均時間。且常數因子很小,所以就平均時間而言,快速排序是很好的內部排序方法。最佳情況下(每次劃分都對稱)時間複雜度o(n*logn)。最壞情況下(每次劃分都不對稱,如輸入的序列有序或者逆序時)時間複雜度為o(n^2),所以在待排序序列有序或逆序時不宜選用快速排序。此外,快速排序是不穩定的。
最佳情況下,每次劃分都是對稱的,由於樞紐值不再考慮,所以得到的兩個子問題的大小不可能大於n/2,同時一趟快速排序時間為o(n),所以已耗用時間遞迴運算式:
T(n)<=2T(n/2)+o(n)。這個遞迴式的解法請參考下一篇部落格中歸併排序效率分析。其解為T(n)=o(n*logn)。
最壞情況下,每次劃分都很不對稱,T(n)=T(n-1)+o(n),可以用遞迴樹來解,第i層的代價為n-i+1.總共有n層。把每一層代價加起來有n-1個n相加。所以這個遞迴式的解為T(n)=o(n^2),此時就是冒泡排序。
4、附錄 4.1 參考書籍
《資料結構》嚴蔚敏版 《演算法導論》
4.2 所有原始碼
#include<stdio.h>//冒泡排序void bubbleSort(int *a,int n){int i,j;for(i=1;i<n;i++)for(j=1;j<n-i+1;j++){if(a[j+1]<a[j]){a[j]=a[j]+a[j+1];a[j+1]=a[j]-a[j+1];a[j]=a[j]-a[j+1];}}}void main(){int i;int a[7]={0,3,5,8,9,1,2};//不考慮a[0]bubbleSort(a,6);for(i=1;i<=6;i++)printf("%-4d",a[i]);printf("\n");}
#include<stdio.h>//快速排序(兩端交替著向中間掃描)void quickSort1(int *a,int low,int high){int pivotkey=a[low];//以a[low]為樞紐值int i=low,j=high;if(low>=high)return;//一趟快速排序while(i<j){//雙向掃描while(i < j && a[j] >= pivotkey)j--;a[i]=a[j];while(i < j && a[i] <= pivotkey)i++;a[j]=a[i];}a[i]=pivotkey;//放置樞紐值//分別對左邊、右邊排序 quickSort1(a,low,i-1); quickSort1(a,i+1,high); }//快速排序(以最後一個記錄的值為樞紐值,單向掃描數組)void quickSort2(int *a,int low,int high){int pivotkey=a[high];//以a[high]為樞紐值 int i=low-1,temp,j;if(low>=high)return;//一趟快速排序for(j=low;j<high;j++){if(a[j]<=pivotkey){i++; temp=a[i];a[i]=a[j];a[j]=temp;}}i++;//放置樞紐值temp=a[i];a[i]=pivotkey;a[high]=temp;//分別對左邊、右邊排序 quickSort2(a,low,i-1); quickSort2(a,i+1,high); }void main(){int i;int a[7]={0,3,5,8,9,1,2};//不考慮a[0]quickSort2(a,1,6);quickSort1(a,1,6);for(i=1;i<=6;i++)printf("%-4d",a[i]);printf("\n");}