必學十大經典排序算法,看這篇就夠了

算法 計算複雜性理論 文章 程序員 數據結構 真致信息技術 2019-04-21

來自:算法與數據結構

說明

十大排序算法可以說是每個程序員都必須得掌握的了,花了一天的時間把代碼實現且整理了一下,為了方便大家學習,我把它整理成一篇文章,每種算法會有簡單的算法思想描述,為了方便大家理解,我還找來了動圖演示;這還不夠,我還附上了對應的優質文章,看完不懂你來砍我,如果不想砍我就給我來個好看

術語鋪墊

有些人可能不知道什麼是穩定排序、原地排序、時間複雜度、空間複雜度,我這裡先簡單解釋一下:

1、穩定排序:如果 a 原本在 b 的前面,且 a == b,排序之後 a 仍然在 b 的前面,則為穩定排序。

2、非穩定排序:如果 a 原本在 b 的前面,且 a == b,排序之後 a 可能不在 b 的前面,則為非穩定排序。

3、原地排序:原地排序就是指在排序過程中不申請多餘的存儲空間,只利用原來存儲待排數據的存儲空間進行比較和交換的數據排序。

4、非原地排序:需要利用額外的數組來輔助排序。

5、時間複雜度:一個算法執行所消耗的時間。

6、空間複雜度:運行完一個算法所需的內存大小。

十大排序講解順序

為了方便大家查找,我這裡弄一個偽目錄,沒有跳轉功能。

  • 選擇排序
  • 插入排序
  • 冒泡排序
  • 非優化版本
  • 優化版本
  • 希爾排序
  • 歸併排序
  • 遞歸式歸併排序
  • 非遞歸式歸併排序
  • 快速排序
  • 堆排序
  • 基數排序
  • 非優化版本
  • 優化版本
  • 桶排序
  • 基數排序

另:

代碼說明:代碼我自己寫的,並且都是經過好幾組數據測試通過,應該沒啥問題,如有錯,還請反饋下,謝謝。

圖片說明:圖片和動畫都是在百度搜索的,如有侵權,還望聯繫我刪除,謝謝

1、選擇排序

過程簡單描述:

首先,找到數組中最小的那個元素,其次,將它和數組的第一個元素交換位置(如果第一個元素就是最小元素那麼它就和自己交換)。其次,在剩下的元素中找到最小的元素,將它與數組的第二個元素交換位置。如此往復,直到將整個數組排序。這種方法我們稱之為選擇排序

為方便理解我還準備了動圖:


必學十大經典排序算法,看這篇就夠了


如果還是不懂的話我還給你準備了優質的文章講解:選擇排序

代碼如下(代碼片可以左右拉動,下同):

 1public class SelectSort {
2 public static int[] selectSort(int[] a) {
3 int n = a.length;
4 for (int i = 0; i < n - 1; i++) {
5 int min = i;
6 for (int j = i + 1; j < n; j++) {
7 if(a[min] > a[j]) min = j;
8 }
9 //交換
10 int temp = a[i];
11 a[i] = a[min];
12 a[min] = temp;
13 }
14 return a;
15 }
16}

性質:1、時間複雜度:O(n2) 2、空間複雜度:O(1) 3、非穩定排序 4、原地排序

2、插入排序

我們在玩打牌的時候,你是怎麼整理那些牌的呢?一種簡單的方法就是一張一張的來,將每一張牌插入到其他已經有序的牌中的適當位置。當我們給無序數組做排序的時候,為了要插入元素,我們需要騰出空間,將其餘所有元素在插入之前都向右移動一位,這種算法我們稱之為插入排序

過程簡單描述:

1、從數組第2個元素開始抽取元素。

2、把它與左邊第一個元素比較,如果左邊第一個元素比它大,則繼續與左邊第二個元素比較下去,直到遇到不比它大的元素,然後插到這個元素的右邊。

3、繼續選取第3,4,….n個元素,重複步驟 2 ,選擇適當的位置插入。

為方便理解我還準備了動圖:

必學十大經典排序算法,看這篇就夠了

如果還是不懂的話我還給你準備了優質的文章講解:插入排序

