十種排序算法的講解過程


一、排序算法概述

1、定義

將雜亂無章的數據元素,通過一定的方法按關鍵字順序排列的過程叫做排序。

2、分類

十種常見排序算法可以分為兩大類:

  • 非線性時間比較類排序:通過比較來決定元素間的相對次序,由於其時間復雜度不能突破O(nlogn),因此稱為非線性時間比較類排序。

  • 線性時間非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間運行,因此稱為線性時間非比較類排序。

 
 

3、比較

 
 

4、相關概念

  • 穩定:如果a原本在b前面且a=b,排序之后a仍然在b的前面。
  • 不穩定:如果a原本在b的前面且a=b,排序之后 a 可能會出現在 b 的后面。
  • 時間復雜度:對排序數據的總的操作次數。反映當n變化時,操作次數呈現什么規律。
  • 空間復雜度:是指算法在計算機內執行時所需存儲空間的度量,它也是數據規模n的函數。
  • 內部排序:所有排序操作都在內存中完成。本文主要介紹的是內部排序。
  • 外部排序:待排序記錄的數量很大,以致於內存不能一次容納全部記錄,所以在排序過程中需要對外存進行訪問的排序過程。

二、各算法原理及實現

下面我們來逐一分析十大經典排序算法,主要圍繞下列問題展開:
1、算法的基本思想是什么?
2、算法的代碼實現?
3、算法的時間復雜度是多少?(平均、最好、最壞)什么情況下最好?什么情況下最壞?
4、算法的空間復雜度是多少?
5、算法的穩定性如何?

1、冒泡排序(Bubble Sort)

① 基本思想
冒泡排序是一種簡單的排序算法。它重復地走訪過要排序的數列,一次比較兩個元素,如果它們的順序錯誤就把它們交換過來。走訪數列的工作是重復地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個算法的名字由來是因為每趟比較將當前數列未排序部分的最大的元素“沉”到數列末端,而小的元素會經由交換慢慢“浮”到數列的頂端。

② 算法描述
1)比較相鄰的元素。如果前一個比后一個大,就交換它們兩個;
2)對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最后一對,這樣在最后的元素應該會是最大的數;
3)針對所有的元素重復以上的步驟,除了最后一個;
4)重復步驟1~3,直到排序完成。為了優化算法,可以設立一個布爾標識,每趟排序開始前設為false,如果該趟排序發生了交換就置為true,如果一趟排序結束標識仍為false表示該趟排序沒有發生交換,即數組已經有序,可以提前結束排序。

③ 動圖演示

 
 

 

④ 代碼實現

 public static int[] bubbleSort(int[] array) { if (array.length == 0) return array; for (int i = 0; i < array.length; i++){ //外層循環一次為一趟排序 /*設置標識,判斷這趟排序是否發生了交換。 如果未發生交換,則說明數組已經有序,不必再排序了*/ boolean isSwap = false; for (int j = 0; j < array.length - 1 - i; j++) //內層循環一次為一次相鄰比較 if (array[j + 1] < array[j]) { int temp = array[j + 1]; array[j + 1] = array[j]; array[j] = temp; isSwap = true; } if(!isSwap) break; } return array; } 

⑤ 時間復雜度
冒泡排序平均時間復雜度為O(n2),最好時間復雜度為O(n),最壞時間復雜度為O(n2)。
最好情況:如果待排序元素本來是正序的,那么一趟冒泡排序就可以完成排序工作,比較和移動元素的次數分別是 (n - 1) 和 0,因此最好情況的時間復雜度為O(n)。
最壞情況:如果待排序元素本來是逆序的,需要進行 (n - 1) 趟排序,所需比較和移動次數分別為 n * (n - 1) / 2和 3 * n * (n-1) / 2。因此最壞情況下的時間復雜度為O(n2)。

⑥ 空間復雜度
冒泡排序使用了常數空間,空間復雜度為O(1)

⑦ 穩定性
當 array[j] == array[j+1] 的時候,我們不交換 array[i] 和 array[j],所以冒泡排序是穩定的。

⑧ 算法拓展
雞尾酒排序,又稱定向冒泡排序、攪拌排序等,是對冒泡排序的改進。在把最大的數往后面冒泡的同時,把最小的數也往前面冒泡,同時收縮無序區的左右邊界,有序區在序列左右逐漸累積。

動圖如下:

 

 
 

 

代碼如下:

public static void cocktailSort(int[] array) { int left = 0,right = array.length-1; while(left < right) { for(int i = left; i < right; i++) if(array[i] > array[i+1]) swap(array,i,i + 1); right--; for(int i = right; i > left; i--) if(array[i] < array[i-1]) swap(array,i,i-1); left++; } } 

雞尾酒排序是穩定的。它的平均時間復雜度為O(n2),最好情況是待排序列原先就是正序的,時間復雜度為O(n),最壞情況是待排序列原先是逆序的,時間復雜度為O(n2)。空間復雜度為O(1)。

2、簡單選擇排序(Selection Sort)

① 基本思想
簡單選擇排序(Selection-sort)是一種簡單直觀的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再從剩余未排序元素中繼續尋找最小(大)元素,然后放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。

② 算法描述
n個記錄的簡單選擇排序可經過(n-1)趟簡單選擇排序得到有序結果。具體算法描述如下:
1)初始狀態:無序區為R[1..n],有序區為空;
2)第i趟排序(i=1,2,3…n-1)開始時,當前有序區和無序區分別為R[1..i-1]和R[i..n]。該趟排序從當前無序區中選出關鍵字最小的記錄 R[k],將它與無序區的第1個記錄R交換,使R[1..i]和R[i+1..n]分別變為記錄個數增加1個的新有序區和記錄個數減少1個的新無序區;
3)(n-1)趟結束,數組有序化了。

