排序分類:
- 外排序:需要在內外存之間多次交換數據
- 內排序:
- 插入類排序
- 直接插入排序
- 希爾排序
- 選擇類排序
- 簡單選擇排序
- 堆排序
- 交換類排序
- 冒泡排序
- 快速排序
- 歸並排序
- 歸並排序
- 插入類排序
| 排序方法 | 平均情況 | 最好情況 | 最壞情況 | 輔助空間 | 穩定性 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | 穩定 |
| 簡單選擇排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 穩定 |
| 直接插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | 穩定 |
| 希爾排序 | O(nlogn)~O(n^2) | O(n^1.3) | O(n^2) | O(1) | 不穩定 |
| 堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不穩定 |
| 歸並排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 穩定 |
| 快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(logn)~O(n) | 不穩定 |
冒泡排序(O(n^2))
-
比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
-
對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最后一對。在這一點,最后的元素應該會是最大的數。
-
針對所有的元素重復以上的步驟,除了最后一個。
-
持續每次對越來越少的元素重復上面的步驟,直到沒有任何一對數字需要比較。
算法分析:
時間復雜度
若文件的初始狀態是正序的,一趟掃描即可完成排序。所需的關鍵字比較次數
和記錄移動次數
均達到最小值:
,
。
若初始文件是反序的,需要進行
冒泡排序算法穩定性
//冒泡排序 public static void bubbleSort(int[] arr,int len){ int temp=0; int compareRange=len-1;//冒泡排序中,參與比較的數字的邊界。 //冒泡排序主要是比較相鄰兩個數字的大小,以升序排列為例,如果前側數字大於后側數字,就進行交換,一直到比較邊界。 for (int i = 0; i <len ; i++) {//n個數使用冒泡排序,最多需要n趟完成排序。最外層循環用於控制排序趟數 for (int j = 1; j <=compareRange ; j++) { if(arr[j-1]>arr[j]){ temp=arr[j-1]; arr[j-1]=arr[j]; arr[j]=temp; } } compareRange--;//每進行一趟排序,序列中最大數字就沉到底部,比較邊界就向前移動一個位置。 } System.out.println("排序后數組"+Arrays.toString(arr)); }
在排序后期可能數組已經有序了而算法卻還在一趟趟的比較數組元素大小,可以引入一個標記,如果在一趟排序中,數組元素沒有發生過交換說明數組已經有序,跳出循環即可。優化后的代碼如下:
public static void bubbleSort2(int[] arr,int len){ int temp=0; int compareRange=len-1;//冒泡排序中,參與比較的數字的邊界。 boolean flag=true;//標記排序時候已經提前完成 int compareCounter=0; //冒泡排序主要是比較相鄰兩個數字的大小,以升序排列為例,如果前側數字大於后側數字,就進行交換,一直到比較邊界。 while(flag) { flag=false; for (int j = 1; j <=compareRange ; j++) { if(arr[j-1]>arr[j]){ temp=arr[j-1]; arr[j-1]=arr[j]; arr[j]=temp; flag=true; } } compareCounter++; compareRange--;//每進行一趟排序,序列中最大數字就沉到底部,比較邊界就向前移動一個位置。 } System.out.println("優化后排序次數:"+(compareCounter-1)); System.out.println("排序后數組"+Arrays.toString(arr)); }
還可以利用這種標記的方法還可以檢測數組是否有序,遍歷一個數組比較其大小,對於滿足要求的元素進行交換,如果不會發生交換則數組就是有序的,否則是無序的。
兩種方法的排序結果如下所示:

直接插入排序(O(n^2))
將待排序的數組划分為局部有序子數組subSorted和無序子數組subUnSorted,每次排序時從subUnSorted中挑出第一個元素,從后向前將其與subSorted各元素比較大小,按照大小插入合適的位置,插入完成后將此元素從subUnSorted中移除,重復這個過程直至subUnSorted中沒有元素,總之就時從后向前,一邊比較一邊移動。

對應代碼如下:
public static void straightInsertSort(int[] arr,int len){ int temp=0; int j=0; for (int i = 1; i <len ; i++) { if(arr[i]<arr[i-1]){ temp=arr[i]; for (j = i-1; j>=0&&temp<arr[j] ; j--) { arr[j+1]=arr[j];//從后向前移動數組 } arr[j+1]=temp; } } System.out.println("直接插入排序后數組" + Arrays.toString(arr)); }
基於鏈表的直接插入排序
class Solution { public: ListNode *insertionSortList(ListNode *head) { ListNode* newHead=nullptr; ListNode* toInsert=head; while(toInsert!=nullptr){ ListNode* current=newHead; ListNode* last=nullptr; ListNode* next=toInsert->next; while(current!=nullptr&¤t->val<=toInsert->val){ last=current; current=current->next; } //比任何已排序的數字都要小,則插入頭部 if(last==nullptr){ toInsert->next=newHead; newHead=toInsert; } //鏈表中部或尾部插入方法一致 else{ toInsert->next=last->next; last->next=toInsert; } toInsert=next; } return newHead; } };
快速排序
快速排序采用了一種叫分治的思想。
分治法的基本思想是:將原問題分解為若干個規模更小但結構與原問題相似的子問題。遞歸地解這些子問題,然后將這些子問題的解組合為原問題的解。
利用分治法可將快速排序的分為三步:
- 在數據集之中,選擇一個元素作為”基准”(pivot)。
- 所有小於”基准”的元素,都移到”基准”的左邊;所有大於”基准”的元素,都移到”基准”的右邊。這個操作稱為分區 (partition) 操作,分區操作結束后,基准元素所處的位置就是最終排序后它的位置。
- 對”基准”左邊和右邊的兩個子集,不斷重復第一步和第二步,直到所有子集只剩下一個元素為止。
代碼實現:
int quicksort(vector<int> &v, int left, int right){ if(left < right){ int key = v[left]; int low = left; int high = right; while(low < high){ while(low < high && v[high] > key){ high--; } v[low] = v[high]; while(low < high && v[low] < key){ low++; } v[high] = v[low]; } v[low] = key; quicksort(v,left,low-1); quicksort(v,low+1,right); } }
ListNode* GetPartion(ListNode* pBegin, ListNode* pEnd) { int key = pBegin->key; ListNode* p = pBegin; ListNode* q = p->next; while(q != pEnd) { if(q->key < key) { p = p->next; swap(p->key,q->key); } q = q->next; } swap(p->key,pBegin->key); return p; } void QuickSort(ListNode* pBeign, ListNode* pEnd) { if(pBeign != pEnd) { ListNode* partion = GetPartion(pBeign,pEnd); QuickSort(pBeign,partion); QuickSort(partion->next,pEnd); } }
歸並排序
概述
歸並的含義就是將兩個或多個有序序列合並成一個有序序列的過程,歸並排序就是將若干有序序列逐步歸並,最終形成一個有序序列的過程。以最常見的二路歸並為例,就是將兩個有序序列歸並。歸並排序由兩個過程完成:有序表的合並和排序的遞歸實現。

有序表的合並
再來看看治階段,我們需要將兩個已經有序的子序列合並成一個有序序列,比如上圖中的最后一次合並,要將[4,5,7,8]和[1,2,3,6]兩個已經有序的子序列,合並為最終序列[1,2,3,4,5,6,7,8],來看下實現步驟。


代碼實現:
import java.util.Arrays; /** * Created by chengxiao on 2016/12/8. */ public class MergeSort { public static void main(String []args){ int []arr = {9,8,7,6,5,4,3,2,1}; sort(arr); System.out.println(Arrays.toString(arr)); } public static void sort(int []arr){ int []temp = new int[arr.length];//在排序前,先建好一個長度等於原數組長度的臨時數組,避免遞歸中頻繁開辟空間 sort(arr,0,arr.length-1,temp); } private static void sort(int[] arr,int left,int right,int []temp){ if(left<right){ int mid = (left+right)/2; sort(arr,left,mid,temp);//左邊歸並排序,使得左子序列有序 sort(arr,mid+1,right,temp);//右邊歸並排序,使得右子序列有序 merge(arr,left,mid,right,temp);//將兩個有序子數組合並操作 } } private static void merge(int[] arr,int left,int mid,int right,int[] temp){ int i = left;//左序列指針 int j = mid+1;//右序列指針 int t = 0;//臨時數組指針 while (i<=mid && j<=right){ if(arr[i]<=arr[j]){ temp[t++] = arr[i++]; }else { temp[t++] = arr[j++]; } } while(i<=mid){//將左邊剩余元素填充進temp中 temp[t++] = arr[i++]; } while(j<=right){//將右序列剩余元素填充進temp中 temp[t++] = arr[j++]; } t = 0; //將temp中的元素全部拷貝到原數組中 while(left <= right){ arr[left++] = temp[t++]; } } }
單向鏈表的歸並排序:
ListNode *sortList(ListNode *head) { if(head==nullptr||head->next==nullptr) return head; //采用快慢指針找到中間節點 ListNode *fast=head,*slow=head; while(fast!=nullptr&&fast->next!=nullptr&&fast->next->next!=nullptr){ fast=fast->next->next; slow=slow->next; } //斷開 fast=slow; slow=slow->next; fast->next=nullptr; fast=sortList(head); slow=sortList(slow); return merge(fast,slow); } ListNode* merge(ListNode* sub1,ListNode* sub2){ if(sub1==nullptr)return sub2; if(sub2==nullptr)return sub1; ListNode* head=nullptr; if(sub1->val<sub2->val){ head=sub1; sub1=sub1->next; } else{ head=sub2; sub2=sub2->next; } ListNode* p=head; while(sub1!=nullptr&&sub2!=nullptr){ if(sub1->val<sub2->val){ p->next=sub1; sub1=sub1->next; } else{ p->next=sub2; sub2=sub2->next; } p=p->next; } if(sub1!=nullptr) p->next=sub1; if(sub2!=nullptr) p->next=sub2; return head; }
希爾排序
希爾排序是希爾(Donald Shell)於1959年提出的一種排序算法。希爾排序也是一種插入排序,它是簡單插入排序經過改進之后的一個更高效的版本,也稱為縮小增量排序,同時該算法是沖破O(n2)的第一批算法之一。本文會以圖解的方式詳細介紹希爾排序的基本思想及其代碼實現。
基本思想
希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止。
簡單插入排序很循規蹈矩,不管數組分布是怎么樣的,依然一步一步的對元素進行比較,移動,插入,比如[5,4,3,2,1,0]這種倒序序列,數組末端的0要回到首位置很是費勁,比較和移動元素均需n-1次。而希爾排序在數組中采用跳躍式分組的策略,通過某個增量將數組元素划分為若干組,然后分組進行插入排序,隨后逐步縮小增量,繼續按組進行插入排序操作,直至增量為1。希爾排序通過這種策略使得整個數組在初始階段達到從宏觀上看基本有序,小的基本在前,大的基本在后。然后縮小增量,到增量為1時,其實多數情況下只需微調即可,不會涉及過多的數據移動。
我們來看下希爾排序的基本步驟,在此我們選擇增量gap=length/2,縮小增量繼續以gap = gap/2的方式,這種增量選擇我們可以用一個序列來表示,{n/2,(n/2)/2...1},稱為增量序列。希爾排序的增量序列的選擇與證明是個數學難題,我們選擇的這個增量序列是比較常用的,也是希爾建議的增量,稱為希爾增量,但其實這個增量序列不是最優的。此處我們做示例使用希爾增量。

代碼實現
在希爾排序的理解時,我們傾向於對於每一個分組,逐組進行處理,但在代碼實現中,我們可以不用這么按部就班地處理完一組再調轉回來處理下一組(這樣還得加個for循環去處理分組)比如[5,4,3,2,1,0] ,首次增量設gap=length/2=3,則為3組[5,2] [4,1] [3,0],實現時不用循環按組處理,我們可以從第gap個元素開始,逐個跨組處理。同時,在插入數據時,可以采用元素交換法尋找最終位置,也可以采用數組元素移動法尋覓。希爾排序的代碼比較簡單,如下:
import java.util.Arrays; /** * Created by chengxiao on 2016/11/24. */ public class ShellSort { public static void main(String []args){ int []arr ={1,4,2,7,9,8,3,6}; sort(arr); System.out.println(Arrays.toString(arr)); int []arr1 ={1,4,2,7,9,8,3,6}; sort1(arr1); System.out.println(Arrays.toString(arr1)); } /** * 希爾排序 針對有序序列在插入時采用交換法 * @param arr */ public static void sort(int []arr){ //增量gap,並逐步縮小增量 for(int gap=arr.length/2;gap>0;gap/=2){ //從第gap個元素,逐個對其所在組進行直接插入排序操作 for(int i=gap;i<arr.length;i++){ int j = i; while(j-gap>=0 && arr[j]<arr[j-gap]){ //插入排序采用交換法 swap(arr,j,j-gap); j-=gap; } } } } /** * 希爾排序 針對有序序列在插入時采用移動法。 * @param arr */ public static void sort1(int []arr){ //增量gap,並逐步縮小增量 for(int gap=arr.length/2;gap>0;gap/=2){ //從第gap個元素,逐個對其所在組進行直接插入排序操作 for(int i=gap;i<arr.length;i++){ int j = i; int temp = arr[j]; if(arr[j]<arr[j-gap]){ while(j-gap>=0 && temp<arr[j-gap]){ //移動法 arr[j] = arr[j-gap]; j-=gap; } arr[j] = temp; } } } } /** * 交換數組元素 * @param arr * @param a * @param b */ public static void swap(int []arr,int a,int b){ arr[a] = arr[a]+arr[b]; arr[b] = arr[a]-arr[b]; arr[a] = arr[a]-arr[b]; } }
堆排序
預備知識
堆排序
堆排序是利用堆這種數據結構而設計的一種排序算法,堆排序是一種選擇排序,它的最壞,最好,平均時間復雜度均為O(nlogn),它也是不穩定排序。首先簡單了解下堆結構。
堆
堆是具有以下性質的完全二叉樹:每個結點的值都大於或等於其左右孩子結點的值,稱為大頂堆;或者每個結點的值都小於或等於其左右孩子結點的值,稱為小頂堆。如下圖:

同時,我們對堆中的結點按層進行編號,將這種邏輯結構映射到數組中就是下面這個樣子

該數組從邏輯上講就是一個堆結構,我們用簡單的公式來描述一下堆的定義就是:
大頂堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小頂堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
ok,了解了這些定義。接下來,我們來看看堆排序的基本思想及基本步驟:
堆排序基本思想及步驟
堆排序的基本思想是:將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就為最大值。然后將剩余n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值。如此反復執行,便能得到一個有序序列了
步驟一 構造初始堆。將給定無序序列構造成一個大頂堆(一般升序采用大頂堆,降序采用小頂堆)。
a.假設給定無序序列結構如下

2.此時我們從最后一個非葉子結點開始(葉結點自然不用調整,第一個非葉子結點 arr.length/2-1=5/2-1=1,也就是下面的6結點),從左至右,從下至上進行調整。

4.找到第二個非葉節點4,由於[4,9,8]中9元素最大,4和9交換。

這時,交換導致了子根[4,5,6]結構混亂,繼續調整,[4,5,6]中6最大,交換4和6。

此時,我們就將一個無需序列構造成了一個大頂堆。
步驟二 將堆頂元素與末尾元素進行交換,使末尾元素最大。然后繼續調整堆,再將堆頂元素與末尾元素交換,得到第二大元素。如此反復進行交換、重建、交換。
a.將堆頂元素9和末尾元素4進行交換

b.重新調整結構,使其繼續滿足堆定義

c.再將堆頂元素8與末尾元素5進行交換,得到第二大元素8.

后續過程,繼續進行調整,交換,如此反復進行,最終使得整個序列有序

再簡單總結下堆排序的基本思路:
a.將無需序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆;
b.將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;
c.重新調整結構,使其滿足堆定義,然后繼續交換堆頂元素與當前末尾元素,反復執行調整+交換步驟,直到整個序列有序。
簡單選擇排序