代碼如下:

 1public class InsertSort {
2 public static int[] insertSort(int[] arr) {
3 if(arr == null || arr.length < 2)
4 return arr;
5
6 int n = arr.length;
7 for (int i = 1; i < n; i++) {
8 int temp = arr[i];
9 int k = i - 1;
10 while(k >= 0 && arr[k] > temp)
11 k--;
12 //騰出位置插進去,要插的位置是 k + 1;
13 for(int j = i ; j > k + 1; j--)
14 arr[j] = arr[j-1];
15 //插進去
16 arr[k+1] = temp;
17 }
18 return arr;
19 }
20}

性質:1、時間複雜度:O(n2) 2、空間複雜度:O(1) 3、穩定排序 4、原地排序

3、冒泡排序

1、把第一個元素與第二個元素比較,如果第一個比第二個大,則交換他們的位置。接著繼續比較第二個與第三個元素,如果第二個比第三個大,則交換他們的位置….

我們對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對,這樣一趟比較交換下來之後,排在最右的元素就會是最大的數。

除去最右的元素,我們對剩餘的元素做同樣的工作,如此重複下去,直到排序完成。

為方便理解我還準備了動圖:

必學十大經典排序算法,看這篇就夠了

如果還是不懂的話我還給你準備了優質的文章講解:冒泡排序

代碼如下

 1public class BubbleSort {
2 public static int[] bubbleSort(int[] arr) {
3 if (arr == null || arr.length < 2) {
4 return arr;
5 }
6 int n = arr.length;
7 for (int i = 0; i < n; i++) {
8 for (int j = 0; j < n -i - 1; j++) {
9 if (arr[j + 1] < arr[j]) {
10 int t = arr[j];
11 arr[j] = arr[j+1];
12 arr[j+1] = t;
13 }
14 }
15 }
16 return arr;
17 }
18)

性質:1、時間複雜度:O(n2) 2、空間複雜度:O(1) 3、穩定排序 4、原地排序

優化一下冒泡排序的算法

假如從開始的第一對到結尾的最後一對,相鄰的元素之間都沒有發生交換的操作,這意味著右邊的元素總是大於等於左邊的元素,此時的數組已經是有序的了,我們無需再對剩餘的元素重複比較下去了。

代碼如下:

 1public class BubbleSort {
2 public static int[] bubbleSort(int[] arr) {
3 if (arr == null || arr.length < 2) {
4 return arr;
5 }
6 int n = arr.length;
7 for (int i = 0; i < n; i++) {
8 boolean flag = true;
9 for (int j = 0; j < n -i - 1; j++) {
10 if (arr[j + 1] < arr[j]) {
11 flag = false;
12 int t = arr[j];
13 arr[j] = arr[j+1];
14 arr[j+1] = t;
15 }
16 }
17 //一趟下來是否發生位置交換
18 if(false)
19 break;
20 }
21 return arr;
22 }
23}

4、希爾排序

希爾排序可以說是插入排序的一種變種。無論是插入排序還是冒泡排序,如果數組的最大值剛好是在第一位,要將它挪到正確的位置就需要 n - 1 次移動。也就是說,原數組的一個元素如果距離它正確的位置很遠的話,則需要與相鄰元素交換很多次才能到達正確的位置,這樣是相對比較花時間了。

希爾排序就是為了加快速度簡單地改進了插入排序,交換不相鄰的元素以對數組的局部進行排序。

希爾排序的思想是採用插入排序的方法,先讓數組中任意間隔為 h 的元素有序,剛開始 h 的大小可以是 h = n / 2,接著讓 h = n / 4,讓 h 一直縮小,當 h = 1 時,也就是此時數組中任意間隔為1的元素有序,此時的數組就是有序的了。

為方便理解我還準備了圖片:

必學十大經典排序算法,看這篇就夠了

如果還是不懂的話我還給你準備了優質的文章講解:希爾排序