③ 動圖演示

 
 

 

④ 代碼實現

public static int[] selectionSort(int[] array) { if (array.length == 0) return array; for (int i = 0; i < array.length; i++) { int minIndex = i; for (int j = i; j < array.length; j++) { if (array[j] < array[minIndex]) //找到最小的數 minIndex = j; //將最小數的索引保存 } int temp = array[minIndex]; //將最小數和無序區的第一個數交換 array[minIndex] = array[i]; array[i] = temp; } return array; } 

⑤ 時間復雜度
簡單選擇排序平均時間復雜度為O(n2),最好時間復雜度為O(n2),最壞時間復雜度為O(n2)。
最好情況:如果待排序元素本來是正序的,則移動元素次數為 0,但需要進行 n * (n - 1) / 2 次比較。
最壞情況:如果待排序元素中第一個元素最大,其余元素從小到大排列,則仍然需要進行 n * (n - 1) / 2 次比較,且每趟排序都需要移動 3 次元素,即移動元素的次數為3 * (n - 1)次。
需要注意的是,簡單選擇排序過程中需要進行的比較次數與初始狀態下待排序元素的排列情況無關。

⑥ 空間復雜度
簡單選擇排序使用了常數空間,空間復雜度為O(1)

⑦ 穩定性
簡單選擇排序不穩定,比如序列 2、4、2、1,我們知道第一趟排序第 1 個元素 2 會和 1 交換,那么原序列中 2 個 2 的相對前后順序就被破壞了,所以簡單選擇排序不是一個穩定的排序算法。

3、直接插入排序(Insertion Sort)

① 基本思想
直接插入排序(Insertion-Sort)的算法描述是一種簡單直觀的排序算法。它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從后向前掃描,找到相應位置並插入。

② 算法描述
一般來說,直接插入排序都采用in-place(原地算法)在數組上實現。具體算法描述如下:
1)從第一個元素開始,該元素可以認為已經被排序;
2)取出下一個元素,在已經排序的元素序列中從后向前掃描;
3)如果該元素(已排序)大於新元素,將該元素移到下一位置;
4)重復步驟3,直到找到已排序的元素小於或者等於新元素的位置;
5)將新元素插入到該位置后;
6)重復步驟2~5。

③ 動圖演示

 
 

 

④ 代碼實現

public static int[] insertionSort(int[] array) { if (array.length == 0) return array; int current; for (int i = 1; i < array.length; i++) { current = array[i]; int preIndex = i - 1; while (preIndex >= 0 && current < array[preIndex]) { array[preIndex + 1] = array[preIndex]; preIndex--; } array[preIndex + 1] = current; } return array; } 

⑤ 時間復雜度
直接插入排序平均時間復雜度為O(n2),最好時間復雜度為O(n),最壞時間復雜度為O(n2)。
最好情況:如果待排序元素本來是正序的,比較和移動元素的次數分別是 (n - 1) 和 0,因此最好情況的時間復雜度為O(n)。
最壞情況:如果待排序元素本來是逆序的,需要進行 (n - 1) 趟排序,所需比較和移動次數分別為 n * (n - 1) / 2和 n * (n - 1) / 2。因此最壞情況下的時間復雜度為O(n2)。

⑥ 空間復雜度
直接插入排序使用了常數空間,空間復雜度為O(1)

⑦ 穩定性
直接插入排序是穩定的。

⑧ 算法拓展
在直接插入排序中,待插入的元素總是在有序區線性查找合適的插入位置,沒有利用有序的優勢,考慮使用二分查找搜索插入位置進行優化,即二分插入排序

