【演算法】深入排序演算法的多語言實現
作者:白寧超
2015年10月8日20:08:11
摘要:十一假期於實驗室無趣,逐研究起資料結構之排序。起初覺得就那麼幾種排序,兩三天就搞定了,後來隨著研究的深入,發覺裡面有不少東西。本文介紹常用的排序演算法,主要從以下幾個方面:演算法的介紹、演算法思想、演算法步驟、演算法優缺點、演算法實現、執行結果、演算法優化等。最後對本文進行總結。本文為作者原創,程式經測試無誤。部分資料引用論文和網路材料以及部落格,後續參見參考文獻。()
1 排序的基本概念
排序: 所謂排序,就是要整理檔案中的記錄,使之按關鍵字遞增(或遞減)次序排列起來。其確切定義如下:
輸入:n個記錄R1,R2,…,Rn,其相應的關鍵字分別為K1,K2,…,Kn。
輸出:Ril,Ri2,…,Rin,使得Ki1≤Ki2≤…≤Kin。(或Ki1≥Ki2≥…≥Kin)。
排序的穩定性:當待排序記錄的關鍵字均不相同時,排序結果是惟一的,否則排序結果不唯一。在待排序的檔案中,若存在多個關鍵字相同的記錄,經過排序後這些具有相同關鍵字的記錄之間的相對次序保持不變,該排序方法是穩定的;若具有相同關鍵字的記錄之間的相對次序發生變化,則稱這種排序方法是不穩定的。
注意: 排序演算法的穩定性是針對所有輸入例項而言的。即在所有可能的輸入例項中,只要有一個例項使得演算法不滿足穩定性要求,則該排序演算法就是不穩定的。
排序方法的分類:
1.按是否涉及資料的內、外存交換分
在排序過程中,若整個檔案都是放在記憶體中處理,排序時不涉及資料的內、外存交換,則稱之為內部排序(簡稱內排序);反之,若排序過程中要進行資料的內、外存交換,則稱之為外部排序。
注意: ① 內排序適用於記錄個數不很多的小檔案 ② 外排序則適用於記錄個數太多,不能一次將其全部記錄放人記憶體的大檔案。
2.按策略劃分內部排序方法
可以分為五類:插入排序、選擇排序、交換排序、歸併排序和分配排序。
排序演算法分析
1.排序演算法的基本操作 :
(1) 比較兩個關鍵字的大小;
(2) 改變指向記錄的指標或移動記錄本身。
注意:第(2)種基本操作的實現依賴於待排序記錄的儲存方式。
2.待排檔案的常用儲存方式
(1) 以順序表(或直接用向量)作為儲存結構
排序過程:對記錄本身進行物理重排(即通過關鍵字之間的比較判定,將記錄移到合適的位置)
(2) 以連結串列作為儲存結構
排序過程:無須移動記錄,僅需修改指標。通常將這類排序稱為連結串列(或鏈式)排序;
(3) 用順序的方式儲存待排序的記錄,但同時建立一個輔助表(如包括關鍵字和指向記錄位置的指標組成的索引表)
排序過程:只需對輔助表的表目進行物理重排(即只移動輔助表的表目,而不移動記錄本身)。適用於難於在連結串列上實現,仍需避免排序過程中移動記錄的排序方法。
3.排序演算法效能評價
(1) 評價排序演算法好壞的標準
① 執行時間和所需的輔助空間 ② 演算法本身的複雜程度
(2) 排序演算法的空間複雜度
若排序演算法所需的輔助空間並不依賴於問題的規模n,即輔助空間是O(1),則稱之為就地排序(In-PlaceSou)。 非就地排序一般要求的輔助空間為O(n)。
(3) 排序演算法的時間開銷
大多數排序演算法的時間開銷主要是關鍵字之間的比較和記錄的移動。有的排序演算法其執行時間不僅依賴於問題的規模,還取決於輸入例項中資料的狀態。
檔案的順序儲存結構表示
#define n l00 //假設的檔案長度,即待排序的記錄數目 typedef int KeyType; //假設的關鍵字型別 typedef struct{ //記錄型別 KeyType key; //關鍵字項 InfoType otherinfo;//其它資料項,型別InfoType依賴於具體應用而定義 }RecType; typedef RecType SeqList[n+1];//SeqList為順序表型別,表中第0個單元一般用作哨兵
2 交換排序
交換排序的基本思想是:兩兩比較待排序記錄的關鍵字,發現兩個記錄的次序相反時即進行交換,直到沒有反序的記錄為止。
應用交換排序基本思想的主要排序方法有:氣泡排序和快速排序。
2.1 氣泡排序
氣泡排序:一種簡單直觀的排序演算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個演算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端。
演算法步驟:
1)比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
2)對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
3)針對所有的元素重複以上的步驟,除了最後一個。
4)持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
排序演算法特點,演算法複雜度
時間複雜度為O(n^2),雖然不及堆排序、快速排序的O(nlogn,底數為2),但是有兩個優點:1.“程式設計複雜度”很低,很容易寫出程式碼;2.具有穩定性。
其中若記錄序列的初始狀態為"正序",則氣泡排序過程只需進行一趟排序,在排序過程中只需進行n-1次比較,且不移動記錄;反之,若記錄序列的初始狀態為"逆序",則需進行n(n-1)/2次比較和記錄移動。因此氣泡排序總的時間複雜度為O(n*n)。
氣泡排序示意圖:
氣泡排序示意圖
資料結構演算法的實現:
void BubbleSort(SeqList R) { //R(l..n)是待排序的檔案,採用自下向上掃描,對R做氣泡排序 int i,j; Boolean exchange; //交換標誌 for(i=1;i<n;i++){ //最多做n-1趟排序 exchange=FALSE; //本趟排序開始前,交換標誌應為假 for(j=n-1;j>=i;j--) //對當前無序區R[i..n]自下向上掃描 if(R[j+1].key<R[j].key){//交換記錄 R[0]=R[j+1]; //R[0]不是哨兵,僅做暫存單元 R[j+1]=R[j]; R[j]=R[0]; exchange=TRUE; //發生了交換,故將交換標誌置為真 } if(!exchange) //本趟排序未發生交換,提前終止演算法 return; } //endfor(外迴圈) } //BubbleSort
排序演算法的java實現
package com.multiplesort.bnc; import java.util.Arrays; /** * 各種排序演算法分析比較之氣泡排序:【交換排序】 * @author bnc * @see http://www.cnblogs.com/liuling/p/2013-7-24-01.html */ public class BubbleSort { /** * 隨機生成從0-n的隨機陣列 * @param n 陣列的成都 * @return resultArr 陣列 * @author 白寧超 */ public static int[] randomArray(int arrayLength,int maxNum){ int[] array=new int[arrayLength]; for(int i=0;i<array.length;i++){ array[i]=(int)(Math.random()*maxNum); } return array; } /** * 資料交換 * @param data 整數型陣列 * @param i 第一層迴圈指標 * @param j 第二層迴圈指標 */ private static void swap(int[] data, int i, int j) { int temp=data[i]; data[i]=data[j]; data[j]=temp; } /** * 簡單的氣泡排序:穩定 * @deprecated :氣泡排序是一種穩定的排序方法。 •若檔案初狀為正序,則一趟起泡就可完成排序,排序碼的比較次數為n-1,且沒有記錄移動,時間複雜度是O(n) •若檔案初態為逆序,則需要n-1趟起泡,每趟進行n-i次排序碼的比較,且每次比較都移動三次,比較和移動次數均達到最大值∶O(n^2) •起泡排序平均時間複雜度為O(n^2) * @param data 整數陣列 */ public static void BubbleSort0(int[] array){ int i,j; for(i=0;i<array.length;i++){ for(j=i+1;j<array.length;j++){ if(array[i]>array[j]) swap(array,i,j);//資料交換 } } System.out.println(); System.out.println("簡單【氣泡排序】後的結果:"); for (i = 0; i < array.length; i++) { System.out.print(array[i]+" "); } } /** * 改進後的氣泡排序:穩定 * @deprecated :氣泡排序是一種穩定的排序方法。 •若檔案初狀為正序,則一趟起泡就可完成排序,排序碼的比較次數為n-1,且沒有記錄移動,時間複雜度是O(n) •若檔案初態為逆序,則需要n-1趟起泡,每趟進行n-i次排序碼的比較,且每次比較都移動三次,比較和移動次數均達到最大值∶O(n^2) •起泡排序平均時間複雜度為O(n^2) * @param data 整數陣列 */ public static void BubbleSort1(int[] array){ int i,j; for(i=0;i<array.length;i++){ for(j=array.length-2;j>=i;j--){ if(array[j]>array[j+1]){ // System.out.println(array[j]+"<--->"+array[j+1]);//測試結果前面排序影響後面,相當於是從快取有序的陣列中獲取 swap(array,j,j+1);//資料交換 } } } System.out.println(); System.out.println("改進後【氣泡排序】的結果:"); for (i = 0; i < array.length; i++) { System.out.print(array[i]+" "); } } /** * 當陣列基本有序時,如何改進排序演算法 * @param array */ public static void BubbleSort2(int[] array){ int i,j; Boolean flag=true; for(i=0;i<array.length&&flag;i++){//如果flag為flag退出迴圈 flag=false; for(j=array.length-2;j>=i;j--){ if(array[j]>array[j+1]){ swap(array,j,j+1);//資料交換 //System.out.println(array[j]+"<--->"+array[j+1]); flag=true;//如果有資料交換,則flag為true } } } System.out.println(); System.out.println("基本有序陣列【氣泡排序】的結果:"); for (i = 0; i < array.length; i++) { System.out.print(array[i]+" "); } } public static void main(String[] args) { int[] array=randomArray(20, 100);//隨機生成0--100的20個長度的陣列 int[] array1={1,3,2,4,5,6};//基本有序陣列 System.out.println("氣泡排序前:"); for(int i=0;i<array.length;i++){ System.out.print(array[i]+" "); } System.out.println(); System.out.println("使用內部排序的結果:"); for(int i=0;i<array.length;i++){ //使用內部排序的結果 Arrays.sort(array);//內部排序 System.out.print(array[i]+" "); } //BubbleSort0(array); //BubbleSort1(array); BubbleSort2(array1); } }View Code
排序演算法的phthon實現
def bubble_sort(lists): # 氣泡排序 count = len(lists) for i in range(0, count): for j in range(i + 1, count): if lists[i] > lists[j]: lists[i], lists[j] = lists[j], lists[i] return lists
2.2 快速排序
快速排序:是由東尼·霍爾所發展的一種排序演算法。在平均狀況下,排序 n 個專案要Ο(n log n)次比較。在最壞狀況下則需要Ο(n2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他Ο(n log n) 演算法更快,因為它的內部迴圈(inner loop)可以在大部分的架構上很有效率地被實現出來。
快速排序使用分治法(Divide and conquer)策略來把一個序列(list)分為兩個子序列(sub-lists)。
演算法步驟:
1 從數列中挑出一個元素,稱為 “基準”(pivot),
2 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分割槽退出之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作。
3 遞迴地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
遞迴的最底部情形,是數列的大小是零或一,也就是永遠都已經被排序好了。雖然一直遞迴下去,但是這個演算法總會退出,因為在每次的迭代(iteration)中,它至少會把一個元素擺到它最後的位置去。
快速排序示意圖:
快速排序示意圖
資料結構演算法的實現:
void QuickSort(SeqList R,int low,int high) { //對R[low..high]快速排序 int pivotpos; //劃分後的基準記錄的位置 if(low<high){//僅當區間長度大於1時才須排序 pivotpos=Partition(R,low,high); //對R[low..high]做劃分 QuickSort(R,low,pivotpos-1); //對左區間遞迴排序 QuickSort(R,pivotpos+1,high); //對右區間遞迴排序 } } //QuickSort
排序演算法的java實現
package com.multiplesort.bnc; /** * 快速排序 * 基本思想:選擇一個基準元素,通常選擇第一個元素或者最後一個元素,通過一趟掃描,將待排序列分成兩部分,一部分比基準元素小,一部分大於等於基準元素, * 此時基準元素在其排好序後的正確位置,然後再用同樣的方法遞迴地排序劃分的兩部分。 * 分析: 快速排序是不穩定的排序。 快速排序的時間複雜度為O(nlogn)。 當n較大時使用快排比較好,當序列基本有序時用快排反而不好。 * @author bnc * */ public class QuickSort { //隨機生成陣列 public static int[] randomArray(int arrayLength,int maxNum){ int[] array=new int[arrayLength]; for(int i=0;i<array.length;i++){ array[i]=(int)(Math.random()*maxNum); } return array; } //資料交換 public static void swap(int[] data,int i,int j){ int temp=data[i]; data[i]=data[j]; data[j]=temp; } //打印出陣列 public static void printArray(int[] array){ for(int i=0;i<array.length;i++){ System.out.print(array[i]+" "); } System.out.println(); } private static void quickSort(int[] array, int low, int high) { if(array.length>0) { if(low<high){ //如果不加這個判斷遞迴會無法退出導致堆疊溢位異常 int middle = getMiddle(array,low,high); quickSort(array, 0, middle-1); quickSort(array, middle+1, high); } } } private static int getMiddle(int[] array, int low, int high) { //int m=low+(high-low)/2;//計算陣列元素中間的下標 int temp = array[low];//基準元素 while(low<high){ //找到比基準元素小的元素位置 while(low<high && array[high]>=temp){ high--; } array[low] = array[high]; while(low<high && array[low]<=temp){ low++; } array[high] = array[low]; } array[low] = temp; return low; } //快排 public static void quickSort(int[] array){ System.out.println("快速排序前的結果"); printArray(array); quickSort(array,0,array.length-1); System.out.println("快速排序後的結果"); printArray(array); } public static void main(String[] args) { quickSort(randomArray(20, 100)); } }View Code
排序演算法的phthon實現
def quick_sort(lists, left, right): # 快速排序 if left >= right: return lists key = lists[left] low = left high = right while left < right: while left < right and lists[right] >= key: right -= 1 lists[left] = lists[right] while left < right and lists[left] <= key: left += 1 lists[right] = lists[left] lists[right] = key quick_sort(lists, low, left - 1) quick_sort(lists, left + 1, high) return lists
3 選擇排序
3.1 直接選擇排序
選擇排序(Selection sort)也是一種簡單直觀的排序演算法。
演算法步驟:
1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
2)再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。
3)重複第二步,直到所有元素均排序完畢。
排序演算法特點,演算法複雜度
選擇排序的交換操作介於0和(n-1)次之間。選擇排序的比較操作為n(n-1)/2次之間。選擇排序的賦值操作介於0和3(n-1)次之間。
比較次數O(n^2),比較次數與關鍵字的初始狀態無關,總的比較次數N=(n-1)+(n-2)+...+1=n*(n-1)/2。 交換次數O(n),最好情況是,已經有序,交換0次;最壞情況是,逆序,交換n-1次。 交換次數比氣泡排序少多了,由於交換所需CPU時間比比較所需的CPU時間多,n值較小時,選擇排序比氣泡排序快。
直接選擇排序示意圖:
選擇排序示意圖
資料結構演算法的實現:
void SelectSort(SeqList R) { int i,j,k; for(i=1;i<n;i++){//做第i趟排序(1≤i≤n-1) k=i; for(j=i+1;j<=n;j++) //在當前無序區R[i..n]中選key最小的記錄R[k] if(R[j].key<R[k].key) k=j; //k記下目前找到的最小關鍵字所在的位置 if(k!=i){ //交換R[i]和R[k] R[0]=R[i];R[i]=R[k];R[k]=R[0]; //R[0]作暫存單元 } //endif } //endfor } //SeleetSort
排序演算法的java實現
package com.multiplesort.bnc; /** * 選擇排序:簡單選擇排序、堆排序。 * 思想:每趟從待排序的記錄序列中選擇關鍵字最小的記錄放置到已排序表的最前位置,直到全部排完。 * 關鍵問題:在剩餘的待排序記錄序列中找到最小關鍵碼記錄。 * •方法: –直接選擇排序 –堆排序 * @author bnc * */ public class SimpleSelectionSort { //隨機生成一組數 public static int[] randomArray(int arrayLength,int maxNum){ int[] array=new int[arrayLength]; for(int i=0;i<array.length;i++){ array[i]=(int)(Math.random()*maxNum); } return array; } //資料交換 public static void swap(int[] data,int i,int j){ int temp=data[i]; data[i]=data[j]; data[j]=temp; } //打印出陣列 public static void printArray(int[] array){ for(int i=0;i<array.length;i++){ System.out.print(array[i]+" "); } System.out.println(); } ////簡單的選擇排序: //基本思想:在要排序的一組數中,選出最小的一個數與第一個位置的數交換;然後在剩下的數當中再找最小的與第二個位置的數交換,如此迴圈到倒數第二個數和最後一個數比較為止。 public static void simpleSeclectSort(int[] array){ int i,j,min; for(i=0;i<array.length;i++){ min=i; //將當前下標定義最小下標 for(j=i+1;j<array.length;j++){ //迴圈之後的資料 if(array[min]>array[j]) //如果有小於當前值的最小資料 min=j; //關鍵字的最小下標賦值給min } if(i!=min){ //若min!=i,說明找到最小值交換 swap(array,i,min); //最小的一個數與第i位置的數交換 } } System.out.println("排序後的陣列:"); printArray(array); } ////簡單的選擇排序: //基本思想:在要排序的一組數中,選出最小的一個數與第一個位置的數交換;然後在剩下的數當中再找最小的與第二個位置的數交換,如此迴圈到倒數第二個數和最後一個數比較為止。 public static void simpleSeclectSort1(int[] array){ int i,j,min; for(i=0;i<array.length;i++){ min=array[i]; int n=i;//將當前下標定義最小下標 for(j=i+1;j<array.length;j++){ //迴圈之後的資料 if(min>array[j]){ //如果有小於當前值的最小資料 min=array[j]; n=j; //關鍵字的最小下標賦值給min } } array[n]=array[i]; array[i]=min; } System.out.println("排序後的陣列:"); printArray(array); } public static void main(String[] args) { int[] array=randomArray(10, 100); System.out.println("排序前的陣列:"); printArray(array); simpleSeclectSort1(array); } }View Code
排序演算法的phthon實現
def select_sort(lists): # 選擇排序 count = len(lists) for i in range(0, count): min = i for j in range(i + 1, count): if lists[min] > lists[j]: min = j lists[min], lists[i] = lists[i], lists[min] return lists
3.2 堆排序
堆排序(Heapsort)是指利用堆這種資料結構所設計的一種排序演算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
堆排序的平均時間複雜度為Ο(nlogn) 。
演算法步驟:
1)建立一個堆H[0..n-1]
2)把堆首(最大值)和堆尾互換
3)把堆的尺寸縮小1,並呼叫shift_down(0),目的是把新的陣列頂端資料調整到相應位置
4) 重複步驟2,直到堆的尺寸為1
排序演算法特點,演算法複雜度
堆排序的平均時間複雜度為O(nlogn),空間複雜度為O(1)。
由於它在直接選擇排序的基礎上利用了比較結果形成。效率提高很大。它完成排序的總比較次數為O(nlog2n)。它是對資料的有序性不敏感的一種演算法。但堆排序將需要做兩個步驟:-是建堆,二是排序(調整堆)。所以一般在小規模的序列中不合適,但對於較大的序列,將表現出優越的效能。
堆排序示意圖:
堆排序示意圖
資料結構演算法的實現:
void HeapSort(SeqIAst R) { //對R[1..n]進行堆排序,不妨用R[0]做暫存單元 int i; BuildHeap(R); //將R[1-n]建成初始堆 for(i=n;i>1;i--){ //對當前無序區R[1..i]進行堆排序,共做n-1趟。 R[0]=R[1];R[1]=R[i];R[i]=R[0]; //將堆頂和堆中最後一個記錄交換 Heapify(R,1,i-1); //將R[1..i-1]重新調整為堆,僅有R[1]可能違反堆性質 } //endfor } //HeapSort
排序演算法的java實現
package com.multiplesort.bnc; import java.util.Arrays; /** * 選擇排序:堆排序 * 適用於大資料 * 穩定性:不穩定 * 堆排序是一種樹形選擇排序,是對直接選擇排序的有效改進。 * 堆的定義下:具有n個元素的序列 (h1,h2,...,hn),當且僅當滿足(hi>=h2i,hi>=2i+1)或(hi<=h2i,hi<=2i+1) (i=1,2,...,n/2)時稱之為堆。 * 在這裡只討論滿足前者條件的堆。由堆的定義可以看出,堆頂元素(即第一個元素)必為最大項(大頂堆)。完全二 叉樹可以很直觀地表示堆的結構。堆頂為根,其它為左子樹、右子樹。 * 思想:初始時把要排序的數的序列看作是一棵順序儲存的二叉樹,調整它們的儲存序,使之成為一個 堆,這時堆的根節點的數最大。然後將根節點與堆的最後一個節點交換。 * 然後對前面(n-1)個數重新調整使之成為堆。依此類推,直到只有兩個節點的堆,並對 它們作交換,最後得到有n個節點的有序序列。 * 從演算法描述來看,堆排序需要兩個過程,一是建立堆,二是堆頂與堆的最後一個元素交換位置。所以堆排序有兩個函式組成。一是建堆的滲透函式,二是反覆呼叫滲透函式實現排序的函式。 * 初始序列:50,10,90,30,70,40,80,60,20 * @author bnc * */ public class HeapSort { //隨機生成一組數 public static int[] randomArray(int arrayLength,int maxNum){ int[] array=new int[arrayLength]; for(int i=0;i<array.length;i++){ array[i]=(int)(Math.random()*maxNum); } return array; } //資料交換 public static void swap(int[] data,int i,int j){ int temp=data[i]; data[i]=data[j]; data[j]=temp; } //打印出陣列 public static void printArray(int[] array){ for(int i=0;i<array.length;i++){ System.out.print(array[i]+" "); } System.out.println(); } /** *構建大頂堆 * @param array 待建堆的資料 * @param lastIndex 從lastIndex處節點(最後一個節點)的父節點開始 */ public static void buildMaxHeap(int[] array,int lastIndex){ for(int i=(lastIndex-1)/2;i>=0;i--){ int k=i; //k儲存正在判斷的節點 while(k*2+1<=lastIndex){ //如果當前k節點的子節點存在 int biggerIndex=2*k+1; //k節點的左子節點的索引 if(biggerIndex<lastIndex){ //如果biggerIndex小於lastIndex,即biggerIndex+1代表的k節點的右子節點存在 if(array[biggerIndex]<array[biggerIndex+1]) //若果右子節點的值較大 biggerIndex++; //biggerIndex總是記錄較大子節點的索引 } if(array[k]<array[biggerIndex]){ //如果k節點的值小於其較大的子節點的值 swap(array,k,biggerIndex); k=biggerIndex; //將biggerIndex賦予k,開始while迴圈的下一次迴圈,重新保證k節點的值大於其左右子節點的 } else break; } } } //堆排序 public static void heapSort(int[] array){ int arrayLength=array.length; //迴圈建堆 for(int i=0;i<arrayLength-1;i++){ buildMaxHeap(array,arrayLength-1-i); //交換堆頂和最後一個元素 swap(array,0,arrayLength-1-i); } printArray(ar