代碼如下

 1public class ShellSort {
2 public static int[] shellSort(int arr[]) {
3 if (arr == null || arr.length < 2) return arr;
4 int n = arr.length;
5 // 對每組間隔為 h的分組進行排序,剛開始 h = n / 2;
6 for (int h = n / 2; h > 0; h /= 2) {
7 //對各個局部分組進行插入排序
8 for (int i = h; i < n; i++) {
9 // 將arr[i] 插入到所在分組的正確位置上
10 insertI(arr, h, i);
11 }
12 }
13 return arr;
14 }
15
16 /**
17 * 將arr[i]插入到所在分組的正確位置上
18 * arr[i]] 所在的分組為 ... arr[i-2*h],arr[i-h], arr[i+h] ...
19 */
20 private static void insertI(int[] arr, int h, int i) {
21 int temp = arr[i];
22 int k;
23 for (k = i - h; k > 0 && temp < arr[k]; k -= h) {
24 arr[k + h] = arr[k];
25 }
26 arr[k + h] = temp;
27 }
28}

需要注意的是,對各個分組進行插入的時候並不是先對一個組排序完了再來對另一個組排序,而是輪流對每個組進行排序。

性質:1、時間複雜度:O(nlogn) 2、空間複雜度:O(1) 3、非穩定排序 4、原地排序

5、歸併排序

將一個大的無序數組有序,我們可以把大的數組分成兩個,然後對這兩個數組分別進行排序,之後在把這兩個數組合併成一個有序的數組。由於兩個小的數組都是有序的,所以在合併的時候是很快的。

通過遞歸的方式將大的數組一直分割,直到數組的大小為 1,此時只有一個元素,那麼該數組就是有序的了,之後再把兩個數組大小為1的合併成一個大小為2的,再把兩個大小為2的合併成4的 ….. 直到全部小的數組合並起來。

為方便理解我還準備了動圖:

必學十大經典排序算法,看這篇就夠了

如果還是不懂的話我還給你準備了優質的文章講解:歸併排序

代碼如下:

 1public class MergeSort {
2 // 歸併排序
3 public static int[] mergeSort(int[] arr, int left, int right) {
4 // 如果 left == right,表示數組只有一個元素,則不用遞歸排序
5 if (left < right) {
6 // 把大的數組分隔成兩個數組
7 int mid = (left + right) / 2;
8 // 對左半部分進行排序
9 arr = mergeSort(arr, left, mid);
10 // 對右半部分進行排序
11 arr = mergeSort(arr, mid + 1, right);
12 //進行合併
13 merge(arr, left, mid, right);
14 }
15 return arr;
16 }
17
18 // 合併函數,把兩個有序的數組合並起來
19 // arr[left..mif]表示一個數組,arr[mid+1 .. right]表示一個數組
20 private static void merge(int[] arr, int left, int mid, int right) {
21 //先用一個臨時數組把他們合併彙總起來
22 int[] a = new int[right - left + 1];
23 int i = left;
24 int j = mid + 1;
25 int k = 0;
26 while (i <= mid && j <= right) {
27 if (arr[i] < arr[j]) {
28 a[k++] = arr[i++];
29 } else {
30 a[k++] = arr[j++];
31 }
32 }
33 while(i <= mid) a[k++] = arr[i++];
34 while(j <= right) a[k++] = arr[j++];
35 // 把臨時數組複製到原數組
36 for (i = 0; i < k; i++) {
37 arr[left++] = a[i];
38 }
39 }
40}

性質:1、時間複雜度:O(nlogn) 2、空間複雜度:O(n) 3、穩定排序 4、非原地排序

然而面試官要你寫個非遞歸式的歸併排序怎麼辦?別怕,我這還擼了個非遞歸式的歸併排序,代碼如下:

 1public class MergeSort {
2 // 非遞歸式的歸併排序
3 public static int[] mergeSort(int[] arr) {
4 int n = arr.length;
5 // 子數組的大小分別為1,2,4,8...
6 // 剛開始合併的數組大小是1,接著是2,接著4....
7 for (int i = 1; i < n; i += i) {
8 //進行數組進行劃分
9 int left = 0;
10 int mid = left + i - 1;
11 int right = mid + i;
12 //進行合併,對數組大小為 i 的數組進行兩兩合併
13 while (right < n) {
14 // 合併函數和遞歸式的合併函數一樣
15 merge(arr, left, mid, right);
16 left = right + 1;
17 mid = left + i - 1;
18 right = mid + i;
19 }
20 // 還有一些被遺漏的數組沒合併,千萬別忘了
21 // 因為不可能每個字數組的大小都剛好為 i
22 if (left < n && mid < n) {
23 merge(arr, left, mid, n - 1);
24 }
25 }
26 return arr;
27 }
28}

