資料結構-深入淺出細談八大排序
1.排序的基本概念:
排序是各門語言中的核心,也是計算機資料處理中的核心運算,是我們學過的“資料結構與演算法”課程的重點。排序演算法能夠體現演算法設計和演算法分析的精神。有效的排序演算法在一些演算法(例如搜尋演算法與合併演算法)中是重要的,如此這些演算法才能得到正確解答。 這篇博文主要包含了8大內部排序的演算法複雜度,穩定性以及描述演算法和視覺化過程,花時間總結了很久,但是肯定仍有不足,希望各位大神能指點迷津。
小注:剛發現,視覺化過程的圖片是gif格式,但是傳上去之後好像不動,不好意思。請在點連線:視覺化檢視視覺直觀感受 7 種常用的排序演算法(在最後的參考資料中也有)
(1)排序演算法的輸出必須遵守下列兩個原則:
a)輸出結果為遞增序列(遞增是針對所需的排序順序而言);
b)輸出結果是原輸入的一種排列、或是重組。
(2)被排序的物件-檔案
被排序的物件--檔案由一組記錄組成。記錄則由若干個資料項(或域)組成。其中有一項可用來標識一個記錄,稱為關鍵字項。該資料項的值稱為關鍵字(Key)。
(3)排序運算的依據--關鍵字
關鍵字,可以是數字型別,也可以是字元型別。 關鍵字的選取應根據問題的要求而定。
2.排序的分類
1)按是否涉及內外存交換:
(1) 內部排序:
待排序的記錄全部存放在記憶體中進行排序的過程。
(2) 外部排序:
待排序的記錄的數量很大,以至於記憶體不能容納全部記錄,在排序過程中需要對外存進行訪問的排序過程。
2) 按策略劃分內部排序方法
(1) 插入排序:
直接插入排序,折半插入排序;
(2) 選擇排序:
簡單選擇排序,堆排序;
(3) 交換排序:
快速排序,氣泡排序;
(4) 歸併排序:
歸併排序;
(5) 分配排序:
基數排序;
3)按穩定性劃分內部排序
(1)穩定排序:
直接插入排序,氣泡排序,歸併排序,基數排序
(2)不穩定排序:
簡單選擇排序,希爾排序,快速排序,堆排序
3.內部排序演算法的操作
大多數排序演算法都有兩個基本的操作:比較和移動;
(1) 比較兩個關鍵字的大小;
(2) 改變指向記錄的指標或移動記錄本身。
操作的實現依賴於待排序記錄的儲存方式(①待排序的記錄存放在連續的一組儲存單元上,類似於線性表的順序儲存
;②待排序的記錄存放在靜態連結串列中;③待排序的記錄本身儲存在一組地址連續的儲存單元內,同時另設一個指示各個記錄儲存位置的地址向量,排序時不移動記錄本身,而是移動地址向量中這些記錄的地址)。
4.內部排序各演算法程式碼圖示詳解
(1) 氣泡排序
a)氣泡排序程式碼:
void bubbleSort(){ for(i=0;i<n-1;i++){ change=false; for(j=0;j<n-i-1;j++){ if(a[j]>a[j+1]){ a[j]←→a[j+1]; change=true; } if(!change){ break; } } } }
b)氣泡排序的視覺化試圖:
(2) 選擇排序:
a)選擇排序程式碼:
void selectionSort(){ for(i=0;i<n-1;i++){ k=i; for(j=i+1;j<n;j++){ if(a[j]<a[k]){ k=j; } if(k!=i){ a[i]←→a[k]; } } } }
b)選擇排序的視覺化試圖:
(3) 插入排序
a)插入排序程式碼:
//直接插入排序 void InsertSort(int array[], int size) { for(int i = 2; i < size; i++ ) { if(array[i] < array[i-1]) { array[0] = array[i]; int j; for(j = i - 1; array[0] < array[j]; j--) { array[j+1] =array[j]; } array[j+1] = array[0]; } } for(int i = 1; i < size; i++) { cout << array[i] << endl; } }
(4) 快速排序
a)快速排序程式碼:
void QiuckSort(){ If(low<high){ Pivot=a[low]; i=low; j=high; while(i<j){ while(i<j&&a[j]>=pivot){ j--; a[i] ←→a[j]; } while(i<j&&a[j]<=pivot){ i++; a[i] ←→a[j]; } a[i]=pivot; QiuckSort(a,low,i-1); QiuckSort(a,i-1,high); } } }
b)快速排序的視覺化試圖:
(5)歸併排序
a)歸併排序程式碼:
//歸併操作 void Merge(int sourceArr[], int targetArr[], int startIndex, int midIndex, int endIndex) { int i, j, k; for(i = midIndex+1, j = startIndex; startIndex <= midIndex && i <= endIndex; j++) { if(sourceArr[startIndex] < sourceArr[i]) { targetArr[j] = sourceArr[startIndex++]; } else { targetArr[j] = sourceArr[i++]; } } if(startIndex <= midIndex) { for(k = 0; k <= midIndex-startIndex; k++) { targetArr[j+k] = sourceArr[startIndex+k]; } } if(i <= endIndex) { for(k = 0; k <= endIndex- i; k++) { targetArr[j+k] = sourceArr[i+k]; } } } //內部使用遞迴,空間複雜度為n+logn void MergeSort(int sourceArr[], int targetArr[], int startIndex, int endIndex) { int midIndex; int tempArr[100]; //此處大小依需求更改 if(startIndex == endIndex) { targetArr[startIndex] = sourceArr[startIndex]; } else { midIndex = (startIndex + endIndex)/2; MergeSort(sourceArr, tempArr, startIndex, midIndex); MergeSort(sourceArr, tempArr, midIndex+1, endIndex); Merge(tempArr, targetArr,startIndex, midIndex, endIndex); } } //呼叫 int _tmain(int argc, _TCHAR* argv[]) { int a[8]={50,10,20,30,70,40,80,60}; int b[8]; MergeSort(a, b, 0, 7); for(int i = 0; i < sizeof(a) / sizeof(*a); i++) cout << b[i] << ' '; cout << endl; system("pause"); return 0; }
b)歸併排序的視覺化試圖:
(6) 基數排序
a)基數排序的程式碼:
#include <stdlib.h> #include <math.h> testBS() { int a[] = {2,343,342,1,123,43,4343,433,687,654,3}; int *a_p = a; //計算陣列長度 int size = sizeof(a)/sizeof(int); //基數排序 bucketSort3( a_p , size ); //列印排序後結果 int i ; for(i = 0 ; i < size ; i++ ) { printf("%d\n ",a[i]); } int t; scanf("%d",t); } //基數排序 void bucketSort3(int *p , int n) { //獲取陣列中的最大數 int maxNum = findMaxNum( p , n ); //獲取最大數的位數,次數也是再分配的次數。 int loopTimes = getLoopTimes(maxNum); int i ; //對每一位進行桶分配 for( i = 1 ; i <= loopTimes ; i++) { sort2(p , n , i ); } } //獲取數字的位數 int getLoopTimes(int num) { int count = 1 ; int temp = num/10; while( temp != 0 ) { count++; temp = temp / 10; } return count; } //查詢陣列中的最大數 int findMaxNum( int *p , int n) { int i ; int max = 0; for( i = 0 ; i < n ; i++) { if(*(p+i) > max) { max = *(p+i); } } return max; } //將數字分配到各自的桶中,然後按照桶的順序輸出排序結果 void sort2(int *p , int n , int loop) { //建立一組桶 此處的20是預設的 根據實際數情況修改 int buckets[10][20] = {} ; //求桶的index的除數 //如798 個位桶index = ( 798 / 1 ) % 10 = 8 // 十位桶index = ( 798 / 10 ) % 10 = 9 // 百位桶index = ( 798 / 100 ) % 10 = 7 // tempNum 為上式中的1、10、100 int tempNum = (int) pow(10 , loop-1); int i , j ; for( i = 0 ; i < n ; i++ ) { int row_index = (*(p+i) / tempNum) % 10; for(j = 0 ; j < 20 ; j++) { if(buckets[row_index][j] ==NULL) { buckets[row_index ][j] = *(p+i) ; break; } } } //將桶中的數,倒回到原有陣列中 int k = 0 ; for(i = 0 ; i < 10 ; i++) { for(j = 0 ; j < 20 ; j++) { if(buckets[i][j] != NULL) { *(p + k ) = buckets[i][j] ; buckets[i][j]=NULL; k++; } } } }
(7)希爾排序(shell)
a)希爾排序程式碼:
void ShallSort(T a[] ,int n){ d=n/2; while(d=){//一趟希爾排序,對d個序列分別進行插入排序 for(i=d;i<n;i++){ x=a[i]; } for(j=i-d;j>=0&&x<a[j];j-=d){ a[j+d]=a[j]; a[j+d]=x; } d=d/2; } }
b)希爾排序的視覺化試圖:
(8)堆排序
a)堆排序的視覺化試圖:
// array是待調整的堆陣列,i是待調整的陣列元素的位置,nlength是陣列的長度 //本函式功能是:根據陣列array構建大根堆 void HeapAdjust(int array[], int i, int nLength) { int nChild; int nTemp; for (nTemp = array[i]; 2 * i + 1 < nLength; i = nChild) { // 子結點的位置=2*(父結點位置)+ 1 nChild = 2 * i + 1; // 得到子結點中較大的結點 if ( nChild < nLength-1 && array[nChild + 1] > array[nChild]) ++nChild; // 如果較大的子結點大於父結點那麼把較大的子結點往上移動,替換它的父結點 if (nTemp < array[nChild]) { array[i] = array[nChild]; array[nChild]= nTemp; } else // 否則退出迴圈 break; } } // 堆排序演算法 void HeapSort(int array[],int length) { int tmp; // 調整序列的前半部分元素,調整完之後第一個元素是序列的最大的元素 //length/2-1是第一個非葉節點,此處"/"為整除 for (int i = length / 2 - 1; i >= 0; --i) HeapAdjust(array, i, length); // 從最後一個元素開始對序列進行調整,不斷的縮小調整的範圍直到第一個元素 for (int i = length - 1; i > 0; --i) { // 把第一個元素和當前的最後一個元素交換, // 保證當前的最後一個位置的元素都是在現在的這個序列之中最大的 /// Swap(&array[0], &array[i]); tmp = array[i]; array[i] = array[0]; array[0] = tmp; // 不斷縮小調整heap的範圍,每一次調整完畢保證第一個元素是當前序列的最大值 HeapAdjust(array, 0, i); } }
5.內部排序演算法的分析
(1)時間複雜度:
一個演算法執行所耗費的時間,從理論上是不能算出來的,必須上機執行測試才能知道。但我們不可能也沒有必要對每個演算法都上機測試,只需知道哪個演算法花費的時間多,哪個演算法花費的時間少就可以了。並且一個演算法花費的時間與演算法中語句的執行次數成正比例,哪個演算法中語句執行次數多,它花費時間就多。一個演算法中的語句執行次數稱為語句頻度或時間頻度。記為T(n)。
(2)空間複雜度:
一個程式的空間複雜度是指執行完一個程式所需記憶體的大小。利用程式的空間複雜度,可以對程式的執行所需要的記憶體多少有個預先估計。一個程式執行時除了需要儲存空間和儲存本身所使用的指令、常數、變數和輸入資料外,還需要一些對資料進行操作的工作單元和儲存一些為現實計算所需資訊的輔助空間。
(3)內部排序演算法複雜度的總結:
6.內部排序演算法的效能比較
1)排序的穩定性及分析:
假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,ri=rj,且ri在rj之前,而在排序後的序列中,ri仍在rj之前,則稱這種排序演算法是穩定的;否則稱為不穩定的。
(1)氣泡排序
氣泡排序就是把小的元素往前調或者把大的元素往後調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。我們知道,氣泡排序的交換條件是:a[j]>a[j+1]或者a[j]<a[j+1]很明顯不包括相等的情況,所以如果兩個元素相等,他們不會交換;如果兩個相等的元素沒有相鄰,那麼即使通過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,所以相同元素的前後順序不會改變,所以氣泡排序是一種穩定排序演算法。
(2)選擇排序
選擇排序是給每個位置選擇當前元素最小的,比如給第一個位置選擇最小的,在剩餘元素裡面給第二個元素選擇第二小的,依次類推,直到第n-1個元素,第n個元素不用選擇了,因為只剩下它一個最大的元素了。那麼,在一趟選擇,如果當前元素比一個元素小,而該小的元素又出現在一個和當前元素相等的元素後面,那麼交換後穩定性就被破壞了。比較拗口,舉個例子,序列5 8 5 2 9, 我們知道第一遍選擇第1個元素5會和2交換,那麼原序列中2個5的相對前後順序就被破壞了,所以選擇排序不是一個穩定的排序演算法。
(3)插入排序
插入排序是在一個已經有序的小序列的基礎上,一次插入一個元素。當然,剛開始這個有序的小序列只有1個元素,就是第一個元素。比較是從有序序列的末尾開始,也就是想要插入的元素和已經有序的最大者開始比起,如果比它大則直接插入在其後面,否則一直往前找直到找到它該插入的位置。如果碰見一個和插入元素相等的,那麼插入元素把想插入的元素放在相等元素的後面。即和氣泡排序一樣:a[j]>a[j+1]或者a[j]<a[j+1]很明顯不包括相等的情況,所以如果兩個元素相等,他們不會交換;所以,相等元素的前後順序沒有改變,從原無序序列出去的順序就是排好序後的順序,所以插入排序是穩定的。
(4)快速排序
快速排序有兩個方向,左邊的i下標一直往右走,當a[i] <= [center_index](center_index中樞元素的陣列下標),一般取為陣列第0個元素。而右邊的j下標一直往左走,當a[j] > a[center_index]。如果i和j都走不動了,i <= j, 交換a[i]和a[j],重複上面的過程,直到i>j。 交換a[j]和a[center_index],完成一趟快速排序。在中樞元素和a[j]交換的時候,很有可能把前面的元素的穩定性打亂,比如序列為 5,3,3,4,3,8,9,10,11, 現在中樞元素5和3(第5個元素,下標從1開始計)交換就會把元素3的穩定性打亂,所以快速排序是一個不穩定的排序演算法,不穩定發生在中樞元素和a[j]交換的時刻。
快速排序是高效排序演算法了。實踐證明,快速排序是所有排序演算法中最高效的一種。它採用了分治的思想:先保證列表的前半部分都小於後半部分,然後分別對前半部分和後半部分排序,這樣整個列表就有序了。這是一種先進的思想,也是它高效的原因。因為在排序演算法中,演算法的高效與否與列表中數字間的比較次數有直接的關係,而"保證列表的前半部分都小於後半部分"就使得前半部分的任何一個數從此以後都不再跟後半部分的數進行比較了,大大減少了數字間不必要的比較。但查詢資料得另當別論了。
(5)歸併排序
所謂“歸併”,試講兩個或兩個以上的有序檔案合併成一個新的有序檔案。歸併排序是把一個有n個記錄的無序檔案看成是由n個長度為1的有序子檔案組成的檔案,然後進行兩兩歸併,得到[n/2]個長度為2或1的有序檔案,再兩兩歸併,如此重複,直至最後形成包含n個記錄的有序檔案為止。所以,歸併排序也是穩定的排序演算法。
(6)基數排序
基數排序的思想是按組成關鍵字的各個數位的值進行排序,他是分配排序的一種。基數排序是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先順序順序的,先按低優先順序排序,再按高優先級排序,最後的次序就是高優先順序高的在前,高優先順序相同的低優先順序高的在前。為了減少記錄的移動次數,佇列可以採用鏈式儲存分配,稱為鏈佇列。基數排序基於分別排序,分別收集,所以其是穩定的排序演算法。
(7)希爾排序(shell)
希爾排序又稱為“縮小增量排序”是按照不同步長對元素進行插入排序,當剛開始元素很無序的時候,步長最大,所以插入排序的元素個數很少,速度很快;當元素基本有序了,步長很小,插入排序對於有序的序列效率很高。關鍵步驟是取增量d,那全體記錄分成d組,進行直接插入排序,直到d=1.所以,希爾排序的時間複雜度會比o(n^2)好一些。由於多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂,所以shell排序是不穩定的。
(8)堆排序
我們知道堆的結構是節點i的孩子為2*i和2*i+1節點,大頂堆要求父節點大於等於其2個子節點,小頂堆要求父節點小於等於其2個子節點。在一個長為n的序列,堆排序的過程是從第n/2開始和其子節點共3個值選擇最大(大頂堆)或者最小(小頂堆),這3個元素之間的選擇當然不會破壞穩定性。但當為n /2-1, n/2-2, ...1這些個父節點選擇元素時,就會破壞穩定性。有可能第n/2個父節點交換把後面一個元素交換過去了,而第n/2-1個父節點把後面一個相同的元素沒有交換,那麼這2個相同的元素之間的穩定性就被破壞了。所以,堆排序不是穩定的排序演算法。
參考資料:
3.資料結構(C語言版) 清華大學出版社 嚴蔚敏
4.軟體設計師教程 清華大學出版社 胡聖明