說明
術語
- 穩定 :如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
- 不穩定 :如果a原本在b的前面,而a=b,排序之后a可能會出現在b的后面;
- 內排序 :所有排序操作都在內存中完成;
- 外排序 :由於數據太大,因此把數據放在磁盤中,而排序通過磁盤和內存的數據傳輸才能進行;
- 時間復雜度 : 一個算法執行所耗費的時間。
- 空間復雜度 :運行完一個程序所需內存的大小。
算法總結
上圖片名詞解釋:
n: 數據規模
k: “桶”的個數
In-place: 占用常數內存,不占用額外內存
Out-place: 占用額外內存
算法分類
比較和非比較
比較排序:快速排序、歸並排序、堆排序、冒泡排序
在排序的最終結果里,元素之間的次序依賴於它們之間的比較。每個數都必須和其他數進行比較,才能確定自己的位置 。
在冒泡排序之類的排序中,問題規模為n,又因為需要比較n次,所以平均時間復雜度為O(n²)。在歸並排序、快速排序之類的排序中,問題規模通過分治法消減為logN次,所以時間復雜度平均O(nlogn)。
比較排序的優勢是,適用於各種規模的數據,也不在乎數據的分布,都能進行排序。可以說,比較排序適用於一切需要排序的情況。
非比較排序:計數排序、基數排序、桶排序
非比較排序是通過確定每個元素之前,應該有多少個元素來排序。針對數組arr,計算arr[i]之前有多少個元素,則唯一確定了arr[i]在排序后數組中的位置 。
非比較排序只要確定每個元素之前的已有的元素個數即可,所有一次遍歷即可解決。算法時間復雜度O(n)。
非比較排序時間復雜度底,但由於非比較排序需要占用空間來確定唯一位置。所以對數據規模和數據分布有一定的要求。
十大排序算法
冒泡排序(Bubble Sort)
冒泡排序算法是把較小的元素往前調或者把較大的元素往后調。這種方法主要是通過對相鄰兩個元素進行大小的比較,根據比較結果和算法規則對該二元素的位置進行交換,這樣逐個依次進行比較和交換,就能達到排序目的。
算法步驟
1 首先將第1個和第2個記錄的關鍵字比較大小,如果是逆序的,就將這兩個記錄進行交換;
2 再對第2個和第3個記錄的關鍵字進行比較,依次類推,重復進行上述計算,直至完成第(n一1)個和第n個記錄的關鍵字之間的比較。這步做完后,最后的元素會是最大的數;
3 再按照上述過程進行第2次、第3次排序,持續每次對越來越少的元素重復上面的步驟,直到沒有任何一對數字需要比較。
說明:當相鄰兩個元素大小一致時,這一步操作就不需要交換位置,因此也說明冒泡排序是一種嚴格的穩定排序算法,它不改變序列中相同元素之間的相對位置關系。。
JAVA代碼實現
public static void main(String[] args) {
int[] a={12,14,2,155,2,5,17,56,555,534534,887};
bubbleSort(a);
// selectionSort(a);
// insertionSort(a);
// quickSort(a,0,a.length-1);
// shellSort(a);
// heapSort(a);
// mergeSort(a,new int[a.length],0,a.length-1);
// int[] a={98,93,98,90,97,91,98,93,92,92,90,99,95,97,98,93,96,93};
// countingSort(a);
// bucketSort(a);
// radixSort(a);
System.out.println(Arrays.toString(a));
}
//冒泡排序算法實現
public static void bubbleSort(int[] a){
for (int i = 0; i <a.length ; i++) {
for (int j = 1; j <a.length-i ; j++) {
// 若前面的數字大於后邊的數字則交換位置
if (a[j-1]>a[j]){
int temp=a[j-1];
a[j-1]=a[j];
a[j]=temp;
}
}
}
}
選擇排序(Selection Sort)
選擇排序是一種簡單直觀的排序算法,無論什么數據進去都是 O(n²) 的時間復雜度。所以用到它的時候,數據規模越小越好。唯一的好處可能就是不占用額外的內存空間了吧。
選擇排序算法的基本思路是為每一個位置選擇當前最小的元素。
算法步驟
1 首先從第1個位置開始對全部元素進行選擇,選出全部元素中最小的給該位置;
2 再對第2個位置進行選擇,在剩余元素中選擇最小的給該位置即可;
3 以此類推,重復進行“最小元素”的選擇,直至完成第(n-1)個位置的元素選擇,則第n個位置就只剩唯一的最大元素,此時不需再進行選擇;
4 每次遍歷的時候,將前面找出的最小值,看成一個有序的列表,后面的看成無序的列表,然后每次遍歷無序列表找出最小值。
說明:舉例,序列5 8 5 3 9.我們知道第一遍選擇第1個元素“5”會和元素“3”交換,那么原序列中的兩個相同元素“5”之間的前后相對順序就發生了改變。因此,我們說選擇排序不是穩定的排序算法,它在計算過程中會破壞穩定性。
JAVA代碼實現
// 選擇排序算法實現
public static void selectionSort(int[] a){
for (int i = 0; i <a.length ; i++) {
//首先將第i個值定義為最小的值
int k=i;
int min=a[i];
//找到最小值的標
for (int j = i+1; j <a.length ; j++) {
if(min>a[j]){
min=a[j];
k=j;
}
}
//將i與k值互換
if(i!=k)
{
int t=a[i];
a[i]=a[k];
a[k]=t;
}
}
}
插入排序(Insertion Sort)
插入排序算法是基於某序列已經有序排列的情況下,通過一次插入一個元素的方式按照原有排序方式增加元素。這種比較是從該有序序列的最末端開始執行,即要插入序列中的元素最先和有序序列中最大的元素比較,若其大於該最大元素,則可直接插入最大元素的后面即可,否則再向前一位比較查找直至找到應該插入的位置為止。
插入排序的基本思想是,每次將1個待排序的記錄按其關鍵字大小插入到前面已經排好序的子序列中,尋找最適當的位置,直至全部記錄插入完畢。
算法步驟
1 從第一個元素開始,該元素可以認為已經被排序;
2 取出下一個元素,在已經排序的元素序列中從后向前掃描;
3 如果該元素(已排序)大於新元素,將該元素移到下一位置;
4 重復步驟3,直到找到已排序的元素小於或者等於新元素的位置;
5 將新元素插入到該位置后;
6 重復步驟2~5。
說明:執行過程中,若遇到和插入元素相等的位置,則將要插人的元素放在該相等元素的后面,因此插入該元素后並未改變原序列的前后順序。我們認為插入排序也是一種穩定的排序方法。
JAVA代碼實現
// 插入排序的算法實現
public static void insertionSort(int[] a){
//從第二個數據開始判斷,比較當前的數據與之前所有數據判斷適合的插入位置
for (int i = 1; i <a.length ; i++) {
for (int j = i; j >0 ; j--) {
if(a[j]<a[j-1]){
//交換
int t=a[j];
a[j]=a[j-1];
a[j-1]=t;
}else{
break;//插入完畢
}
}
}
}
希爾排序(Shell Sort)
希爾排序,也稱遞減增量排序算法,是插入排序的一種更高效的改進版本。同時該算法是沖破O(n2)的第一批算法之一,但希爾排序是非穩定排序算法。
希爾排序是基於插入排序的以下兩點性質而提出改進方法的:
- 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率;
- 但插入排序一般來說是低效的,因為插入排序每次只能將數據移動一位;
希爾排序是把記錄按下表的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止。
算法步驟
我們來看下希爾排序的基本步驟,在此我們選擇增量gap=length/2,縮小增量繼續以gap = gap/2的方式,這種增量選擇我們可以用一個序列來表示,{n/2,(n/2)/2…1},稱為增量序列。希爾排序的增量序列的選擇與證明是個數學難題,我們選擇的這個增量序列是比較常用的,也是希爾建議的增量,稱為希爾增量,但其實這個增量序列不是最優的。此處我們做示例使用希爾增量。
先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,具體算法描述:
1 選擇一個增量序列t1,t2,…,tk,其中ti>tj,tk=1;
2 按增量序列個數k,對序列進行k 趟排序;
3 每趟排序,根據對應的增量ti,將待排序列分割成若干長度為m 的子序列,分別對各子表進行直接插入排序。僅增量因子為1 時,整個序列作為一個表來處理,表長度即為整個序列的長度。
說明:基本原理和插入排序類似,不一樣的地方在於。通過間隔多個數據來進行插入排序。希爾排序是非穩定排序算法
JAVA代碼實現
//希爾排序的算法實現
public static void shellSort(int[] a){
int len=a.length; //數組長度
//每次分組的步長為上一次1/2,step為每組數的跨度
for (int step = len/2; step >0 ; step/=2) {
//每組數據為i,i+step,i+2step...,對數據進行排序
for (int i = 0; i <=step ; i++) {
int k=i;//當前待確認的位置
int temp=a[k];//當前待確認位置的數據
//當前的值大於下一個值,則當前位置數據被下一個位置的值替換
while ((k+step)<len&&a[k]>a[k+step]){
a[k]=a[k+step];
k=k+step;
}
//最終位置k為最終確定的位置
a[k]=temp;
}
}
}
歸並排序(Merge Sort)
歸並排序(MERGE-SORT)是利用歸並的思想實現的排序方法,該算法采用經典的分治(divide-and-conquer)策略(分治法將問題分(divide)成一些小的問題然后遞歸求解,而治(conquer)的階段則將分的階段得到的各答案"修補"在一起,即分而治之)。
歸並排序是一種穩定的排序方法。將已有序的子序列合並,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合並成一個有序表,稱為2-路歸並。
歸並排序算法就是把序列遞歸划分成為一個個短序列,以其中只有1個元素的直接序列或者只有2個元素的序列作為短序列的遞歸出口,再將全部有序的短序列按照一定的規則進行排序為長序列。歸並排序融合了分治策略,即將含有n個記錄的初始序列中的每個記錄均視為長度為1的子序列,再將這n個子序列兩兩合並得到n/2個長度為2(當凡為奇數時會出現長度為l的情況)的有序子序列;將上述步驟重復操作,直至得到1個長度為n的有序長序列。
算法步驟
1 將列表按照對等的方式進行拆分
2 拆分小最小快的時候,在將最小塊按照原來的拆分,進行合並
3 合並的時候,通過左右兩塊的左邊開始比較大小。小的數據放入新的塊中
說明:簡單一點就是先對半拆成最小單位,然后將兩半數據合並成一個有序的列表。在進行元素比較和交換時,若兩個元素大小相等則不必刻意交換位置,因此該算法不會破壞序列的穩定性,即歸並排序也是穩定的排序算法
JAVA代碼實現
//歸並排序算法實現
public static void mergeSort(int[] a,int[] res,int start,int end){
if(start>=end)//結束遞歸的判斷,分割為一個的情況
return;
int len=end-start;//當前待排序數組的長度
int mid=start+len/2;//數組的中間位置
int start1=start,end1=mid;//數組的前半部分
int start2=mid+1,end2=end;//數組的后半部分
mergeSort(a,res,start1,end1);
mergeSort(a,res,start2,end2);
//執行的最小單元的時候
//start1--end1與start2--end2數據位置是連續的合並到一起組成有序的序列
//將數據排序后存入res
int k=start;//res的起始位置
//將兩組數按照從小到大順序進行存儲到res
do {
if(start1>end1)//第一組數據已經排序完,放入第二組數據
{
res[k++]=a[start2++];
continue;
}
if(start2>end2)//第二組數據已經排序完,放入第一組數據
{
res[k++]=a[start1++];
continue;
}
res[k++]=((a[start1]<=a[start2])?(a[start1++]):(a[start2++]));
}while (start1<=end1||start2<=end2);
//res只是一個臨時性的存儲,下次比較的時候還是比較的原始數據
//所以將res數據放入到a中
for (int x = start; x <=end ; x++) {
a[x]=res[x];
}
}
快速排序(Quick Sort)
快速排序(Quick Sort)使用分治法策略。它的基本思想是:選擇一個基准數,通過一趟排序將要排序的數據分割成獨立的兩部分;其中一部分的所有數據都比另外一部分的所有數據都要小。然后,再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
快速排序流程:
(1) 從數列中挑出一個基准值。
(2) 將所有比基准值小的擺放在基准前面,所有比基准值大的擺在基准的后面(相同的數可以到任一邊);在這個分區退出之后,該基准就處於數列的中間位置。
(3) 遞歸地把"基准值前面的子數列"和"基准值后面的子數列"進行排序。
算法步驟
1 從數列中挑出一個基准值。
2 將所有比基准值小的擺放在基准前面,所有比基准值大的擺在基准的后面(相同的數可以到任一邊);在這個分區退出之后,該基准就處於數列的中間位置。
3 遞歸地把"基准值前面的子數列"和"基准值后面的子數列"進行排序。
說明:
下面以數列a={30,40,60,10,20,50}為例,演示它的快速排序過程(如下圖):
只是給出了第1趟快速排序的流程。在第1趟中,設置x=a[i],即x=30。
(01) 從"右 --> 左"查找小於x的數:找到滿足條件的數a[j]=20,此時j=4;然后將a[j]賦值a[i],此時i=0;接着從左往右遍歷。
(02) 從"左 --> 右"查找大於x的數:找到滿足條件的數a[i]=40,此時i=1;然后將a[i]賦值a[j],此時j=4;接着從右往左遍歷。
(03) 從"右 --> 左"查找小於x的數:找到滿足條件的數a[j]=10,此時j=3;然后將a[j]賦值a[i],此時i=1;接着從左往右遍歷。
(04) 從"左 --> 右"查找大於x的數:找到滿足條件的數a[i]=60,此時i=2;然后將a[i]賦值a[j],此時j=3;接着從右往左遍歷。
(05) 從"右 --> 左"查找小於x的數:沒有找到滿足條件的數。當i>=j時,停止查找;然后將x賦值給a[i]。此趟遍歷結束!
按照同樣的方法,對子數列進行遞歸遍歷。最后得到有序數組!
JAVA代碼實現
//快速排序算法的實現
public static void quickSort(int[] a,int l,int r){
if(l<r){
int x=a[l];//定義最左邊的為基准數據
int i=l;//左邊界
int j=r;//右邊界
while(i<j){
// 由於定義的最左邊數據為基准,首先把基准數的位置空出來
// 所以先從右向左直到找到小於基准數數移到左邊
while(i<j&&a[j]>x)
j--;
if(i<j)//找到則把當前值移到最左邊的空位,並把左邊界右移
a[i++]=a[j];
//在從左到右直到找到大於基准數移到右邊
while (i<j&&a[i]<x)
i++;
if(i<j)//找到則把當前值移到最右邊的空位,並把右邊界左移
a[j--]=a[i];
}
//進行循環直到i==j,此處一定是空位則把基准值移到此處
a[i]=x;
//此時基准數位於中間,小於基准的無序數在左邊,大於基准的無序數在右邊,兩組數進行遞歸
quickSort(a,l,i-1);
quickSort(a,i+1,r);
}
}
堆排序(Heap Sort)
堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。堆排序可以說是一種利用堆的概念來排序的選擇排序。分為兩種方法:
- 大頂堆:每個節點的值都大於或等於其子節點的值,在堆排序算法中用於升序排列;
- 小頂堆:每個節點的值都小於或等於其子節點的值,在堆排序算法中用於降序排列;
對於n個元素的序列{R0, R1, ... , Rn}當且僅當滿足下列關系之一時,稱之為堆:
(1) Ri <= R2i+1 且 Ri <= R2i+2 (小根堆)
(2) Ri >= R2i+1 且 Ri >= R2i+2 (大根堆)
其中i=1,2,…,n/2向下取整;
設當前元素在數組中以R[i]表示,那么,
(1) 它的左孩子結點是:R[2*i+1];
(2) 它的右孩子結點是:R[2*i+2];
(3) 它的父結點是:R[(i-1)/2];
(4) R[i] <= R[2*i+1] 且 R[i] <= R[2i+2]。
算法步驟
1 根據初始的數構造初始的堆:大根堆(構建一個完全二叉樹,保證所有的父結點都比它的孩子結點數值大)
2 將堆首(最大值)和堆尾互換,此時確保了堆尾數據為當前堆中最大的數;
3 將堆的尺寸縮小1,然后把剩下元素重新調整為大根堆,調整后堆首依舊是當前堆最大值;
4 重復步驟2,每次重復都會將最大值放到堆尾,直到堆的尺寸為1,則全部排好。
一組數據5 ,16,3,10,17,4 初始堆的構造過程:
一組數 6, 8, 1, 9 ,3 ,0 ,7 ,2 , 4 ,5 的排序過程:
JAVA代碼實現
//堆排序的算法實現
public static void heapSort(int[] a){
//首先創建大頂堆,從最后一個非葉子節點從下至上,從右到左開始調整結構
int len=a.length;
int first=(len-1)/2;//第一個非葉子節點
for (int i = first ; i >=0 ; i--) {
//調用調整結構的公共方法
topHeap(a,first,a.length);
}
//創建大頂堆之后,將第一個元素與最后元素互換,然后剩余元素重新構造大頂堆
for (int i = len-1; i >0 ; i--) {
//交換第一個元素和最后一個元素
int temp=a[i];
a[i]=a[0];
a[0]=temp;
topHeap(a,0,i);
}
}
/**
* 構造大頂堆的方法
* @param parent 待調整的節點,一直調整到葉子節點
* @param length 數組的長度,用於判斷是否到達葉子節點
*/
public static void topHeap(int[] a ,int parent,int length){
//初始化最大節點為當前待確認位置節點
int max=parent;
//左子節點
int lChild= 2*parent+1;
//右子節點
int rChild=lChild+1;
if(lChild<length&&a[lChild]>a[max])
max=lChild;
if (rChild<length&&a[rChild]>a[max])
max=rChild;
//節點互換則執行遞歸操作直到最底層
if (max!=parent){
//交換數據
int temp=a[max];
a[max]=a[parent];
a[parent]=temp;
//將當前的數據進行遞歸操作
topHeap(a,max,length);
}
}
計數排序(Counting Sort)
計數排序的核心在於將輸入的數據值轉化為鍵存儲在額外開辟的數組空間中。作為一種線性時間復雜度的排序,計數排序要求輸入的數據必須是有確定范圍的整數。
算法步驟
1 找出待排序的數組A中最大max和最小的元素min
2 定義新數組C長度(max-min+1)
3 將原數組A中的數據與最小值min的差值作為新數組C的下標,C數組的值為元素出現的次數
4 輸出C中的數據到A中,C數組的下標i+min為原數據的值,C[i]的值為多少就輸出多少次
注意:若出現非穩定則反向填充目標數組:將每個元素i放在新數組的第C[i]項,每放一個元素就將C[i]減去1
JAVA代碼實現
//計數排序算法實現
public static void countingSort(int[] a){
//首先找到最大值與最小值
int min=a[0];
int max=a[0];
for (int i = 1; i < a.length; i++) {
if (a[i]<min)
min=a[i];
if(a[i]>max) {
max=a[i];
}
}
int len=max-min+1;
//定義新的數組C,大小為len,初始化數組元素的值都為0
int[] C=new int[len];
Arrays.fill(C,0);
//將原來數組的數據都放到新定義的數組中
for (int i = 0; i < a.length; i++) {
C[a[i]-min]++;//將數據與最小值的差值作為下標,沒處理一個數據則此位置的值+1
}
//此時所有的值都已經放到C中,遍歷輸出
for (int i=0,k = 0; i < C.length; i++) {
while (C[i]>0){
a[k++]=min+i;//設置結果的值為最小值加上下標值
C[i]--;//當前的數組值-1
}
}
}
桶排序(Bucket Sort)
桶排序是計數排序的升級版。它利用了函數的映射關系,高效與否的關鍵就在於這個映射函數的確定。桶排序 (Bucket sort)的工作的原理:假設輸入數據服從均勻分布,將數據分到有限數量的桶里,每個桶再分別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排)。
算法步驟
1 設置一個定量的數組當作空桶子;
2 遍歷輸入數據,並且把項目一個一個放到對應的桶子去;
3 對每個不是空的桶子進行排序,可以使用其它排序方法,也可以遞歸使用桶排序;
4 從不是空的桶子里把項目再放回原來的序列中。
桶排序假設數據會均勻入桶,在這個前提下,桶排序很快!
JAVA代碼實現
//桶排序算法實現
public static void bucketSort(int[] a){
//桶的定義根據實際的情況確認,當前以隨意的數據為例子
// 例如:數據在[0,100)整數,則初始化10個桶,數據/10為桶的下標
// 例如:數據在[0,1),則初始化10個桶,數據*10為桶的下標
//定義桶數
int max=a[0];
int min=a[0];
for (int i = 1; i < a.length; i++) {
if (max<a[i])
max=a[i];
if (min>a[i])
min=a[i];
}
int bucketCount=(max-min)/a.length+1;
ArrayList<Integer>[] buckets=new ArrayList[bucketCount];
//初始化桶
for (int i = 0; i < bucketCount; i++) {
buckets[i]=new ArrayList();
}
//將數據放入到桶中
for (int i = 0; i < a.length; i++) {
//首先確定放入的桶的下標
int num=(a[i]-min)/a.length;
//放入桶中
buckets[num].add(a[i]);
}
//對每個桶中的數據進行排序
for (int i = 0,k=0; i < buckets.length; i++) {
//桶中的數據
ArrayList<Integer> bucket=buckets[i];
//排序,此處自定義方法進行鏈表的排序
Collections.sort(bucket);
//排序后的數據放回到數組中
for (int value:bucket
) {
a[k++]=value;
}
}
}
基數排序(Radix Sort)
基數排序(radix sort)屬於“分配式排序”(distribution sort),又稱“桶子法”(bucket sort)或bin sort,顧名思義,它是透過鍵值的部份資訊,將要排序的元素分配至某些“桶”中,藉以達到排序的作用,基數排序法是屬於穩定性的排序。基數排序有從低位到高位的排序LSD,也有按照先高位后低位的排序MSD。
這里介紹LSD:基數排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最后的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。
算法步驟
將所有待比較數值統一為同樣的數位長度,數位較短的數前面補零。然后,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以后, 數列就變成一個有序序列。
1 首先確定最大值,並計算最大值的位數;
2 初始化0-9號十個桶,按個位數字,將關鍵字入桶,入完后,依次遍歷10個桶,按檢出順序回填到數組中
3 然后取十位數字將關鍵字入桶,入完后,依次遍歷10個桶,按檢出順序回填到數組中
4 假如數組中最大的數為三位數那么再將百位數字作關鍵字入桶,這樣就能實現基數排序了
JAVA代碼實現
//基數排序算法的實現
public static void radixSort(int[] a){
//首先確定最大值及最大位數
int max=a[0];//初始化最大值為a[0]
for (int i = 1; i < a.length; i++) {
if(a[i]>max)
max=a[i];
}
int maxDig=(max+"").length();
//定義10個桶並初始化,每個桶存儲每個位數與下標相同的數據鏈表
ArrayList<Integer>[] buckets=new ArrayList[10];
for (int i = 0; i < 10; i++) {
buckets[i]=new ArrayList<>();
}
//排序的次數為最大位數
for (int i = 1,k=0; k < maxDig;k++, i*=10) {
//進行第i位數的排序
for (int j = 0; j < a.length; j++) {
//第i位數的值放入對應的桶中
int value=a[j]/i%10;
buckets[value].add(a[j]);
}
//取出桶中的數據放回到原數組a中,並將桶清空
for (int j = 0,l=0; j < buckets.length; j++) {
ArrayList<Integer> bucket=buckets[j];
for (int value:bucket
) {
a[l++]=value;
}
buckets[j]=new ArrayList<>();//初始化
}
}
}