6、快速排序

我們從數組中選擇一個元素,我們把這個元素稱之為中軸元素吧,然後把數組中所有小於中軸元素的元素放在其左邊,所有大於或等於中軸元素的元素放在其右邊,顯然,此時中軸元素所處的位置的是有序的。也就是說,我們無需再移動中軸元素的位置。

從中軸元素那裡開始把大的數組切割成兩個小的數組(兩個數組都不包含中軸元素),接著我們通過遞歸的方式,讓中軸元素左邊的數組和右邊的數組也重複同樣的操作,直到數組的大小為1,此時每個元素都處於有序的位置

為方便理解我還準備了動圖:

必學十大經典排序算法,看這篇就夠了

如果還是不懂的話我還給你準備了優質的文章講解:不要在問我快速排序

代碼如下:

 1public class QuickSort {
2 public static int[] quickSort(int[] arr, int left, int right) {
3 if (left < right) {
4 //獲取中軸元素所處的位置
5 int mid = partition(arr, left, right);
6 //進行分割
7 arr = quickSort(arr, left, mid - 1);
8 arr = quickSort(arr, mid + 1, right);
9 }
10 return arr;
11 }
12
13 private static int partition(int[] arr, int left, int right) {
14 //選取中軸元素
15 int pivot = arr[left];
16 int i = left + 1;
17 int j = right;
18 while (true) {
19 // 向右找到第一個小於等於 pivot 的元素位置
20 while (i <= j && arr[i] <= pivot) i++;
21 // 向左找到第一個大於等於 pivot 的元素位置
22 while(i <= j && arr[j] >= pivot ) j--;
23 if(i >= j)
24 break;
25 //交換兩個元素的位置,使得左邊的元素不大於pivot,右邊的不小於pivot
26 int temp = arr[i];
27 arr[i] = arr[j];
28 arr[j] = temp;
29 }
30 arr[left] = arr[j];
31 // 使中軸元素處於有序的位置
32 arr[j] = pivot;
33 return j;
34 }
35}

性質:1、時間複雜度:O(nlogn) 2、空間複雜度:O(logn) 3、非穩定排序 4、原地排序

7、堆排序

堆的特點就是堆頂的元素是一個最值,大頂堆的堆頂是最大值,小頂堆則是最小值。

堆排序就是把堆頂的元素與最後一個元素交換,交換之後破壞了堆的特性,我們再把堆中剩餘的元素再次構成一個大頂堆,然後再把堆頂元素與最後第二個元素交換….如此往復下去,等到剩餘的元素只有一個的時候,此時的數組就是有序的了。

為方便理解我還準備了動圖:

必學十大經典排序算法,看這篇就夠了

如果還是不懂的話我還給你準備了優質的文章講解:堆排序是什麼鬼?

代碼如下:

 1public class Head {
2 // 堆排序
3 public static int[] headSort(int[] arr) {
4 int n = arr.length;
5 //構建大頂堆
6 for (int i = (n - 2) / 2; i >= 0; i--) {
7 downAdjust(arr, i, n - 1);
8 }
9 //進行堆排序
10 for (int i = n - 1; i >= 1; i--) {
11 // 把堆頂元素與最後一個元素交換
12 int temp = arr[i];
13 arr[i] = arr[0];
14 arr[0] = temp;
15 // 把打亂的堆進行調整,恢復堆的特性
16 downAdjust(arr, 0, i - 1);
17 }
18 return arr;
19 }
20
21 //下沉操作
22 public static void downAdjust(int[] arr, int parent, int n) {
23 //臨時保存要下沉的元素
24 int temp = arr[parent];
25 //定位左孩子節點的位置
26 int child = 2 * parent + 1;
27 //開始下沉
28 while (child <= n) {
29 // 如果右孩子節點比左孩子大,則定位到右孩子
30 if(child + 1 <= n && arr[child] < arr[child + 1])
31 child++;
32 // 如果孩子節點小於或等於父節點,則下沉結束
33 if (arr[child] <= temp ) break;
34 // 父節點進行下沉
35 arr[parent] = arr[child];
36 parent = child;
37 child = 2 * parent + 1;
38 }
39 arr[parent] = temp;
40 }
41}