代碼實現如下:

 public static int[] BinaryInsertionSort(int[] array) { if (array.length == 0) return array; for(int i = 1;i < array.length;i++) { int left = 0; int right = i - 1; // left 和 right 分別為有序區的左右邊界 int current = array[i]; while (left <= right) { //搜索有序區中第一個大於 current 的位置,即為 current 要插入的位置 int mid = left + ((right - left) >> 1); if(array[mid] > current){ right = mid - 1; }else{ left = mid + 1; } } for(int j = i - 1;j >= left;j--) { array[j + 1] = array[j]; } array[left] = current; // left 為第一個大於 current 的位置,插入 current } return array; } 

二分插入排序是穩定的。它的平均時間復雜度是O(n2),最好時間復雜度為O(nlogn),最壞時間復雜度為O(n2)。

4、希爾排序(Shell Sort)

① 基本思想
1959年Shell發明,第一個突破O(n2)的排序算法,是直接插入排序的改進版。它與直接插入排序的不同之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序

② 算法描述
先將整個待排元素序列分割成 gap 個增量為 gap 的子序列(每個子序列由位置相差為 gap 的元素組成,整個序列正好分割成 gap 個子序列,每個序列中有 n / gap 個元素)分別進行直接插入排序,然后縮減增量為之前的一半再進行排序,待 gap == 1時,希爾排序就變成了直接插入排序。因為此時序列已經基本有序,直接插入排序在元素基本有序的情況下(接近最好情況),效率是很高的。gap初始值一般取 len / 2。

③ 動圖演示

 
 

 

④ 代碼實現

 public static int[] ShellSort(int[] array) { int len = array.length; if(len == 0) return array; int current, gap = len / 2; while (gap > 0) { for (int i = gap; i < len; i++) { current = array[i]; int preIndex = i - gap; while (preIndex >= 0 && array[preIndex] > current) { array[preIndex + gap] = array[preIndex]; preIndex -= gap; } array[preIndex + gap] = current; } gap /= 2; } return array; } 

⑤ 時間復雜度
希爾排序平均時間復雜度為O(nlogn),最好時間復雜度為O(nlog2n),最壞時間復雜度為O(nlog2n)。希爾排序的時間復雜度與增量序列的選取有關。

⑥ 空間復雜度
希爾排序使用了常數空間,空間復雜度為O(1)

⑦ 穩定性
由於相同的元素可能在各自的序列中插入排序,最后其穩定性就會被打亂,比如序列 2、4、1、2,所以希爾排序是不穩定的。

5、歸並排序(Merge Sort)

① 基本思想
歸並排序是建立在歸並操作上的一種有效的排序算法。該算法是采用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合並,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合並成一個有序表,稱為2-路歸並。

② 算法描述
1)把長度為 n 的輸入序列分成兩個長度為 n / 2 的子序列;
2)對這兩個子序列分別采用歸並排序;
3)將兩個排序好的子序列合並成一個最終的排序序列。

③ 動圖演示

 
 

 

④ 代碼實現

    /** * 歸並排序 * * @param array * @return */ public static int[] MergeSort(int[] array) { if (array.length < 2) return array; int mid = array.length / 2; int[] left = Arrays.copyOfRange(array, 0, mid); int[] right = Arrays.copyOfRange(array, mid, array.length); return merge(MergeSort(left), MergeSort(right)); } /** * 歸並排序——將兩段有序數組結合成一個有序數組 * * @param left * @param right * @return */ public static int[] merge(int[] left, int[] right) { int[] result = new int[left.length + right.length]; int i = 0,j = 0,k = 0; while (i < left.length && j < right.length) { if (left[i] <= right[j]) { result[k++] = left[i++]; } else { result[k++] = right[j++]; } } while (i < left.length) { result[k++] = left[i++]; } while (j < right.length) { result[k++] = right[j++]; } return result; } 

⑤ 時間復雜度
歸並排序平均時間復雜度為O(nlogn),最好時間復雜度為O(nlogn),最壞時間復雜度為O(nlogn)。
歸並排序的形式就是一棵二叉樹,它需要遍歷的次數就是二叉樹的深度,而根據完全二叉樹的可以得出它在任何情況下時間復雜度均是O(nlogn)。

⑥ 空間復雜度
歸並排序空間復雜度為O(n)

⑦ 穩定性
歸並排序是穩定的。

⑧ 算法應用
歸並排序可以用於求解逆序對數量問題,具體見:劍指offer - 數組中的逆序對

解法如下:

import java.util.*; public class Solution { private static final int MOD = 1000000007; private int cnt = 0; //遞歸調用 private int[] MergeSort(int[] array) { if (array.length < 2) return array; int mid = array.length / 2; int[] left = Arrays.copyOfRange(array, 0, mid); int[] right = Arrays.copyOfRange(array, mid, array.length); return merge(MergeSort(left), MergeSort(right)); } /** * 將兩段有序數組結合成一個有序數組 * * @param left * @param right * @return */ private int[] merge(int[] left, int[] right) { int[] result = new int[left.length + right.length]; int i = 0,j = 0,k = 0; while (i < left.length && j < right.length) { if (left[i] <= right[j]) { result[k++] = left[i++]; } else { result[k++] = right[j++]; /*歸並同時統計逆序對數量,因為歸並的兩個子序列都已有序,故當left[i] > right[j],有left[i...left.length - 1]均大於right[j]*/ this.cnt = (this.cnt % MOD + (left.length - i) % MOD) % MOD; } } while (i < left.length) { result[k++] = left[i++]; } while (j < right.length) { result[k++] = right[j++]; } return result; } public int InversePairs(int [] array) { MergeSort(array); return cnt % MOD; } } 

6、快速排序(Quick Sort)

① 基本思想
快速排序的基本思想:通過一趟排序將待排記錄分隔成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序。

② 算法描述
快速排序使用分治法來把一個數列分為兩個子數列。具體算法描述如下:
1)從數列中挑出一個元素,稱為 “基准”(pivot);
2)重新排序數列,所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的數可以到任一邊),該基准就處於數列的中間位置。這稱為分區(partition)操作;
3)遞歸地(recursive)對小於基准值元素的子數列和大於基准值元素的子數列進行快速排序。

