十大排序演算法--多圖預警
十大排序演算法
- 十大排序演算法
- 簡單的排序演算法
- 插入排序
- 氣泡排序
- 選擇排序
- 高效的比較排序演算法
- 希爾排序
- 快速排序
- 歸併排序
- 堆排序
- 犧牲空間的線性排序演算法
- 計數排序
- 桶排序
- 基數排序
- 綜合分析
- 簡單的排序演算法
簡單的排序演算法
Θ(n^2)
插入排序
- 動畫演示
enter description here
-
原理
將陣列看成兩部分,一部分為已排序好的陣列,後面的部分為未排序陣列,每次從後面的陣列中取出元素與前面的有序元素一一比較,若小於則向前移動,直到找到正確的位置插入。遍歷後面的陣列直到整個陣列排序完成。
-
程式碼
// 準備工作,交換函式 public static void exc(int[] a,int i, int j) { if (a[i]!=a[j]) { a[i]^=a[j]; a[j]^=a[i]; a[i]^=a[j]; } } // 插入排序 public static void insertSort(int[] a, int n) { for (int i = 1; i < n; i++) { for (int j = i; j>0&&a[j-1]>a[j]; j--) { exc(a, j, j-1); } } }
-
分析
時間複雜度
- 平均: n×n/4 次比較,n×n/4 次交換
- 最好: n-1 次比較,0次交換
- 最壞: n×n/2 次比較, n×n/2 交換
評價:
插入排序與陣列的逆序度有關,最好情況為 O(n),所以經常與快速排序一起出現,詳見C語言的quickSort的實現
氣泡排序
-
動畫演示
-
原理
就像泡泡一樣,不斷把大的數字往上浮,遍歷完整個陣列排序即完成。
-
程式碼
public static void bubbleSort(int[] a, int n) { boolean flag = true; for (int i = 0; i < n-1&&flag; i++) { flag = false; for (int j = 0; j < n-i-1; j++) { if (a[j]>a[j+1]) { exc(a, j, j+1); flag=true; } } } }
-
分析
時間複雜度:
- 平均情況下:冒泡比較的次數約是插入排序的兩倍,移動次數一致。
- 平均情況下: 冒泡比較的次數與比較排序是一樣的,移動次數多出n次。
評價:
大家也看到上述程式碼有個標記變數 flag,這是氣泡排序的一種改進,如果在第二次迴圈中沒有發生交換說明排序已經完成,不需要再迴圈下去了。
選擇排序
-
動畫演示
-
原理
選擇排序的原理很簡單,就是從需要排序的資料中
選擇
最小的(從小到大排序),然後放在第一個,選擇第二小的放在第二個…… -
程式碼
// 選擇排序,穩定 public static void selectSort(int[] a,int n) { for (int i = 0; i < n; i++) { int min=i; for (int j = i+1; j < n; j++) { if(a[min]>a[j]){ min = j; } } if (min!=i) { exc(a, i, min); } } }
-
分析
時間複雜度:
-
比較的次數: (n-1)+(n-2)+...+1= n(n-1)/2
-
交換的次數: n
評價:
- 執行時間與輸入無關,因為前一次的操作,不能為後面提供資訊
- 資料的移動次數是最小的
-
高效的比較排序演算法
Θ(nlogn)
希爾排序
-
圖片演示
-
原理
希爾排序是基於插入排序進行改進,又稱之為遞減增量排序。在前面中我們知道,插入排序是將小的元素往前挪動位置,並且每次只移動一個位置。那麼希爾排序是怎麼解決這個問題的呢?
希爾排序的理念和梳排序的理念有點類似。在梳排序中,我們比較距離相差為
step
的兩個元素來完成交換。在希爾排序中,我們的做法也是類似。我們在陣列中每隔h
取出陣列中的元素,然後進行插入排序。當h=1時,則就是前面所寫的插入排序了。 -
程式碼
// 6. 希爾排序 public static void shellSort(int[] a, int n) { int h =1; while (h<n/3) { // 陣列 1,4,13,40... h = h*3+1; } while (h>=1) { for (int i = h; i < n; i++) { for(int j=i;j>=h&&a[j-h]>a[j];j-=h){ exc(a, j, j-h); } } h/=3; } }
-
分析
是第一個突破時間複雜度O(n^2)的演算法
思路--計算步長,對每次分組進行直接插入排序,減小逆序度
演算法時間複雜度在插入排序和快速排序之間
快速排序
-
動畫演示
-
原理
快速排序使用了,
Divide and Conquer
(分治)策略,不斷地把陣列分為較大和較小的兩個子序列,然後對每個子序列進行遞迴,直到不可再分。思路就是在拆分的同時進行排序 與歸併排序
不同。 -
步驟:
-
挑選基準值:從數列中挑出一個元素,稱為“基準”(pivot),
-
分割:重新排序數列,所有比基準值小的元素擺放在基準前面,所有比基準值大的元素擺在基準後面(與基準值相等的數可以到任何一邊)。在這個分割結束之後,對基準值的排序就已經完成。
-
遞迴排序子序列:遞迴地將小於基準值元素的子序列和大於基準值元素的子序列排序。
遞迴到最底部的判斷條件是數列的大小是零或一,此時該數列顯然已經有序。
-
-
程式碼
// 第一部分 public static int partition(int[] a,int l,int h) { int mid = l+((h-l)>>1); int pivot = a[mid]; exc(a, l, mid); int i = l; int j = h+1; while (true) { while (a[++i]<pivot) { if(i==h) break; } while (a[--j]>pivot) { if(j==l) break; } if (i>=j) { break; } exc(a, i, j); } exc(a, l, j); return j; } public static void quickSort(int[] a, int n) { quickSort(a, 0, n-1); } // 第二部分 public static void quickSort(int[] a, int lo, int h) { if (lo>=h) { return; } int j = partition(a, lo, h); quickSort(a, lo, j-1); quickSort(a, j+1, h); }
-
分析
快速排序的最壞時間複雜度為O(n^2),平均時間複雜度為 O(n logn),快速排序基本上被認為是比較排序演算法中,平均效能最好的。多種語言皆實現了快速排序的類庫。
歸併排序
-
動畫演示
-
原理
採用分治法:
- 分割:遞迴地把當前序列平均分割成兩半。
- 整合:在保持元素順序的同時將上一步得到的子序列整合到一起(歸併)。
- 與快速排序不同的是,歸併是拆分完成後,在合併階段進行排序,而
快速排序
是邊拆分邊排序
-
程式碼
// 第一部分 合併 public static void merge(int[] a, int low, int mid, int high) { // 第一種寫法 int i = low; int j = mid + 1; int k = 0; int[] a2 = new int[high - low + 1]; while (i <= mid && j <= high) { if (a[i] < a[j]) { a2[k] = a[i]; i++; k++; } else { a2[k] = a[j]; j++; k++; } } while (i <= mid) { a2[k] = a[i]; i++; k++; } while (j <= high) { a2[k] = a[j]; j++; k++; } for (k = 0, i = low; i <= high; k++, i++) { a[i] = a2[k]; } } public static void mergeSort(int[] a, int n) { mergeSort(a, 0, n - 1); } // 第二部分 遞迴 public static void mergeSort(int[] a, int low, int high) { if (low >= high) return; int mid = (high + low) / 2; mergeSort(a, low, mid); mergeSort(a, mid + 1, high); merge(a, low, mid, high); }
-
分析
歸併排序是一種穩定的且十分高效的排序。時間複雜度總是 O(nlogn),不論好壞,但缺點是,它不是原地排序,佔用額外的空間,空間複雜度為 O(n)
堆排序
-
動畫演示
-
原理
堆排序是藉助堆這一資料結構實現的排序
我們利用大頂堆(堆頂元素最大)實現排序,在一個堆中,位置k的結點的父元素的位置是
(k+1)/2-1
,而它的兩個子節點的位置分別是2k+1
和2k+2
,這樣我們就可以通過計算陣列的索引在樹中上下移動。思路: 不斷把堆頂的元素與最後的元素交換位置,重新堆化,不斷得到第k(=1,2,3...)大的元素。相當於一個將大的元素 sink(下沉) 的過程。
-
程式碼
// 建堆 public static void buildHeap(int[] a, int n) { for (int i = n / 2; i >= 0; i--) { heapify(a, n - 1, i); } } // 堆化 public static void heapify(int[] a, int n, int i) { while (true) { int maxPos = i; if (i * 2 + 1 <= n && a[i] < a[2 * i + 1]) { maxPos = i * 2 + 1; } if (i * 2 + 2 <= n && a[maxPos] < a[i * 2 + 2]) { maxPos = i * 2 + 2; } if (i == maxPos) { break; } exc(a, i, maxPos); i = maxPos; } } public static void heapSort(int[] a, int n) { buildHeap(a, n); int k = n - 1; while (k > 0) { // 交換堆頂元素,把第1,2,3...大元素放到底部 exc(a, 0, k); --k; heapify(a, k, 0); } }
-
分析
- 時間複雜度一直都是 O(nlogn),不論最好最壞情況。
- 缺點:
- 不穩定演算法
- 堆排序的每次排序其陣列逆序度都比其他演算法高
- 對記憶體訪問不友好(不連續)
犧牲空間的線性排序演算法
Θ(n)
計數排序
-
動畫演示
-
原理
計數排序使用一個額外的陣列C,其中 C 中第i個元素是待排序陣列A中值等於i的元素的個數。然後根據陣列C 來將A中的元素排到正確的位置。
tips:當然,如果資料比較集中的話,我們大可不必建立那麼大的陣列,我們找出最小和最大的元素,以最小的元素作為基底以減小陣列的大小。
-
程式碼
// 非比較排序 public static void countSort(int[] a, int n) { int max = a[0]; for (int i = 0; i < n; i++) { if (a[i] > max) { max = a[i]; } } int[] c = new int[max + 1]; int indexArray = 0; for (int i = 0; i < n; i++) { c[a[i]]++; } for (int i = 0; i <= max; i++) { if (c[i] != 0) { a[indexArray] = i; c[i]--; indexArray++; } } }
桶排序
-
圖片演示
-
原理
桶排序的基本思想是假設資料在[min,max]之間均勻分佈,其中min、max分別指資料中的最小值和最大值。那麼將區間[min,max]等分成n份,這n個區間便稱為n個桶。將資料加入對應的桶中,然後每個桶內單獨排序。由於桶之間有大小關係,因此可以從大到小(或從小到大)將桶中元素放入到陣列中。
-
程式碼
public static void bucketSort(int[] a, int n, int bucketSize) { int max = a[0]; int min = a[1]; for (int v : a) { if (v > max) { max = v; } else if (v < min) { min = v; } } // 桶的大小 int bucketCount = (max - min) / bucketSize + 1; int bucket[][] = new int[bucketCount][bucketSize]; int indexArr[] = new int[bucketCount]; // 將數字放到對應的桶中 for (int v : a) { int j = (v - min) / bucketSize; if (indexArr[j] == bucket[j].length) { ensureCapacity(bucket, j); } bucket[j][indexArr[j]++] = v; } // 每個桶快排 // 也可以使用插入保證穩定性 int k = 0; for (int i = 0; i < bucketCount; i++) { if (indexArr[i] == 0) { continue; } quickSort(bucket[i], indexArr[i]); for (int j = 0; j < indexArr[i]; j++) { a[k++] = bucket[i][j]; } } } // 擴容函式 private static void ensureCapacity(int[][] bucket, int j) { int[] tempArr = bucket[j]; int[] newArr = new int[tempArr.length * 2]; for (int k = 0; k < tempArr.length; k++) { newArr[k] = tempArr[k]; } bucket[j] = newArr; }
-
分析
桶排序是線性排序的一種,桶排序的核心就是根據資料的範圍 (m) ,把資料 (大小為n),儘可能均勻得放到
K個桶
裡,每個桶再各自實現排序,然後把桶從小到大的列出來,即完成排序。- 時間複雜度 O(N+C),其中C=N*(logN-logK),空間複雜度為 O(N+K)
- 更適用於外部排序,尤其是當 N很大,而M較小時,比如高考排名,分數是固定的,從 0-750分,考生人數很多,用桶排序就能很快得出排名。
基數排序
-
動畫演示
-
原理
在日常的使用中,我們接觸基數排序比較少,它也是桶排序的一種變形。
它的具體實現分為 LSD (Least sgnificant digital) , MSD (Most sgnificant digital) 兩種方法,上面的演示是第一種(LSD),從低位到高位,根據每一位上的數字將元素放入桶中,再按順序取出,直到比較完最高位,完成排序。
-
程式碼
/** * * @param x 每一位上的值 * @param d 第d位 * @param dg 輔助陣列 * @return 對應的桶的標號 */ public static int getDigit(int x, int d, int[] dg) { return (x / dg[d - 1] % 10); } /** * * @param a 待排序陣列 * @param n 陣列長度 */ public static void radixSort(int[] a, int n) { // 最大的數 int max = 0; int j = 0, i = 0; // 預設十進位制 final int radix = 10; for (int val : a) { if (val > max) { max = val; } } // 求最大位數 int N; if (max == 0) { N = 1; } else { N = (int) Math.log10(max) + 1; } // 設定輔助陣列 int dg[] = new int[N + 1]; for (i = 1, dg[0] = 1; i < N + 1; i++) { dg[i] = 10 * dg[i - 1]; } // 初始化桶 int bucket[][] = new int[radix][n]; int indexArr[] = new int[radix]; for (int d = 1; d <= N; d++) { for (int var : a) { j = getDigit(var, d, dg); bucket[j][indexArr[j]++] = var; } int count = 0; for (i = 0; i < radix; i++) { if (indexArr[i] != 0) { for (int k = 0; k < indexArr[i]; k++) { a[count++] = bucket[i][k]; } indexArr[i] = 0; } } } }
-
分析
時間複雜度為 O(k*n),空間複雜度為O(n),當處理較大(位數多)的數字排序時,比計數排序更好用。
綜合分析
-
我們可以看出基於比較的排序演算法,他的時間複雜度的最好上界是逼近 O(nlogn) 的,這是因為,比較排序可以看成是決策樹,而陣列共有 n! 種排列方式,根據 斯特林公式 比較排序的時間複雜度的最好上界是接近於 nlogn的
-
我們可以看出基於非比較排序的線性時間排序的思路,大致相同,都是找到與元素匹配的桶,完成排序。都是空間換時間的思想。
文章最後,感謝大家的閱讀,文中若有錯漏之處,請在留言區積極指出,十分歡迎大家一起交流討論!
另外感謝朋友們的支援,友情連結 。