性質:1、時間複雜度:O(nlogn) 2、空間複雜度:O(1) 3、非穩定排序 4、原地排序

8、計數排序

計數排序是一種適合於最大值和最小值的差值不是不是很大的排序。

基本思想:就是把數組元素作為數組的下標,然後用一個臨時數組統計該元素出現的次數,例如 temp[i] = m, 表示元素 i 一共出現了 m 次。最後再把臨時數組統計的數據從小到大彙總起來,此時彙總起來是數據是有序的。

為方便理解我還準備了動圖:

必學十大經典排序算法,看這篇就夠了

如果還是不懂的話我還給你準備了優質的文章講解:什麼是計數排序?

代碼如下:

 1public class Counting {
2 public static int[] countSort(int[] arr) {
3 if(arr == null || arr.length < 2) return arr;
4
5 int n = arr.length;
6 int max = arr[0];
7 // 尋找數組的最大值
8 for (int i = 1; i < n; i++) {
9 if(max < arr[i])
10 max = arr[i];
11 }
12 //創建大小為max的臨時數組
13 int[] temp = new int[max + 1];
14 //統計元素i出現的次數
15 for (int i = 0; i < n; i++) {
16 temp[arr[i]]++;
17 }
18 int k = 0;
19 //把臨時數組統計好的數據彙總到原數組
20 for (int i = 0; i <= max; i++) {
21 for (int j = temp[i]; j > 0; j--) {
22 arr[k++] = i;
23 }
24 }
25 return arr;
26 }
27}

性質:1、時間複雜度:O(n+k) 2、空間複雜度:O(k) 3、穩定排序 4、非原地排序

注:K表示臨時數組的大小,下同

優化一下

上面的代碼中,我們是根據 max 的大小來創建對應大小的數組,假如原數組只有10個元素,並且最小值為 min = 10000,最大值為 max = 10005,那我們創建 10005 + 1 大小的數組不是很吃虧,最大值與最小值的差值為 5,所以我們創建大小為6的臨時數組就可以了。

也就是說,我們創建的臨時數組大小 (max - min + 1)就可以了,然後在把 min作為偏移量。優化之後的代碼如下所示:

 1public class Counting {
2 public static int[] sort(int[] arr) {
3 if(arr == null || arr.length < 2) return arr;
4
5 int n = arr.length;
6 int min = arr[0];
7 int max = arr[0];
8 // 尋找數組的最大值與最小值
9 for (int i = 1; i < n; i++) {
10 if(max < arr[i])
11 max = arr[i];
12 if(min > arr[i])
13 min = arr[i];
14 }
15 int d = max - min + 1;
16 //創建大小為max的臨時數組
17 int[] temp = new int[d];
18 //統計元素i出現的次數
19 for (int i = 0; i < n; i++) {
20 temp[arr[i] - min]++;
21 }
22 int k = 0;
23 //把臨時數組統計好的數據彙總到原數組
24 for (int i = 0; i < d; i++) {
25 for (int j = temp[i]; j > 0; j--) {
26 arr[k++] = i + min;
27 }
28 }
29 return arr;
30 }
31}

9、桶排序

桶排序就是把最大值和最小值之間的數進行瓜分,例如分成 10 個區間,10個區間對應10個桶,我們把各元素放到對應區間的桶中去,再對每個桶中的數進行排序,可以採用歸併排序,也可以採用快速排序之類的。

之後每個桶裡面的數據就是有序的了,我們在進行合併彙總。

為方便理解我還準備了圖片:

必學十大經典排序算法,看這篇就夠了

如果還是不懂的話我還給你準備了優質的文章講解:什麼是桶排序?

代碼如下:

 1public class BucketSort {
2 public static int[] BucketSort(int[] arr) {
3 if(arr == null || arr.length < 2) return arr;
4
5 int n = arr.length;
6 int max = arr[0];
7 int min = arr[0];
8 // 尋找數組的最大值與最小值
9 for (int i = 1; i < n; i++) {
10 if(min > arr[i])
11 min = arr[i];
12 if(max < arr[i])
13 max = arr[i];
14 }
15 //和優化版本的計數排序一樣,弄一個大小為 min 的偏移值
16 int d = max - min;
17 //創建 d / 5 + 1 個桶,第 i 桶存放 5*i ~ 5*i+5-1範圍的數
18 int bucketNum = d / 5 + 1;
19 ArrayList<LinkedList<Integer>> bucketList = new ArrayList<>(bucketNum);
20 //初始化桶
21 for (int i = 0; i < bucketNum; i++) {
22 bucketList.add(new LinkedList<Integer>());
23 }
24 //遍歷原數組,將每個元素放入桶中
25 for (int i = 0; i < n; i++) {
26 bucketList.get((arr[i]-min)/d).add(arr[i] - min);
27 }
28 //對桶內的元素進行排序,我這裡採用系統自帶的排序工具
29 for (int i = 0; i < bucketNum; i++) {
30 Collections.sort(bucketList.get(i));
31 }
32 //把每個桶排序好的數據進行合併彙總放回原數組
33 int k = 0;
34 for (int i = 0; i < bucketNum; i++) {
35 for (Integer t : bucketList.get(i)) {
36 arr[k++] = t + min;
37 }
38 }
39 return arr;
40 }
41}

性質:1、時間複雜度:O(n+k) 2、空間複雜度:O(n+k) 3、穩定排序 4、非原地排序

注:k 表示桶的個數,下同

10、基數排序

基數排序的排序思路是這樣的:先以個位數的大小來對數據進行排序,接著以十位數的大小來多數進行排序,接著以百位數的大小……

排到最後,就是一組有序的元素了。不過,他在以某位數進行排序的時候,是用“桶”來排序的。

由於某位數(個位/十位….,不是一整個數)的大小範圍為0-9,所以我們需要10個桶,然後把具有相同數值的數放進同一個桶裡,之後再把桶裡的數按照0號桶到9號桶的順序取出來,這樣一趟下來,按照某位數的排序就完成了

為方便理解我還準備了動圖:

必學十大經典排序算法,看這篇就夠了

如果還是不懂的話我還給你準備了優質的文章講解:為什麼說O(n)複雜度的基數排序沒有快速排序快?

代碼如下:

 1public class RadioSort {
2
3 public static int[] radioSort(int[] arr) {
4 if(arr == null || arr.length < 2) return arr;
5
6 int n = arr.length;
7 int max = arr[0];
8 // 找出最大值
9 for (int i = 1; i < n; i++) {
10 if(max < arr[i]) max = arr[i];
11 }
12 // 計算最大值是幾位數
13 int num = 1;
14 while (max / 10 > 0) {
15 num++;
16 max = max / 10;
17 }
18 // 創建10個桶
19 ArrayList<LinkedList<Integer>> bucketList = new ArrayList<>(10);
20 //初始化桶
21 for (int i = 0; i < 10; i++) {
22 bucketList.add(new LinkedList<Integer>());
23 }
24 // 進行每一趟的排序,從個位數開始排
25 for (int i = 1; i <= num; i++) {
26 for (int j = 0; j < n; j++) {
27 // 獲取每個數最後第 i 位是數組
28 int radio = (arr[j] / (int)Math.pow(10,i-1)) % 10;
29 //放進對應的桶裡
30 bucketList.get(radio).add(arr[j]);
31 }
32 //合併放回原數組
33 int k = 0;
34 for (int j = 0; j < 10; j++) {
35 for (Integer t : bucketList.get(j)) {
36 arr[k++] = t;
37 }
38 //取出來合併了之後把桶清光數據
39 bucketList.get(j).clear();
40 }
41 }
42 return arr;
43 }
44}

性質:1、時間複雜度:O(kn) 2、空間複雜度:O(n+k) 3、穩定排序 4、非原地排序

總結

用一張圖彙總了10大排序算法的性質

必學十大經典排序算法,看這篇就夠了

如果你是複習/學習十大排序算法,一定要自己不看示例代碼手動實現一遍,一定要自己不看示例代碼手動實現一遍,一定要自己不看示例代碼手動實現一遍。

相關推薦

推薦中...