③ 動圖演示

 
 

 

④ 代碼實現
快速排序最核心的步驟就是partition操作,即從待排序的數列中選出一個數作為基准,將所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的數可以到任一邊),該基准就處於數列的中間位置。partition函數返回基准的位置,然后就可以對基准位置的左右子序列遞歸地進行同樣的快排操作,從而使整個序列有序。

下面我們來介紹partition操作的兩種實現方法:左右指針法 和 挖坑法

方法一:左右指針法
基本思路:
1.將數組的最后一個數 right 作為基准數 key。
2.分區過程:從數組的首元素 begin 開始向后找比 key 大的數(begin 找大);end 開始向前找比 key 小的數(end 找小);找到后交換兩者(swap),直到 begin >= end 終止遍歷。最后將 begin(此時begin == end)和最后一個數交換( 這個時候 end 不是最后一個位置),即 key 作為中間數(左區間都是比key小的數,右區間都是比key大的數)
3.再對左右區間重復第二步,直到各區間只有一個數。

 
 
/** * partition操作 * @param array * @param left 數列左邊界 * @param right 數列右邊界 * @return */ public static int partition(int[] array,int left,int right) { int begin = left; int end = right; int key = right; while( begin < end ) { //begin找大 while(begin < end && array[begin] <= array[key]) begin++; //end找小 while(begin < end && array[end] >= array[key]) end--; swap(array,begin,end); } swap(array,begin,right); return begin; //返回基准位置 } /** * 交換數組內兩個元素 * @param array * @param i * @param j */ public static void swap(int[] array, int i, int j) { int temp = array[i]; array[i] = array[j]; array[j] = temp; } 

方法二:挖坑法
基本思路:
1.定義兩個指針 left 指向起始位置,right 指向最后一個元素的位置,然后指定一個基准 key(right),作為坑。
2.left 尋找比基准(key)大的數字,找到后將 left 的數據賦給 right,left 成為一個坑,然后 right 尋找比基數(key)小的數字,找到將 right 的數據賦給 left,right 成為一個新坑,循環這個過程,直到 begin 指針與 end指針相遇,然后將 key 填入那個坑(最終:key的左邊都是比key小的數,key的右邊都是比key大的數),然后進行遞歸操作。

 
 
/** * partition操作 * @param array * @param left 數列左邊界 * @param right 數列右邊界 * @return */ public static int partition(int[] array,int left,int right) { int key = array[right];//初始坑 while(left < right) { //left找大 while(left < right && array[left] <= key ) left++; array[right] = array[left];//賦值,然后left作為新坑 //right找小 while(left <right && array[right] >= key) right--; array[left] = array[right];//right作為新坑 } array[left] = key; /*將key賦值給left和right的相遇點, 保持key的左邊都是比key小的數,key的右邊都是比key大的數*/ return left;//最終返回基准 } 

實現了partition操作,我們就可以遞歸地進行快速排序了

 /** * 快速排序方法 * @param array * @param left 數列左邊界 * @param right 數列右邊界 * @return */ public static void Quicksort(int array[], int left, int right) { if(left < right){ int pos = partition(array, left, right); Quicksort(array, left, pos - 1); Quicksort(array, pos + 1, right); } } 

⑤ 代碼優化
我們之前選擇基准的策略都是固定基准,即固定地選擇序列的右邊界值作為基准,但如果在待排序列幾乎有序的情況下,選擇的固定基准將是序列的最大(小)值,快排的性能不好(因為每趟排序后,左右兩個子序列規模相差懸殊,大的那部分最后時間復雜度很可能會達到O(n2))。

下面提供幾種常用的快排優化:
優化一:隨機基准
每次隨機選取基准值,而不是固定選取左或右邊界值。將隨機選取的基准值和右邊界值進行交換,然后就回到了之前的解法。
只需要在 partition 函數前增加如下操作即可:

int random = (int) (left + Math.random() * (right - left + 1)); //隨機選擇 left ~ right 之間的一個位置作為基准 swap(array, random, right); //把基准值交換到右邊界 

優化二:三數取中法
基本思想:
取第一個數,最后一個數,第(N/2)個數即中間數,三個數中數值中間的那個數作為基准值。
舉個例子,對於int[] array = { 2,5,4,9,3,6,8,7,1,0},2、3、0分別是第一個數,第(N/2)個是數以及最后一個數,三個數中3最大,0最小,2在中間,所以取2為基准值。
實現getMid函數即可:

/** * 三數取中,返回array[left]、array[mid]、array[right]三者的中間者下標作為基准 * @param array * @param left * @param right * @return */ public static int getMid(int[] array,int left,int right) { int mid = left + ((right - left) >> 1); int a = array[left]; int b = array[mid]; int c = array[right]; if ((b <= a && a <= c) || (c <= a && a <= b)) { //a為中間值 return left; } if ((a <= b && b <= c) || (c <= b && b <= a)) { //b為中間值 return mid; } if ((a <= c && c <= b) || (b <= c && c <= a)) { //c為中間值 return right; } return left; } 

優化三:當待排序序列的長度分割到一定大小后,使用插入排序
在子序列比較小的時候,直接插入排序性能較好,因為對於有序的序列,插排可以達到O(n)的復雜度,如果序列比較小,使用插排效率要比快排高。
實現方式也很簡單,快排是在子序列元素個數為 1 時才停止遞歸,我們可以設置一個閾值n,假設為5,則大於5個元素,子序列繼續遞歸,否則選用插排。
此時QuickSort()函數如下:

public static void Quicksort(int array[], int left, int right) { if(right - left > 5){ int pos = partition(array, left, right); Quicksort(array, left, pos - 1); Quicksort(array, pos + 1, right); }else{ insertionSort(array); } } 

這種優化非常實用。
實測發現當待排序列為 [100000,99999,99998,...,3,2,1] 時,不加插入優化的快排由於遞歸次數過多甚至拋出了 java.lang.StackOverflowError!

 
 

 

而加入了插入優化並選擇閾值為 12500 時,排序用時如下:

 

 
 

實驗發現閾值的選擇也很關鍵,選擇閾值為 5 ,排序用時如下:

 

 
 

優化四:三路划分
如果待排序列中重復元素過多,也會大大影響排序的性能,這是因為大量相同元素參與快排時,左右序列規模相差極大,快排將退化為冒泡排序,時間復雜度接近O(n2)。這時候,如果采用三路划分,則會很好的避免這個問題。
三路划分的思想是利用 partition 函數將待排序列划分為三部分:第一部分小於基准v,第二部分等於基准v,第三部分大於基准v。這樣在遞歸排序區間的時候,我們就不必再對第二部分元素均相等的區間進行快排了,這在待排序列存在大量相同元素的情況下能大大提高快排效率。

來看下面的三路划分示意圖:

 

 
 

說明:紅色部分為小於基准v的序列,綠色部分為等於基准v的序列,白色部分由於還未被 cur 指針遍歷到,屬於大小未知的部分,藍色部分為大於基准v的序列。
left 指針為整個待排區間的左邊界,right 指針為整個待排區間的右邊界。less 指針指向紅色部分的最后一個數(即小於v的最右位置),more 指針指向藍色部分的第一個數(即大於v的最左位置)。cur 指針指向白色部分(未知部分)的第一個數,即下一個要判斷大小的位置。

算法思路:
1)由於最初紅色和藍色區域沒有元素,初始化 less = left - 1,more = right + 1,cur = left。整個區間為未知部分(白色)。
2)如果當前 array[cur] < v,則 swap(array,++less,cur++),即把紅色區域向右擴大一格(less指針后移),把 array[cur] 交換到該位置,cur 指針前移判斷下一個數。
3)如果當前 array[cur] = v,則不必交換,直接 cur++
4)如果當前 array[cur] > v,則 swap(array,--more,cur),即把藍色區域向左擴大一格(more指針前移),把 array[cur] 交換到該位置。特別注意!此時cur指針不能前移,這是因為交換到cur位置的元素來自未知區域,還需要進一步判斷array[cur]。

利用三路划分,我們就可以遞歸地進行三路快排了!並且可以愉快地避開所有重復元素區間。

代碼實現:

public static int[] partition(int[] array,int left,int right){ int v = array[right]; //選擇右邊界為基准 int less = left - 1; // < v 部分的最后一個數 int more = right + 1; // > v 部分的第一個數 int cur = left; while(cur < more){ if(array[cur] < v){ swap(array,++less,cur++); }else if(array[cur] > v){ swap(array,--more,cur); }else{ cur++; } } return new int[]{less + 1,more - 1}; //返回的是 = v 區域的左右下標 } public static void Quicksort(int array[], int left, int right) { if (left < right) { int[] p = partition(array,left,right); Quicksort(array,left,p[0] - 1); //避開重復元素區間 Quicksort(array,p[1] + 1,right); } } 

三路划分可以解決經典的荷蘭國旗問題,具體見 leetcode 75

解法如下:

class Solution { // 方法一:使用計數排序解決,但需要兩趟掃描,不符合要求 /*public void sortColors(int[] nums) { int[] count = new int[3]; for(int i = 0; i < nums.length; i++) count[nums[i]]++; int k = 0; for(int i = 0; i < 3; i++){ for(int j = 0; j < count[i]; j++){ nums[k++] = i; } } }*/ // 方法二:使用快速排序的三路划分,時間復雜度為O(n),空間復雜度為O(1) public void sortColors(int[] nums) { int len = nums.length; if(len == 0) return; int less = -1; int more = len; int cur = 0; while(cur < more){ if(nums[cur] == 0){ swap(nums,++less,cur++); }else if(nums[cur] == 2){ swap(nums,--more,cur); }else{ cur++; } } } public static void swap(int[] array,int i,int j){ int temp = array[i]; array[i] = array[j]; array[j] = temp; } } 

⑥ 時間復雜度
快速排序平均時間復雜度為O(nlogn),最好時間復雜度為O(nlogn),最壞時間復雜度為O(n2)。
最好情況:基准選擇得當,partition函數每次恰好能均分序列,其遞歸樹的深度就為logn,時間復雜度為O(nlogn)。
最壞情況:選擇了最大或者最小數字作為基准,每次划分只能將序列分為一個元素與其他元素兩部分,此時快速排序退化為冒泡排序,如果用樹畫出來,得到的將會是一棵單斜樹,即所有的結點只有左(右)結點的樹,樹的深度為 n,時間復雜度為O(n2)。

⑦ 空間復雜度
快速排序的空間復雜度主要考慮遞歸時使用的棧空間。
在最好情況下,即partition函數每次恰好能均分序列,空間復雜度為O(logn);在最壞情況下,即退化為冒泡排序,空間復雜度為O(n)。平均空間復雜度為O(logn)。

⑧ 穩定性
快速排序是不穩定的。

⑨ 算法拓展
快速選擇算法
快速選擇算法用於求解 Kth Element 問題(無序數組第K大元素),使用快速排序的 partition() 進行實現。
快速排序的 partition() 方法會返回一個整數 j 使得 a[left..j-1] 小於等於 a[j],且 a[j+1..right] 大於等於 a[j]。
此時 a[j] 就是數組的第 j 小的元素,我們可以轉換一下題意,第 k 大的元素就是第 nums.size() - k 小的元素。
找到 Kth Element 之后,再遍歷一次數組,所有大於等於 Kth Element 的元素都是 TopK Elements。
時間復雜度 O(N),空間復雜度 O(1)。

還可以使用小根堆求解此問題,時間復雜度 O(NlogK),空間復雜度 O(K)。具體見:leetcode 215

解法如下:

class Solution { private: int partition(vector<int>& array,int left,int right) { int key = array[right]; //初始坑 while(left < right) { //left找大 while(left < right && array[left] <= key ) left++; array[right] = array[left]; //賦值,然后left作為新坑 //right找小 while(left <right && array[right] >= key) right--; array[left] = array[right]; //right作為新坑 } array[left] = key; /*將key賦值給left和right的相遇點, 保持key的左邊都是比key小的數,key的右邊都是比key大的數*/ return left;//最終返回基准 } public: /*方法1:堆。用於求解 TopK Elements 問題,通過維護一個大小為 K 的小根堆, 堆中的元素就是TopK Elements。堆頂元素就是 Kth Element。如果是第K小的元素 就建立大根堆。時間復雜度 O(NlogK),空間復雜度 O(K)。*/ /* int findKthLargest(vector<int>& nums, int k) { int n = nums.size(); priority_queue<int,vector<int>,greater<int>> q; for(int i = 0;i < n;i++){ q.push(nums[i]); if(q.size() > k) q.pop(); } return q.top(); }*/ /*方法2:快速選擇。用於求解 Kth Element 問題,使用快速排序的 partition() 進行實現。 快速排序的 partition() 方法會返回一個整數 j 使得 a[left..j-1] 小於等於 a[j], 且 a[j+1..right] 大於等於 a[j],此時 a[j] 就是數組的第 j 小的元素, 我們可以轉換一下題意,第 k 大的元素就是第 nums.size() - k 小的元素。 找到 Kth Element 之后,再遍歷一次數組,所有大於等於 Kth Element 的元素都是 TopK Elements。時間復雜度 O(N),空間復雜度 O(1)*/ int findKthLargest(vector<int>& nums, int k) { k = nums.size() - k; int left = 0, right = nums.size() - 1; while (left < right) { int j = partition(nums, left, right); if (j == k) { //選擇的基准等於目標,跳出循環 break; } else if (j < k) { //選擇的基准小於目標,在右側子序列中繼續選擇 left = j + 1; } else { //選擇的基准大於目標,在左側子序列中繼續選擇 right = j - 1; } } return nums[k]; } }; 

拓展:Arrays.sort() 和 Collections.sort() 原理,Collections.sort() 底層調用的是 Arrays.sort()。Arrays.sort() 原理見 剖析JDK8中Arrays.sort底層原理及其排序算法的選擇

7、堆排序(Heap Sort)

① 基本思想
堆排序是一種樹形選擇排序方法,它利用了這種數據結構。在排序的過程中,將array[0,...,n-1]看成是一顆完全二叉樹的順序存儲結構,利用完全二叉樹中雙親結點和孩子結點之間的關系,在當前無序區中選擇關鍵字最大(最小)的元素。

② 概念
堆:堆是一種完全二叉樹,且滿足所有父節點的值均大於等於(或小於等於)其子節點的值。
大根堆(最大堆):滿足所有父節點的值均大於等於其子節點的值的堆稱為大根堆,堆頂元素是堆中元素的最大值。
小根堆(最小堆):滿足所有父節點的值均小於等於其子節點的值的堆稱為小根堆,堆頂元素是堆中元素的最小值。
堆的順序存儲結構:使用順序數據結構(數組)存儲堆,表示方法為:
1.數組按層序遍歷的順序存放完全二叉樹的結點,下標為 0 處為堆頂,下標為 len - 1 處為堆尾。
2.結點 i 如果存在左孩子(下標不超過 len - 1 就存在),左孩子的下標為(2 * i + 1);如果存在右孩子,右孩子的下標為(2 * i + 2)。結點 i 的父結點下標為 (i - 1) / 2 (下標為 0 的結點除外,它沒有父結點)。最后一個非葉子結點即為堆尾元素的父結點,下標為 (len - 1 - 1) / 2 = (len - 2) / 2。

③ 算法描述
1)將初始待排序關鍵字序列(R1,R2….Rn)構建成大頂堆,此堆為初始的無序區;
2)將堆頂元素R[1]與最后一個元素R[n]交換,此時得到新的無序區(R1,R2,……Rn-1)和新的有序區(Rn),且滿足R[1,2…n-1]<=R[n];
3)由於交換后新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1,R2,……Rn-1)調整為新堆,然后再次將R[1]與無序區最后一個元素交換,得到新的無序區(R1,R2….Rn-2)和新的有序區(Rn-1,Rn)。不斷重復此過程直到有序區的元素個數為(n-1),則整個排序過程完成。

④ 動圖演示

 
 

 

⑤ 代碼實現

//聲明全局變量,用於記錄數組array的長度; static int len; /** * 堆排序算法 * @param array * @return */ public static int[] HeapSort(int[] array) { len = array.length; if (len == 0) return array; //1.構建一個大根堆 buildMaxHeap(array); //2.循環將堆頂(最大值)與堆尾交換,刪除堆尾元素,然后重新調整大根堆 while (len > 0) { swap(array, 0, len - 1); len--; //原先的堆尾進入有序區,刪除堆尾元素 adjustHeap(array, 0); //重新調整大根堆 } return array; } /** * 自頂向下調整以 i 為根的堆為大根堆 * @param array * @param i */ public static void adjustHeap(int[] array, int i) { int maxIndex = i; //如果有左子樹,且左子樹大於父節點,則將最大指針指向左子樹 if (2 * i + 1 < len && array[2 * i + 1] > array[maxIndex]) maxIndex = 2 * i + 1; //如果有右子樹,且右子樹大於父節點,則將最大指針指向右子樹 if (2 * i + 2 < len && array[2 * i + 2] > array[maxIndex]) maxIndex = 2 * i + 2; //如果父節點不是最大值,則將父節點與最大值交換,並且遞歸調整與父節點交換的位置。 if (maxIndex != i) { swap(array, maxIndex, i); adjustHeap(array, maxIndex); } } /** * 自底向上構建初始大根堆 * @param array */ public static void buildMaxHeap(int[] array) { //從最后一個非葉子節點開始自底向上構造大根堆 for (int i = (len - 2) / 2; i >= 0; i--) { adjustHeap(array, i); } } 

拓展:
1)插入元素:只需要把待插入的元素放置在堆尾,然后 len++ 把其納入堆,然后調用 adjustHeap 函數重新調整堆即可。
2)刪除堆頂元素:只需要把堆頂元素交換到堆尾,然后 len-- 把其移出堆,然后調用 adjustHeap 函數重新調整堆即可。

⑥ 時間復雜度
堆排序平均時間復雜度為O(nlogn),最好時間復雜度為O(nlogn),最壞時間復雜度為O(nlogn)。
堆排序的形式就是一棵二叉樹,它需要遍歷的次數就是二叉樹的深度,而根據完全二叉樹的可以得出它在任何情況下時間復雜度均是O(nlogn)。

⑦ 空間復雜度
堆排序使用了常數空間,空間復雜度為O(1)。

⑧ 穩定性
堆排序是不穩定的。

8、計數排序(Counting Sort)

① 基本思想
計數排序不是基於比較的排序算法,其核心在於將輸入的數據值轉化為鍵存儲在額外開辟的數組空間中。 作為一種線性時間復雜度的排序,計數排序要求輸入的數據必須是有確定范圍的整數。

② 算法描述
1)找出待排序的數組中最大和最小的元素;
2)統計數組中每個值為 i 的元素出現的次數,存入數組C的第i項;
3)對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加);
4)反向填充目標數組:將每個元素 i 放在新數組的第C(i)項,每放一個元素就將C(i)減去1。

③ 動圖演示

 
 

 

④ 代碼實現

/** * 計數排序 * * @param array * @return */ public static int[] CountingSort(int[] array) { if (array.length == 0) return array; int bias, min = Integer.MAX_VALUE, max = Integer.MIN_VALUE; for (int i = 0; i < array.length; i++) { max = Math.max(max, array[i]); min = Math.min(min, array[i]); } //計算偏移量,將 min ~ max 映射到 bucket 數組的 0 ~ (max - min) 位置上 bias = -min; int[] bucket = new int[max - min + 1]; Arrays.fill(bucket, 0); for (int i = 0; i < array.length; i++) { bucket[array[i] + bias]++; } int index = 0, i = 0; while (index < array.length) { if (bucket[i] != 0) { array[index] = i - bias; bucket[i]--; index++; } else i++; } return array; } 

⑤ 時間復雜度
計數排序平均時間復雜度為O(n + k),最好時間復雜度為O(n + k),最壞時間復雜度為O(n + k)。n 為遍歷一趟數組計數過程的復雜度,k 為遍歷一趟桶取出元素過程的復雜度。

⑥ 空間復雜度
計數排序空間復雜度為O(k),k為桶數組的長度。

⑦ 穩定性
計數排序是穩定的。

9、桶排序(Bucket Sort)

① 基本思想
桶排序與計數排序很相似,不過現在的桶不單計數,是實實在在地放入元素。按照映射函數將數據分配到不同的桶里,每個桶內元素再分別排序(可能使用別的排序算法),最后拼接各個桶中排好序的數據。映射函數人為設計,但要保證桶 i 中的數均小於桶 j (i < j)中的數,即必須桶間必須有序,桶內可以無序,可以考慮按照數的區間范圍划分桶。下面代碼的桶映射函數為:(i - min) / arr.length。

② 算法描述
1)設置一個定量的數組當作空桶;
2)遍歷輸入數據,並且把數據一個一個放到對應的桶里去;
3)對每個不是空的桶的桶內元素進行排序(可以使用直接插入排序等);
4)從不是空的桶里把排好序的數據拼接起來。

③ 動圖演示

 
 

 

④ 代碼實現

public static int[] bucketSort(int[] array){ int max = Integer.MIN_VALUE; int min = Integer.MAX_VALUE; for(int i = 0; i < array.length; i++){ max = Math.max(max, array[i]); min = Math.min(min, array[i]); } /*桶映射函數:自己設計,要保證桶 i 的數均小於桶 j (i < j)的數, 即必須桶間必須有序,桶內可以無序。這里桶映射函數為:(i - min) / arr.length*/ int bucketNum = (max - min) / array.length + 1; ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum); for(int i = 0; i < bucketNum; i++){ bucketArr.add(new ArrayList<Integer>()); } //將每個元素放入桶 for(int i = 0; i < array.length; i++){ int num = (array[i] - min) / (array.length); bucketArr.get(num).add(array[i]); } //對每個桶進行排序 for(int i = 0; i < bucketArr.size(); i++){ Collections.sort(bucketArr.get(i)); } int k = 0; for(int i = 0; i < bucketArr.size(); i++){ for(int j = 0;j < bucketArr.get(i).size();j++) { array[k++] = bucketArr.get(i).get(j); } } return array; } 

⑤ 時間復雜度
桶排序平均時間復雜度為O(n + k),最好時間復雜度為O(n + k),最壞時間復雜度為O(n2)。

