排序是計算機程序設計中的一種重要操做。若是數據可以根據某種規則排序,就能大大挺高數據處理的算法效率。html
排序就是整理文件中的記錄,使之按關鍵字遞增(或遞減)的次序排列起來。前端
排序的對象是文件,它由一組記錄組成。每條記錄則由一個或若干個數據項(或域)組成。java
關鍵字項就是可用來標識一個記錄的一個或多個組合的數據項。該數據項的值稱爲關鍵字(key)。須要注意的是,在不易產生混淆時,可將關鍵字項簡稱爲關鍵字。算法
用來做爲排序運算的關鍵字,能夠是數字類型,也能夠是字符類型。關鍵字的選取應根據問題的要求而定。shell
當待排序記錄的關鍵字均不相同時,排序結果是唯一的,不然排序結果不唯一。後端
在待排序的文件中,若存在多個關鍵字相同的記錄,通過排序後這些具備相同關鍵字的記錄之間的相對次序保持不變,該排序方法是穩定的;若具備相同關鍵字的記錄之間的相對次序發生變化,則稱這種排序方法是不穩定的。數組
根據排序時待排序的數據元素數量的不一樣,使得排序過程當中涉及的存儲器不一樣,能夠將排序方法分爲兩類。 一類是整個排序過程在內存儲器中進行,稱爲內部排序,簡稱內排序。數據結構
另外一類是因爲待排序元素數量太大,以致於內存儲器沒法容納所有數據,排序須要藉助外部存儲設備才能完成,這類排序稱爲外部排序。app
通常狀況下,內排序適宜在記錄個數很少的小文件中使用,外排序則適用於記錄個數太多,不能一次將其所有記錄放入內存的大文件。ide
對於外排序,可進一步分爲兩種方法:
對於內排序,按策略進行劃分,能夠分爲:
分析排序算法時,應該考慮比較的次數和數據移動的次數。
具體須要考慮如下3中狀況的比較和移動次數:
不少排序方法在這3種狀況下的性能是迥然不一樣的,因此能夠根據實際條件選擇使用哪種算法。
插入排序的思想:
每次將一個待排序的記錄按其關鍵字大小插入到前面已經排好序的子文件的適當位置,直到所有記錄插入完成爲止。
直接插入排序是一種最簡單的排序方法,它的基本操做是將一個記錄插入到已經排好序的有序表中,從而獲得一個新的有序表。
假設待排序的記錄存放在數組R[1……n]中,排序過程當中,R被分爲兩個子區間R[1……i]和R[i+1……n],其中R[1……i]是已經排好序的有序區;R[i+1……n]是當前未排序的部分。將當前無序區的第一個記錄R[i+1]插入到有序區R[1……i]的適當位置,使R[1……i+1]變爲新的有序區,每次插入一個數據,知道全部的數據有序爲止。
通常來講,插入排序都採用in-place在數組上實現。具體算法描述以下:
// 插入排序 public static int[] insertionSort(int[] arr) { int preIndex, temp; //比較的論數 for (int i = 1; i < arr.length; i++) { //當前下標的前一位下標 preIndex = i - 1; //當前位置的元素 temp = arr[i]; //前一位下標不越界而且前一位的元素值大於當前位置的元素值 while (preIndex >= 0 && arr[preIndex] > temp) { //移動元素位置 arr[preIndex + 1] = arr[preIndex]; //下標前移繼續比較 preIndex--; } //插入相應位置 arr[preIndex + 1] = temp; } return arr; }
對於具備n個記錄的文件,要進行n-1趟排序。 各類狀態下的時間複雜度以下表:
算法所需的輔助空間是一個監視哨,輔助空間負責度S(n)=O(1),是一個就地排序。
直接插入排序是穩定的排序方法。
希爾排序是插入排序的一種,因D.L.Shell於1959年提出而得名。希爾排序也稱做縮減增量排序(diminishing increment sort)
先取定一個小於n的整數d1做爲第一個增量,把文件的所有記錄分紅d1個組,全部距離爲d1的倍數的記錄放在同一個組中,在各組內進行插入排序;而後,取第二個增量d2<d1,重複上述的分組和排序,直至所取的增量dt=1(dt<……<d2<d1),即全部記錄放在同一組中進行直接插入排序爲止。
先將整個待排序的記錄序列分割成爲若干子序列分別進行直接插入排序,具體算法描述:
// 希爾排序 public static void shellSort2(int[] arr) { int j; // 初始值爲數組長度的一半,而後按照一半的一半遞減至1 for (int gap = arr.length / 2; gap > 0; gap /= 2) { // 初始取值爲數組長度的一半,而後加一遞增,不能超過數組長度 for (int i = gap; i < arr.length; i++) { int tmp = arr[i];// 取出數組的一半位置的元素。 // 比較步長兩端的元素,若是後端小於前端 for (j = i; j >= gap && tmp - arr[j - gap] < 0; j -= gap) { arr[j] = arr[j - gap];// 將前端的值賦予後端 } // 此時j減去了gap則爲前端的位置,將後端的值賦予前端。完成不一樣位置的元素交換。 arr[j] = tmp; } } }
Shell排序的執行時間依賴於增量序列。
好的在增量序列的共同特徵爲:
希爾排序的時間性能優於直接插入排序,緣由以下:
希爾排序是一種不穩定的排序方法。
交換排序的基本思想:
兩兩比較待排序記錄的關鍵字,發現兩個記錄的次序相反時,即進行交換,直到沒有反序的記錄爲止。 應用交換排序基本思想的主要排序方法有冒泡和快速排序。
設想被排序的記錄關鍵字保存在數組R[1……n]中,將每一個記錄R[i]看作是重量爲R[i].key的氣泡。根據輕氣泡不能在重氣泡之下的原則,從下往上掃描數組R;凡掃描到違反本原則的輕氣泡,就使其向上「漂浮」。如此反覆進行,直到最後任何兩個氣泡都輕者在上,重者在下爲止。
// 冒泡排序 public static void bubbleSort(int[] arr) { // 比較的輪數 for (int i = 0; i < arr.length - 1; i++) { // 每輪比較的次數 for (int j = 0; j < arr.length - 1 - i; j++) { // 從小到大排序 if (arr[j] > arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; } } } }
若文件的初始狀態是正序的,一趟掃描便可完成排序。所需的關鍵字比較次數C和記錄移動次數M均達到最小值:
Cmin=n-1;Mmin=0
冒泡最好的時間複雜度爲O(n)。
若成績是文件是反序的,須要進行n-1趟排序。每趟排序要進行n-i此關鍵字的比較(1≤i≤n-1),且每次比較都必須移動記錄三次來達到交換記錄位置。在這種狀況下,比較和移動次數均達到最大值:
Cmax=n(n-1)/2=O(n^2);Mmax=3n(n-1)/2=O(n^2)
冒泡的最壞時間複雜度爲O(n^2)。
冒泡算法的平均時間複雜度爲O(n^2)。
冒泡排序是就地排序,且它是穩定的。
快速排序是C.R.A.Hoare於1962年提出的一種劃分交換排序。它採用了一種分治的策略,一般稱其爲分治法(Divide-and-ConquerMethod)。
將原有問題分解爲若干個規模更小但結構與原問題類似的子問題,遞歸地解這些子問題。而後將這些子問題的解組合爲原問題的解。而後將這些子問題的解組合爲原問題的解。
快速排序使用分治法來把一個串(list)分爲兩個子串(sub-lists)。具體算法描述以下:
// 快速排序 public static void fastSort(int[] arr, int left, int right) { // 最小位置 int minIndex = left; // 最大位置 int maxIndex = right; // 三方變量 int temp = 0; // 最大和最小位置不相等時 if (minIndex < maxIndex) { // 三方變量從最小位置開始取值比較 temp = arr[minIndex]; // 當最大和最小位置不重疊時 while (minIndex != maxIndex) { // 小位置沒有越過大位置,而且大位置的值大於等於temp的取值 while (maxIndex > minIndex && arr[maxIndex] >= temp) { // 大位置向左移 maxIndex--; } // 不然,小位置的元素取大位置的元素 arr[minIndex] = arr[maxIndex]; // 小位置沒有越過大位置,而且小位置的元素值小於等於三方變量 while (minIndex < maxIndex && arr[minIndex] <= temp) { // 小位置右移 minIndex++; } // 不然,大位置的元素取小位置的元素 arr[maxIndex] = arr[minIndex]; } // 中止處的小位置去三方變量的值。 arr[maxIndex] = temp; // 左半數組重複上面的操做 fastSort(arr, left, minIndex - 1); // 右半數組重複上面的操做 fastSort(arr, maxIndex + 1, right); } }
快速排序的時間主要耗費在劃分操做上,對長度爲k的區間進行劃分,共需k-1次關鍵字的比較。
最壞狀況是每次劃分宣州區的基準都是當前無序區中關鍵字最小(或最大)的記錄,劃分的結果是基準左邊的子區間爲空(或右邊的子區間爲空),而劃分所得的另外一個非空的子區間中記錄數目僅僅比劃分前的無序區中記錄個數減小一個。
所以,快速排序必須作n-1此劃分,第i此劃分開始區間長度爲n-i+1,所需的比較次數爲n-i(1≤i≤n-1),故總的比較次數達到最大值:
Cmax=n(n-1)/2=O(n^2)
最好狀況下,每次劃分所取的基準都是當前無序區的「中值」記錄,劃分的結果是基準的左、右兩個無序子區間的長度大體相等。總的關鍵字比較次數爲:O(nlgn)。 平均時間複雜度爲:O(nlgn)。
快速排序在系統內部須要一個棧來實現遞歸。若每次劃分較爲均勻,則其遞歸樹的高度爲O(lgn),故遞歸後須要棧空間爲O(lgn)。最壞狀況下,遞歸樹的高度爲O(n),所需的棧空間爲O(n)。
快速排序是非穩定的。
選擇排序的基本思想:
每一趟從待排序的記錄中選出關鍵字最小的記錄,順序放在已排好序的子文件的最後,直到所有記錄排序完畢。
選擇排序方法主要有一下兩種:直接選擇排序(或稱簡單選擇排序)和堆排序。
第i趟排序開始時,當前有序區和無序區分別爲R[1……i-1]和R[i……n](1≤i≤n-1),該趟排序則是從當前無序區中選出關鍵字最小的記錄R[k],將它與無序區的第一個記錄R[i]交換,是R[1……i]和R[i+1……n]分別變爲新的有序區和新的無序區。由於每趟排序均使有序區中增長了一個記錄,且有序區中的記錄關鍵字均不大於無序區無序區轟炸過記錄的關鍵字,即第i趟排序以後R[1……i].keys≤R[i+1……n].keys,因此進行n-1趟排序以後有R[1……n-1].keys≤R[n].key,即通過n-1趟排序以後,整個文件R[1……n]遞增有序。
n個記錄的直接選擇排序可通過n-1趟直接選擇排序獲得有序結果。具體算法描述以下:
//直接選擇排序 public static void SelectionSort(int[] arr) { int minIndex,temp; for(int i = 0;i<arr.length;i++) { minIndex=i; for(int j=i+1;j<arr.length;j++) { if(arr[j]<arr[minIndex]) { minIndex = j; } } temp = arr[i]; arr[i] =arr[minIndex]; arr[minIndex]=temp; } }
直接選擇排序的平均時間複雜度爲O(n^2)。
直接選擇排序是一個就地排序,而且是不穩定的。
堆排序是利用徹底二叉樹進行排序的方法。
堆有大根堆(根結點的關鍵字值最大的堆)和小根堆(根結點關鍵字值最小)之分。
堆排序利用了大根堆(或小根堆)堆頂記錄的關鍵字最大(或最小)這一特徵,使得在當前無序區中選取最大(或最小)關鍵字的記錄變得簡單。
大根堆排序思想:
首先將初始文件R[1……n]建成一個大根堆,此堆的初始的無序區;將關鍵字最大的記錄R[1](即堆頂)堆頂和無序區的最後一個記錄R[n]交換,由此獲得新的無序區R[1……n-1]和有序區R[n],且知足R[1……n-1].keys≤R[n].keys,因爲交換後新的根R[1]可能違反堆性質,故應將當前無序區R[1……n-1]調整爲堆;而後再次將R[1……n-1]中關鍵字最大的記錄R[1]和該區間的最後一個記錄R[n-1]交換,由此獲得新的無序區R[1……n-2]和有序區R[n-1……n],且仍知足關係R[1……n-2].keys≤R[n-1……n].keys。一樣要將R[1……n-2]調整爲堆。重複以上步驟,直至按關鍵字有序。
//交換值 private static void swap(int[] arr,int biggerIndex,int rootIndex) { int temp=arr[rootIndex]; arr[rootIndex]=arr[biggerIndex]; arr[biggerIndex]=temp; } //調整大根堆 private static void adjustHeap(int[] arr,int rootIndex,int lastIndex) { //從根結點開始往下調整 int biggerIndex = rootIndex; int leftChildIndex=rootIndex * 2+1; int rightChildIndex=rootIndex*2+2; if(rightChildIndex<=lastIndex) {//若是右結點存在,則左結點必定存在 if(arr[rightChildIndex]> arr[rootIndex]||arr[leftChildIndex]>arr[rootIndex]) { //將子節點更大的元素下標賦值給biggerIndex biggerIndex = arr[rightChildIndex]>arr[leftChildIndex]?rightChildIndex:leftChildIndex; } }else if(leftChildIndex<=lastIndex) {//保證左結點存在,且不越界 if(arr[leftChildIndex]>arr[rootIndex]) { biggerIndex = leftChildIndex; } } if(biggerIndex!=rootIndex) { swap(arr, biggerIndex, rootIndex); adjustHeap(arr, biggerIndex, lastIndex); } } //構建大根堆 private static void buildMaxHeap(int[] arr,int lastIndex) { //從最後一個元素的父結點開始進行調整,一直調整到根結點結束 int j=(lastIndex-1)/2; while(j>=0) { int rootIndex = j; adjustHeap(arr, rootIndex, lastIndex); j--; } } //堆排序 public static void heapSort(int[] arr,int len) { int lastIndex = len -1; //構建最大堆 buildMaxHeap(arr, lastIndex); while(lastIndex>0) { swap(arr, 0, lastIndex); if(--lastIndex==0) {//剩餘元素個數爲1,排序結束,跳出循環 break; } adjustHeap(arr, 0, lastIndex); } }
堆排序的時間主要由創建初始堆和反覆重建堆這兩部分的時間開銷構成,他們均是經過調用heapSort實現的,
堆排序的最壞時間複雜度爲O(nlgn)。堆排序的平均性能較接近於最壞性能。
因爲建初始堆歲序的比較次數較多,所欲堆排序不適宜與記錄數較少的文件。
堆排序是就地排序,輔助空間爲O(1)
堆排序是一種不穩定的排序方法。
歸併排序是將兩個或兩個以上的有序表組合成一個新的有序表。
先將N個數據當作N個長度爲1的表,將相鄰的表成對合並,獲得長度爲2的N/2個有序表,進一步將相鄰的合併,獲得長度爲4的N/4個有序表,一次類推,知道全部數據均合併成一個長度爲N的有序表爲止。
// 合併數據 private static void merge(int[] arr,int left,int center,int right) { int[] newArr = new int[arr.length]; int mid = center+1; int third = left; int tmp = left; while(left <=center&& mid<=right) { if(arr[left]<=arr[mid]) { newArr[third++]=arr[left++]; }else { newArr[third++]=arr[mid++]; } } while(mid<=right) { newArr[third++]=arr[mid++]; } while(left<=center) { newArr[third++]=arr[left++]; } while(tmp<=right) { arr[tmp]=newArr[tmp++]; } } //排序 private static void sort(int[] arr,int left,int right) { if(left>=right) { return; } int center = (left+right)/2; sort(arr, left, center); sort(arr, center+1, right); merge(arr, left, center, right); } //歸併排序 public static void mergeSort(int[] arr) { sort(arr, 0, arr.length-1); }
歸併排序的時間複雜度不管是在最好狀況下,仍是在最壞狀況下均是O(nlgn)。
須要一個輔助向量來暫存兩個有序子文件歸併的結果,故其輔助空間複雜度爲O(n)。
歸併排序是一種穩定的排序。
算法和數據結構的實現能夠基於住存儲器,也能夠基於輔助存儲器,但這會影響算法和數據結構的設計。
主存儲器和輔助存儲器的差異主要與存儲介質中的訪問速度、數據的存儲量和數據的永久性有關。
訪問輔助存儲器比訪問主存儲器要慢不少,而外部排序的過程須要進行屢次的主存儲器和輔助存儲器之間的交換。
調整內部排序算法使之應用於外部排序,這種思路的更廣泛的問題是這樣作不可能比設計一個新的儘可能減小磁盤存取的算法更有效。
進行外部排序的一個更好的方法源於歸併排序。
首先,按可用內存大小,將外存上喊n個記錄的文件分紅若干長度爲l的子文件或段(segment),依次讀入內存並利用有效的內部排序方法對它們進行排序,並將排序後獲得的有序子文件從新寫入外存。一般稱這些有序子文件爲歸併段或順串;而後對這些歸併段進行逐趟歸併,使歸併段組件由小到大,直至整個有序文件爲止。
置換選擇其實是堆排序算法的一個微小變種。
多路歸併與二路歸併相似。若是有B個順串須要歸併,從每一個順串中取出一個塊放在主存中使用,那麼B路歸併算法僅僅查看B個值,而且選擇最小的一個輸出。把這個值從它的順串中移出,而後重複這個過程。當任何順串的當前塊用完時,就從磁盤中獨處這個順串的下一個塊。
通常來講,簡歷大的初始順串能夠把運行時間減小到標準歸併排序的四分之一,使用多路歸併能夠進一步把時間減半。
本博文,部分參考十大經典排序算法(動圖演示),所涉及的動圖,均出自此博文!
上一篇:圖(graph)
下一篇: