寫在前面
最近寫代碼的時候用到堆排序解決一些問題,然後寫的過程中發現有點手生,就想着記錄一些,然後又琢磨着乾脆直接整理一份排序的文章,附帶對排序的分析以及實現代碼,給自己練練手。
所有代碼都搞懂熟練,你還會怕排序麼?如果覺得有所幫助,記得點個關注和點個贊哦
冒泡排序
冒泡排序(Bubble Sort),是一種計算機科學領域的較簡單的排序算法。它重複地走訪過要排序的元素列,依次比較兩個相鄰的元素,如果他們的順序(如從大到小、首字母從A到Z)錯誤就把他們交換過來。走訪元素的工作是重複地進行直到沒有相鄰元素需要交換,也就是說該元素已經排序完成。
實現原理
- 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
- 對每一對相鄰元素做同樣的工作,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。
- 針對所有的元素重複以上的步驟,除了最後一個。
- 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
算法穩定性
冒泡排序總的平均時間複雜度爲 。冒泡排序就是把小的元素往前調或者把大的元素往後調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。所以,如果兩個元素相等,是不會再交換的;如果兩個相等的元素沒有相鄰,那麼即使通過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,所以相同元素的前後順序並沒有改變,所以冒泡排序是一種穩定排序算法。
代碼實現
public void bubbleSort(int[] sourceArray){
for (int i = 1; i < sourceArray.length; i++){
// 設定一個標記,若爲true,則表示此次循環沒有進行交換,也就是待排序列已經有序,排序已經完成。
boolean flag = true;
for (int j = 0; j < sourceArray.length - i; j++){
if (sourceArray[j] > sourceArray[j + 1]){
sourceArray[j] = sourceArray[j] ^ sourceArray[j + 1];
sourceArray[j + 1] = sourceArray[j] ^ sourceArray[j + 1];
sourceArray[j] = sourceArray[j] ^ sourceArray[j + 1];
flag = false;
}
}
if (flag){
break;
}
}
}
快速排序
快速排序(英語:Quicksort),又稱劃分交換排序(partition-exchange sort),簡稱快排,一種排序算法,最早由東尼·霍爾提出。快速排序通常明顯比其他算法更快,因爲它的內部循環(inner loop)可以在大部分的架構上很有效率地達成。
實現原理
- 從數列中挑出一個元素,稱爲 “基準”(pivot),
- 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區退出之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作。
- 遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
算法穩定性
在平均狀況下,排序個項目要 ,事實上,快速排序 通常明顯比其他算法更快。交換 a[r]
和 a[mid]
,完成一趟快速排序。在中樞元素和 a[r]
交換的時候,很有可能把前面的元素的穩定性打亂,比如序列爲5 3 3 4 3 8 9 10 11
,現在中樞元素 5
和 3(第5個元素,下標從1開始計)
交換就會把序列中,三個元素 3
的穩定性打亂(就是最後一個三到了最前面),所以快速排序是一個不穩定的排序算法。
代碼實現
public void quickSort(int[] sourceArray, int left, int right){
if (left >= right) return;
int l = left;
int r = right;
int mid = sourceArray[left];
while (l < r) {
while (l < r && sourceArray[r] >= mid)
r--;
if (l < r)
sourceArray[l++] = sourceArray[r];
while (l < r && sourceArray[l] < mid)
l++;
if (l < r)
sourceArray[r--] = sourceArray[l];
}
sourceArray[l] = mid;
quickSort(sourceArray, left, l - 1);
quickSort(sourceArray, l + 1, right);
}
快速選擇排序
這裏單獨提一下一個混合的算法,是基於快速排序而來的,類似對快速排序進行剪枝,用於計算第大(小)的數。快速選擇算法的平均時間複雜度爲 。就像快速排序那樣,本算法也是 Tony Hoare 發明的,因此也被稱爲 Hoare選擇算法。本方法大致上與快速排序相同。簡便起見,注意到第 k
個最大元素也就是第 N - k
個最小元素,因此可以用第 k
小算法來解決本問題。
首先,我們選擇一個樞軸,並在線性時間內定義其在排序數組中的位置。這可以通過 劃分算法 的幫助來完成。
爲了實現劃分,沿着數組移動,將每個元素與樞軸進行比較,並將小於樞軸的所有元素移動到樞軸的左側。
這樣,在輸出的數組中,樞軸達到其合適位置。所有小於樞軸的元素都在其左側,所有大於或等於的元素都在其右側。這樣,數組就被分成了兩部分。如果是快速排序算法,會在這裏遞歸地對兩部分進行快速排序,時間複雜度爲 。而在這裏,由於知道要找的第 N - k
小的元素在哪部分中,我們不需要對兩部分都做處理,這樣就將平均時間複雜度下降到 ,最終的算法十分直接了當 :
- 隨機選擇一個樞軸。
- 使用劃分算法將樞軸放在數組中的合適位置 pos。將小於樞軸的元素移到左邊,大於等於樞軸的元素移到右邊。
- 比較 pos 和 N - k 以決定在哪邊繼續遞歸處理。
選擇排序
選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工作原理是每一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,直到全部待排序的數據元素排完。 選擇排序是不穩定的排序方法。
實現原理
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。
- 重複第二步,直到所有元素均排序完畢。
算法穩定性
選擇排序是 的時間複雜度,在一趟選擇,如果一個元素比當前元素小,而該小的元素又出現在一個和當前元素相等的元素後面,那麼交換後穩定性就被破壞了。比較拗口,舉個例子,序列 5 8 5 2 9
,我們知道第一遍選擇第 1
個元素 5
會和 2
交換,那麼原序列中兩個 5
的相對前後順序就被破壞了,所以選擇排序是一個不穩定的排序算法。
代碼實現
public void selectSort(int[] sourceArray){
// 總共要經過 N-1 輪比較
for (int i = 0; i < sourceArray.length - 1; i++){
int cur = i;
// 每輪需要比較的次數 N-i
for (int j = i + 1; j < sourceArray.length; j++){
if (sourceArray[cur] > sourceArray[j]) {
// 記錄目前能找到的最小值元素的下標
cur = j;
}
}
// 將找到的最小值和i位置所在的值進行交換
if (i != cur){
sourceArray[i] = sourceArray[i] ^ sourceArray[cur];
sourceArray[cur] = sourceArray[i] ^ sourceArray[cur];
sourceArray[i] = sourceArray[i] ^ sourceArray[cur];
}
}
}
插入排序
插入排序(英語:Insertion Sort)是一種簡單直觀的排序算法。它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,通常採用in-place排序(即只需用到 的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。
實現原理
- 從第一個元素開始,該元素可以認爲已經被排序
- 取出下一個元素,在已經排序的元素序列中從後向前掃描
- 如果該元素(已排序)大於新元素,將該元素移到下一位置
- 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
- 將新元素插入到該位置後
- 重複步驟2~5
算法穩定性
平均來說插入排序算法複雜度爲 ,如果碰見一個和插入元素相等的,那麼插入元素把想插入的元素放在相等元素的後面。所以,相等元素的前後順序沒有改變,從原無序序列出去的順序就是排好序後的順序,所以插入排序是穩定的
。
代碼實現
public void insertSort(int[] sourceArray){
// 從下標爲1的元素開始選擇合適的位置插入,因爲下標爲0的只有一個元素,默認是有序的
for (int i = 1; i < sourceArray.length; i++){
// 記錄要插入的數據
int temp = sourceArray[i];
// 從已經排序的序列最右邊的開始比較,找到比其小的數
int j = i;
while (j > 0 && temp < sourceArray[j - 1]){
sourceArray[j] = sourceArray[j - 1];
j--;
}
// 存在比其小的數,插入
if (j != i) sourceArray[j] = temp;
}
}
歸併排序
歸併排序(MERGE-SORT)是建立在歸併操作上的一種有效的排序算法,該算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲二路歸併。
實現原理
- 第一步:申請空間,使其大小爲兩個已經 排序序列之和,該空間用來存放合併後的序列
- 第二步:設定兩個 指針,最初位置分別爲兩個已經排序序列的起始位置
- 第三步:比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置
- 重複步驟3直到某一指針超出序列尾
- 將另一序列剩下的所有元素直接複製到合併序列尾
算法穩定性
始終都是 的時間複雜度,代價是需要額外的內存空間。可以發現,在1
個或2
個元素時,1
個元素不會交換,2
個元素如果大小相等也沒有人故意交換,這不會破壞穩定性。那麼,在短的有序序列合併的過程中,穩定性是否受到破壞?沒有,合併過程中我們可以保證如果兩個當前元素相等時,我們把處在前面的序列的元素保存在結果序列的前面,這樣就保證了穩定性。所以,歸併排序也是穩定的排序算法。
代碼實現
public int[] mergeSort(int[] sourceArray){
if (sourceArray.length < 2) return sourceArray;
int mid = (int) Math.floor(sourceArray.length / 2);
int[] left = Arrays.copyOfRange(sourceArray, 0, mid);
int[] right = Arrays.copyOfRange(sourceArray, mid, sourceArray.length);
return merge(mergeSort(left), mergeSort(right));
}
private int[] merge(int[] left, int[] right){
int[] nums = new int[left.length + right.length];
int l = 0;
int r = 0;
int i = 0;
while (l < left.length && r < right.length){
if (left[l] < right[r]) {
nums[i] = left[l];
l++;
}else {
nums[i] = right[r];
r++;
}
i++;
}
while (l < left.length){
nums[i] = left[l];
l++;i++;
}
while (r < right.length){
nums[i] = right[r];
r++;i++;
}
return nums;
}
基數排序
基數排序(radix sort)屬於“分配式排序”(distribution sort),又稱“桶子法”(bucket sort)或bin sort,顧名思義,它是透過鍵值的部份資訊,將要排序的元素分配至某些“桶”中,藉以達到排序的作用,基數排序法是屬於穩定性的排序,其時間複雜度爲O (nlog®m),其中r爲所採取的基數,而m爲堆數,在某些時候,基數排序法的效率高於其它的穩定性排序法。
實現原理
假設原來有一串數值如下所示:73, 22, 93, 43, 55, 14, 28, 65, 39, 81
,首先根據個位數的數值,在走訪數值時將它們分配至編號 0
到 9
的桶子中:
0
1 81
2 22
3 73 93 43
4 14
5 55 65
6
7
8 28
9 39
接下來將這些桶子中的數值重新串接起來,成爲以下的數列:81, 22, 73, 93, 43, 14, 55, 65, 28, 39
,接着再進行一次分配,這次是根據十位數來分配:
0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93
接下來將這些桶子中的數值重新串接起來,成爲以下的數列:14, 22, 28, 39, 43, 55, 65, 73, 81, 93
,這時候整個數列已經排序完畢;如果排序的對象有三位數以上,則持續進行以上的動作直至最高位數爲止。LSD
的基數排序適用於位數小的數列,如果位數多的話,使用MSD
的效率會比較好。MSD
的方式與LSD
相反,是由高位數爲基底開始進行分配,但在分配之後並不馬上合併回一個數組中,而是在每個“桶子”中建立“子桶”,將每個桶子中的數值按照下一數位的值分配到“子桶”中。在進行完最低位數的分配後再合併回單一的數組中。
算法穩定性
基數排序的時間複雜度爲 ,有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序,最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。基數排序基於分別排序,分別收集,所以其是穩定的排序算法。
代碼實現
public void radixSort(int[] sourceArray){
int maxDigit = getMaxDigit(sourceArray);
int mod = 10;
int dev = 1;
for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10){
int[][] counter = new int[mod * 2][0];
for (int j = 0; j < sourceArray.length; j++){
int bucket = ((sourceArray[j] % mod) / dev) + mod;
counter[bucket] = arrayAppend(counter[bucket], sourceArray[j]);
}
int pos = 0;
for (int[] bucket : counter){
for (int value : bucket){
sourceArray[pos++] = value;
}
}
}
}
private int getMaxDigit(int[] arr){
int maxValue = getMaxValue(arr);
return getNumLength(maxValue);
}
private int getMaxValue(int[] arr){
int maxValue = arr[0];
for (int value : arr){
if (maxValue < value){
maxValue = value;
}
}
return maxValue;
}
private int getNumLength(long num){
if (num == 0){
return 1;
}
int length = 0;
for (long temp = num; temp != 0; temp /= 10){
length++;
}
return length;
}
private int[] arrayAppend(int[] arr, int value){
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
希爾排序(shell)
希爾排序(Shell’s Sort)是插入排序的一種又稱“縮小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一種更高效的改進版本。希爾排序是非穩定排序算法。該方法因D.L.Shell於1959年提出而得名。
實現原理
先取一個小於 n
的整數 d1
作爲第一個增量,把文件的全部記錄分組。所有距離爲 d1
的倍數的記錄放在同一個組中。先在各組內進行直接插入排序;然後,取第二個增量 d2=1(<…<d2<d1)
,即所有記錄放在同一組中進行直接插入排序爲止。
算法穩定性
希爾排序的時間複雜度會比 好一些,由於多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂,所以shell排序是不穩定的。
代碼實現
public void shellSort(int[] sourceArray){
int gap = sourceArray.length;
while (true){
gap /= 2;
for (int i = 0; i < gap; i++){
for (int j = i + gap; j < sourceArray.length; j += gap){
int temp = sourceArray[j];
int k = j - gap;
while (k >= 0 && sourceArray[k] > temp){
sourceArray[k + gap] = sourceArray[k];
k -= gap;
}
sourceArray[k + gap] = temp;
}
}
if (gap == 1) break;
}
}
堆排序
堆排序(英語:Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
實現原理
- 創建一個堆 H[0……n-1];
- 把堆首(最大值)和堆尾互換;
- 把堆的尺寸縮小 1,並調用 shift_down(0),目的是把新的數組頂端數據調整到相應位置;
- 重複步驟 2,直到堆的尺寸爲 1。
算法穩定性
堆排序的平均時間複雜度爲 。我們知道堆的結構是節點 i
的孩子爲 2 * i
和 2 * i + 1
節點,大頂堆要求父節點大於等於其 2
個子節點,小頂堆要求父節點小於等於其 2
個子節點。在一個長爲 n
的序列,堆排序的過程是從第 n / 2
開始和其子節點共 3
個值選擇最大(大頂堆)或者最小(小頂堆),這 3
個元素之間的選擇當然不會破壞穩定性。但當爲 n / 2 - 1, n / 2 - 2, ... 1
這些個父節點選擇元素時,就會破壞穩定性。有可能第 n / 2
個父節點交換把後面一個元素交換過去了,而第 n / 2 - 1
個父節點把後面一個相同的元素沒 有交換,那麼這 2
個相同的元素之間的穩定性就被破壞了。所以,堆排序不是穩定的排序算法。
代碼實現
public void heapSort(int[] sourceArray){
int len = sourceArray.length;
buildMaxHeap(sourceArray, len);
for (int i = len - 1; i > 0; i--){
swap(sourceArray, 0, i);
len--;
heapify(sourceArray, 0, len);
}
}
private void buildMaxHeap(int[] arr, int len){
for (int i = (int) Math.floor(len / 2); i >= 0; i--){
heapify(arr, i, len);
}
}
private void heapify(int[] arr, int i, int len){
int left = 2 * i + 1;
int right = 2 * i + 2;
int largest = i;
if (left < len && arr[left] > arr[largest]){
largest = left;
}
if (right < len && arr[right] > arr[largest]){
largest = right;
}
if (largest != i){
swap(arr, i, largest);
heapify(arr, largest, len);
}
}
private void swap(int[] arr, int i, int j){
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
計數排序
計數排序(Counting sort)是一種穩定的線性時間排序算法。計數排序使用一個額外的數組,其中第 個元素是待排序數組中值等於的元素的個數。然後根據數組來將中的元素排到正確的位置。
實現原理
- 找出待排序的數組中最大和最小的元素
- 統計數組中每個值爲i的元素出現的次數,存入數組C的第i項
- 對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加)
- 反向填充目標數組:將每個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1
算法穩定性
計數排序的時間複雜度是 ,由於用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上 1
),這使得計數排序對於數據範圍很大的數組,需要大量時間和內存。例如:計數排序是用來排序 0
到100
之間的數字的最好的算法,但是它不適合按字母順序排序人名。但是,計數排序可以用在基數排序中的算法來排序數據範圍很大的數組。年齡有重複時需要特殊處理(保證穩定性),這就是爲什麼最後要反向填充目標數組,以及將每個數字的統計減去 1
的原因。所以計數排序是穩定的。
代碼實現
private static void countingSort(int[] arr, int maxValue) {
int bucketLen = maxValue + 1;
int[] bucket = new int[bucketLen];
for (int value : arr) {
bucket[value]++;
}
int sortedIndex = 0;
for (int j = 0; j < bucketLen; j++) {
while (bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}
}
private static int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}