⑥ 空間復雜度
桶排序空間復雜度為O(n + k)。

⑦ 穩定性
桶排序是穩定的。

10、基數排序(Radix Sort)

① 基本思想
基數排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最后的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。

② 算法描述
1)取得數組中的最大數,並取得位數;
2)array 為原始數組,從最低位開始取每個位組成 radix 數組;

  1. 對 radix 進行計數排序(利用計數排序適用於小范圍數的特點);

③ 動圖演示

 
 

 

④ 代碼實現

    /** * 基數排序 * @param array * @return */ public static int[] RadixSort(int[] array) { if (array == null || array.length < 2) return array; // 1.先算出最大數的位數; int max = Integer.MIN_VALUE; for (int i = 0; i < array.length; i++) { max = Math.max(max, array[i]); } int maxDigit = 0; while (max != 0) { max /= 10; maxDigit++; } int div = 1; ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>(); for (int i = 0; i < 10; i++) bucketList.add(new ArrayList<Integer>()); //2.進行maxDigit趟分配 for (int i = 0; i < maxDigit; i++,div *= 10) { for (int j = 0; j < array.length; j++) { int num = (array[j] / div) % 10; bucketList.get(num).add(array[j]); } //3.收集 int index = 0; for (int j = 0; j < bucketList.size(); j++) { for (int k = 0; k < bucketList.get(j).size(); k++) array[index++] = bucketList.get(j).get(k); bucketList.get(j).clear(); } } return array; } 

⑤ 時間復雜度
基數排序平均時間復雜度為O(n * k),最好時間復雜度為O(n * k),最壞時間復雜度為O(n * k)。

⑥ 空間復雜度
基數排序空間復雜度為O(n + k)。

⑦ 穩定性
基數排序是穩定的。

三、各排序算法應用場景及選擇

1)若 n 較小(如n ≤ 50)時,可采用直接插入或簡單選擇排序。
2)若元素初始狀態基本有序(正序),直接插入、冒泡或快速排序為宜。
3)若 n 較大,則應采用時間復雜度為O(nlogn)的排序方法:快速排序、堆排序或歸並排序。
快速排序是目前基於比較的內部排序中被認為是最好的方法,當待排序的關鍵字是隨機分布時,快速排序的平均時間最短。
堆排序所需的輔助空間少於快速排序,並且不會出現快速排序可能出現的最壞情況。這兩種排序都是不穩定的。
若要求排序穩定,則可選用歸並排序。但本文介紹的從單個記錄起進行兩兩歸並的歸並排序算法並不值得提倡,通常可以將它和直接插入排序結合在一起使用。先利用直接插入排序求得較長的有序數列,然后再兩兩歸並之。因為直接插入排序是穩定的,所以改進后的歸並排序仍是穩定的。
4)當范圍已知,且空間不是很重要的情況下可以考慮使用桶排序。

參考網址見:https://www.cnblogs.com/xkzhangsanx/p/11001174.html

轉載自:https://www.cnblogs.com/lipengsheng-javaweb/p/12516818.html 如有侵權,請聯系本人刪除


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM