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");
- }