1. 整體介紹
分類
排序大的分類可以分為兩種,內排序和外排序。在排序過程中,全部記錄存放在內存,則稱為內排序,如果排序過程中需要使用外存,則稱為外排序。主要需要理解的都是內排序算法:
內排序可以分為以下幾類:
(1)、插入排序:直接插入排序、二分法插入排序、希爾排序。
(2)、選擇排序:簡單選擇排序、堆排序。
(3)、交換排序:冒泡排序、快速排序。
(4)、歸並排序
(5)、基數排序
性能對比
穩定性:就是能保證排序前兩個相等的數據其在序列中的先后位置順序與排序后它們兩個先后位置順序相同。即如果A i == A j,A i 原來在 A j 位置前,排序后 A i 仍然是在 A j 位置前。
不穩定:簡單選擇排序、快速排序、希爾排序、堆排序不是穩定的排序算法
穩定:冒泡排序、直接插入排序、二分法插入排序,歸並排序和基數排序都是穩定的排序算法。
時間復雜度:一個算法執行所耗費的時間。
O(nlogn):快速排序,歸並排序,希爾排序,堆排序。
O(n^2):直接插入排序,簡單選擇排序,冒泡排序。
O(n): 桶、箱、基數排序
快速排序是目前基於比較的內部排序中最好的方法, 其次是歸並和希爾,堆排序在數據量很大時效果明顯。當數據是隨機分布時快速排序的平均時間最短。
空間復雜度:運行完一個程序所需內存的大小。
解釋:n: 數據規模;k:“桶”的個數;In-place: 占用常數內存,不占用額外內存;Out-place: 占用額外內存。
排序方法的選擇
1.數據規模很小(插入、簡單選擇、冒泡)
(1)數據基本有序的情況下,可選直接插入排序;
(2)數據無序時,對穩定性不作要求宜用簡單選擇排序,對穩定性有要求宜用插入或冒泡
2.數據規模一般(快速排序、歸並排序)
(1)完全可以用內存空間,序列雜亂無序,對穩定性沒有要求,快速排序,此時要付出log(N)的額外空間。
(2)序列本身可能有序,對穩定性有要求,空間允許下,宜用歸並排序
3.數據規模很大(歸並、桶)
(1)對穩定性有求,則可考慮歸並排序。
(2)對穩定性沒要求,宜用堆排序
4.待排序列初始基本有序(正序),宜用直接插入,冒泡
2. 插入排序(Insertion Sort)
基本思想:依次遍歷元素,在已排序的序列中找到合適的位置將當前遍歷的元素插入,直到所有元素都已排序。
2.1 直接插入排序
算法思想:
- <1>.從第一個元素開始,該元素可以認為已經被排序;
- <2>.取出下一個元素,在已經排序的元素序列中從后向前掃描;
- <3>.如果該元素(已排序)大於新元素,將該已排序元素移到下一位置;
- <4>.重復步驟3,直到找到已排序的元素小於或者等於新元素的位置;
- <5>.將新元素插入到該位置后;
- <6>.重復步驟2~5。
時間復雜度:平均情況下:O(n2)
最好的情況下:正序有序(從小到大),這樣只需要比較n次,不需要移動。因此時間復雜度為O(n)
最壞的情況下:逆序有序,這樣每一個元素就需要比較n次,共有n個元素,因此實際復雜度為O(n2)
穩定性:穩定。由算法思想易知,反向遍歷已排序元素,若已排序元素小於等於當前元素,則將當前元素插入該已排序元素后的位置,因此相對順序不變,插入排序是穩定的。
Java實現:
public int[] insertSort(int[] arr){ //從前向后遍歷待排序列 for (int i = 1; i < arr.length; i++) { //當前正在遍歷的元素值 int key = arr[i]; //從后向前掃描已排序序列,依次與當前元素對比 int j = i - 1; while(j >= 0 && arr[j] > key){ arr[j+1] = arr[j]; j--; } arr[j+1] = key; } return arr; }
2.2 二分排序(折半插入排序)
二分法查找基本思想:對於一個有序的待查序列,定義三個指針low、high、mid,分別指向待查序列所在范圍的下界、上界及區間中間位置,即mid=(low+high)/2。對比待查數據與mid所指的值,若相等則查找成功並返回mid,若待查數小於mid值,則令high=mid-1,否則令low=mid+1,得到新的需要查找的區間,如此循環直到找出或找不到。如下示例:
二分排序:從第二個數開始往后遍歷,用二分法查找合適的插入位置。當low<high條件不成立時說明找到合適位置,將low及之后元素后移
時間復雜度:二分插入排序的比較次數與待排序記錄的初始狀態無關,僅依賴於記錄的個數。當n較大時,比直接插入排序的最大比較次數少得多。但大於直接插入排序的最小比較次數。算法的移動次數與直接插入排序算法的相同,最壞的情況為n2/2,最好的情況為n,平均移動次數為O(n2)。
穩定性:穩定。
Java實現:
public int[] binaryInsertSort(int[] arr) { int low, high, mid; int key; for (int i = 1; i < arr.length; i++) { key = arr[i]; low = 0; high = i - 1; //執行二分查找,注意符號(<、<=...) while(low <= high){ mid = (low + high)/2; if (key < arr[mid]){ high = mid -1; }else { low = mid + 1; } } //二分查找終止,說明找到合適的位置low //將low位置及之后的所有元素右移一位,再將當前遍歷元素插入到low處 for (int j = i - 1; j >= low; j--) { arr[j+1] = arr[j]; } arr[low] = key; } return arr; }
2.3 希爾排序(Shell Sort)
基本思想:希爾排序也是一種插入排序方法,實際上是一種分組插入方法。先取定一個小於n的整數d1作為第一個增量,這樣可以把表的全部記錄分成d1個組:所有距離為d1的倍數的記錄放在同一個組中,在各組內進行直接插入排序;然后,取第二個增量d2(<d1),重復上述的分組和排序,直至所取的增量dt=1(dt<dt-1<…<d2<d1),即所有記錄放在同一組中進行直接插入排序為止。
希爾排序的核心在於間隔序列的設定。既可以提前設定好間隔序列,也可以動態的定義間隔序列。
時間復雜度: 平均情況下:O(N*logN)
最好情況:由於希爾排序的好壞和步長d的選擇(di到di+1的選擇策略)有很多關系,因此,目前還沒有得出最好的步長如何選擇(現在有些比較好的選擇了,但不確定是否是最好的)。所以,不知道最好的情況下的算法時間復雜度。
最壞情況:O(N*logN),最壞的情況下和平均情況下差不多。
穩定性:由於多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同趟的插入排序過程中,相同的元素可能在各自的插入排序中移動,最后其穩定性就會被打亂,所以shell排序是不穩定的。(有個猜測,方便記憶:一般來說,若存在不相鄰元素間交換,則很可能是不穩定的排序。)
Java實現:
public int[] shellSort(int[] arr) { //分組 int d = arr.length/2; //初始增量 while(d > 0){ for (int i = d; i < arr.length; i++) { //i是當前等待插入的元素的本來位置 int key = arr[i]; int j = i - d; //j用來循環,i位置之前待比較的元素序列 while(j >= 0 && arr[j] > arr[i]){ //待插入元素小了 arr[j+d] = arr[j]; //比待插入元素大的都往后移,類似於直接插入排序,但是增量由1改為d j = j - d; //比較已排序序列的前一位置元素 } arr[j+d] = key; //跳出循環說明找到當前a[j]<待排元素,將待排元素插入到j后的一個位置 } d = d / 2; //步長算法,可以優化 } return arr; }
解釋:就是將增量(d)為1的直接插入排序,增量改為從d到1的遞減。不再是相鄰元素間的對比,而是以d為間隔的對比插入。
3. 交換排序
3.1 冒泡排序(Bubble Sort)
基本思想:通過無序區中相鄰記錄關鍵字間的比較和位置的交換,使關鍵字最小的記錄如氣泡一般逐漸往上“漂浮”直至“水面”。
時間復雜度:平均O(n2)
最好情況:正序有序,則只需要比較n次。故,為O(n)
最壞情況: 逆序有序,則需要比較(n-1)+(n-2)+……+1,故,為O(N2)
穩定性:穩定。排序過程中只交換相鄰兩個元素的位置。因此,當兩個數相等時,是沒必要交換兩個數的位置的。所以相對位置並沒有改變,冒泡排序算法是穩定的!
算法描述:
- <1>.比較相鄰的元素。如果第一個比第二個大,就交換它們兩個;
- <2>.對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最后一對,這樣在最后的元素應該會是最大的數;
- <3>.針對所有的元素重復以上的步驟,除了最后一個;
- <4>.重復步驟1~3,直到排序完成。
Java實現:
public int[] bubbleSort(int[] arr) { for (int i = arr.length - 1; i >= 0; i--) { for (int j = 0; j < i; j++) { if (arr[j]>arr[j+1]) { swap(arr[j], arr[j+1]); } } } return arr; }
/** * 交換兩個數 */ public void swap(int a, int b) { int temp = a; a = b; b = temp; }
冒泡排序改進1:設置一標志性變量pos,用於記錄每趟排序中最后一次進行交換的位置。由於pos位置之后的記錄均已交換到位,故在進行下一趟排序時只要掃描到pos位置即可。因為上一輪冒泡的最后一次交換說明該交換的元素是其位置以前的所有元素中最大的(畢竟是自己冒上來的)。下一趟找最大元素的冒泡從該位置開始即可,不必從頭開始兩兩交換
Java實現:
public int[] bubbleSort2(int[] arr){ for (int i = arr.length - 1; i >= 0; i--) { int pos = 0; //每趟開始,無交換記錄 for (int j = 0; j < i; j++) { if (arr[j]>arr[j+1]) { swap(arr[j], arr[j+1]); pos = j; //記錄交換位置 } i = pos; //從上輪交換最后一次交換的位置開始兩兩對比 } } return arr; }
3.2 快速排序(Quick Sort)
基本思想:由冒泡排序改進而來的。在待排序的n個記錄中任取一個記錄(通常取第一個記錄),把該記錄放入適當位置后,數據序列被此記錄划分成兩部分。所有關鍵字比該記錄關鍵字小的記錄放置在前一部分,所有比它大的記錄放置在后一部分,並把該記錄排在這兩部分的中間(稱為該記錄歸位)。
核心思想:通過一趟排序將待排記錄分隔成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序。
時間復雜度:O(N*logN)
最好的情況:因為每次都將序列分為兩個部分(一般二分都復雜度都和logN相關),故為 O(N*logN)
最壞的情況:基本有序時,退化為冒泡排序,幾乎要比較N*N次,故為O(N*N)
穩定性:不穩定。由於每次都需要和中軸元素(不一定相鄰)交換,因此原來的順序就可能被打亂。快速排序是不穩定的。
算法描述:
快速排序使用分治法來把一個串(list)分為兩個子串(sub-lists)。具體算法描述如下:
- <1>.從數列中挑出一個元素,稱為 "基准"(pivot);
- <2>.重新排序數列,所有元素比基准值小的擺放在基准前面,所有元素比基准值大的擺在基准的后面(相同的數可以到任一邊)。在這個分區退出之后,該基准就處於數列的中間位置。這個稱為分區(partition)操作;
- <3>.遞歸地(recursive)把小於基准值元素的子數列和大於基准值元素的子數列排序。
Java實現:
public int[] quickSort(int[] arr,int low,int high){ int i = low; int j = high; // if ((arr == null) || (arr.length == 0)){ // return null; // } while (i < j) { //查找基准點下標 while (i < j && arr[i] <= arr[j]) // 以數組start下標的數據為key,右側掃描 j--; swap(arr[i], arr[j]); // 從右往左掃描,找出第一個比key小的,交換位置 while (i < j && arr[i] < arr[j]) // 從左往右掃描(此時a[j]中存儲着key值) i++; swap(arr[i], arr[j]); // 找出第一個比key大的,交換位置 } if (i - low > 1) { // 遞歸調用,把key前面的完成排序 quickSort(arr, 0, i - 1); } if (high - j > 1) { quickSort(arr, j + 1, high); // 遞歸調用,把key后面的完成排序 } return arr; }
4. 選擇排序
4.1 簡單選擇排序(Selection Sort)
基本思想:首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再從剩余未排序元素中繼續尋找最小元素,然后放到排序序列末尾。以此類推,直到所有元素均排序完畢。具體做法是:選擇最小的元素與未排序部分的首部交換,使得序列的前面為有序。
時間復雜度:平均情況下:O(N2)
最好情況:交換0次,但是每次都要找到最小的元素,因此大約必須遍歷N*N次,因此為O(N*N)。減少了交換次數!
最壞情況:平均情況下:O(N*N)
穩定性:不穩定。 由於每次都是選取未排序序列A中的最小元素x與A中的第一個元素交換,因此跨距離了,很可能破壞了元素間的相對位置,因此選擇排序是不穩定的!
算法描述:n個記錄的直接選擇排序可經過n-1趟直接選擇排序得到有序結果。
- <1>.初始狀態:無序區為R[1..n],有序區為空;
- <2>.第i趟排序(i=1,2,3...n-1)開始時,當前有序區和無序區分別為R[1..i-1]和R(i..n)。該趟排序從當前無序區中-選出關鍵字最小的記錄 R[k],將它與無序區的第1個記錄R交換,使R[1..i]和R[i+1..n)分別變為記錄個數增加1個的新有序區和記錄個數減少1個的新無序區;
- <3>.n-1趟結束,數組有序化了。
Java實現:
public int[] selectionSort(int[] arr) { int minIndex; int temp; //一趟找出一個最小值 for (int i = 0; i < arr.length - 1; i++) { minIndex = i; for (int j = i + 1; j < arr.length ; j++) { if (arr[j] < arr[minIndex]) { minIndex = j; } } //將找到的最小值放到已排序序列的末尾 temp = arr[minIndex]; arr[minIndex] = arr[i]; arr[i] = temp; } return arr; }
4.2 堆排序(Heap Sort)
基本思想:堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。用完全二叉樹中雙親節點和孩子節點之間的內在關系,在當前無序區中選擇關鍵字最大(或者最小)的記錄。也就是說,以最小堆為例,根節點為最小元素,較大的節點偏向於分布在堆底附近。
時間復雜度:O(nlogn)。最壞情況下,接近於最差情況下:O(N*logN),因此它是一種效果不錯的排序算法。
穩定性:不穩定。需要不斷地調整堆。
算法描述:
- <1>.將初始待排序關鍵字序列(R1,R2....Rn)構建成大頂堆,此堆為初始的無序區;
- <2>.將堆頂元素R[1]與最后一個元素R[n]交換,此時得到新的無序區(R1,R2,......Rn-1)和新的有序區(Rn),且滿足R[1,2...n-1]<=R[n];
- <3>.由於交換后新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1,R2,......Rn-1)調整為新堆,然后再次將R[1]與無序區最后一個元素交換,得到新的無序區(R1,R2....Rn-2)和新的有序區(Rn-1,Rn)。不斷重復此過程直到有序區的元素個數為n-1,則整個排序過程完成。
Java實現:
5. 歸並排序
基本思想:多次將兩個或兩個以上的有序表合並成一個新的有序表。
時間復雜度:O(nlogn)
最好情況:一趟歸並需要n次,總共需要logN次,因此為O(N*logN)
最壞情況:接近於平均情況下,為O(N*logN)
穩定性:歸並排序最大的特色就是它是一種穩定的排序算法。歸並過程中是不會改變元素的相對位置的。
缺點:它需要O(n)的額外空間。但是很適合於多鏈表排序。
6. 基數排序
基本思想:它是一種非比較排序。它是根據位的高低進行排序的,也就是先按個位排序,然后依據十位排序……以此類推。示例如下:
時間復雜度:分配需要O(n),收集為O(r),其中r為分配后鏈表的個數,以r=10為例,則有0~9這樣10個鏈表來將原來的序列分類。而d,也就是位數(如最大的數是1234,位數是4,則d=4),即"分配-收集"的趟數。因此時間復雜度為O(d*(n+r))
穩定性:穩定。
適用情況:如果有一個序列,知道數的范圍(比如1~1000),用快速排序或者堆排序,需要O(N*logN),但是如果采用基數排序,則可以達到O(4*(n+10))=O(n)的時間復雜度。算是這種情況下排序最快的!!
基數排序 vs 計數排序 vs 桶排序
這三種排序算法都利用了桶的概念,但對桶的使用方法上有明顯差異:
- 基數排序:根據鍵值的每位數字來分配桶
- 計數排序:每個桶只存儲單一鍵值
- 桶排序:每個桶存儲一定范圍的數值
參考鏈接:
http://www.cnblogs.com/jztan/p/5878630.html
http://blog.chinaunix.net/uid-25906157-id-3318529.html
http://www.cnblogs.com/leeplogs/p/5863846.html