01-經典排序演算法
學習資源:慕課網liuyubobobo老師的《演算法與資料結構精解》
目錄
- 0、排序演算法說明
- 1、氣泡排序 O(n2)
- 2、選擇排序 O(n2)
- 3、插入排序 O(n2)
- 4、歸併排序 O(nlogn)
- 5、快速排序 O(nlogn)
- 6、堆排序 O(nlogn)
- 7、排序演算法總結
- 8、工具類SortTestHelper
0、排序演算法說明
0.1、排序的定義
對一序列物件根據某個關鍵字進行排序。排序物件可以是基本資料型別,也可以是複雜資料型別,但是類必須具有可比較性。
0.2、 術語說明
- 穩定:如果a原本在b前面,而a=b,排序之後a仍然在b的前面;
- 不穩定:如果a原本在b的前面,而a=b,排序之後a可能會出現在b的後面;
- 內排序:所有排序操作都在記憶體中完成;
- 外排序:由於資料太大,因此把資料放在磁碟中,而排序通過磁碟和記憶體的資料傳輸才能進行;
- 時間複雜度: 一個演算法執行所耗費的時間。
- 空間複雜度:執行完一個程式所需記憶體的大小。
- n: 資料規模
- k: “桶”的個數
- In-place: 佔用常數記憶體,不佔用額外記憶體
- Out-place: 佔用額外記憶體
0.3、排序演算法分類
1、氣泡排序 O(n2)
1.1、簡介
氣泡排序(Bubble Sort)又稱為泡式排序,是一種簡單的排序演算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個演算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端。
演算法描述:
- 比較相鄰的元素。如果第一個比第二個大,就交換它們兩個;
- 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對,這樣在最後的元素應該會是最大的數;
- 針對所有的元素重複以上的步驟,除了最後一個;
- 重複步驟1~3,直到排序完成。
1.2、程式碼
public class MergeSort{
/**
* Description:氣泡排序
*
* @param array 需要排序的陣列
* @author JourWon
* @date 2019/7/11 9:54
*/
public static void bubbleSort(int[] array) {
if (array == null || array.length <= 1) {
return;
}
int length = array.length;
// 外層迴圈控制比較輪數i
for (int i = 0; i < length; i++) {
// 內層迴圈控制每一輪比較次數,每進行一輪排序都會找出一個較大值
// (array.length - 1)防止索引越界,(array.length - 1 - i)減少比較次數
for (int j = 0; j < length - 1 - i; j++) {
// 前面的數大於後面的數就進行交換
if (array[j] > array[j + 1]) {
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
}
}
}
}
1.3、複雜度分析
最佳情況:T(n) = O(n) 最差情況:T(n) = O(n2) 平均情況:T(n) = O(n2)
2、選擇排序 O(n2)
2.1、簡介
選擇排序(Selection sort)是一種簡單直觀的排序演算法。它的工作原理如下:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序畢。
2.2、程式碼
public class SelectionSort {
public static void sort(Comparable[] arr){
int n = arr.length;
for(int i = 0; i < arr.length; i++){
int minIndex = i;
for(int j = i+1; j < n; j++)
if(less(arr[j], arr[minIndex]))
minIndex = j;
swap(arr, i, minIndex);
}
}
private static boolean less(Comparable a, Comparable b){
return a.compareTo(b) < 0;
}
private static void swap(Comparable[] arr, int a, int b){
Comparable temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
2.3、測試
public static void main(String[] args) {
// 測試Integer
int n = 10000;
Integer[] arr = SortTestHelper.generateRandomArray(n, 0, 10000);
SelectionSort.sort(arr);
SortTestHelper.printArray(arr);
System.out.println();
SortTestHelper.testSort("Ⅰ_sorting_basic.Ⅰ_selection_sort.SelectionSort", arr);
// 測試自定義型別,類需要實現Comparable介面
}
2.4、複雜度分析
選擇排序的交換操作介於 0 和 (n - 1) 次之間。選擇排序的比較操作為 n(n - 1) / 2 次。選擇排序的賦值操作介於 0 與 3(n - 1) 次之間。所以總體上來說,時間複雜度為 O(n2) 。
比較次數 O(n2),比較次數與關鍵字的初始狀態無關,總的比較次數 N = (n -1) + (n - 2) + ... + 1。
交換次數 O(n),最好情況是,已經有序,交換0次;最壞情況是,逆序,交換 n - 1 次。交換次數比氣泡排序較少,由於交換所需CPU時間比比較所需的CPU時間多,n 值較小時,選擇排序比氣泡排序快。
原地操作幾乎是選擇排序的唯一優點,當空間複雜度要求較高時,可以考慮選擇排序;實際適用的場合非常罕見。
複雜度 | |
---|---|
平均時間複雜度 | О(n²) |
最壞時間複雜度 | О(n²) |
最優時間複雜度 | О(n²) |
空間複雜度 | 總共О(n),需要輔助空間O(1) |
最佳解 | 偶爾出現 |
3、插入排序 O(n2)
3.1、簡介
插入排序(Insertion Sort)是一種簡單直觀的排序演算法。它的工作原理是通過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,通常採用in-place排序(即只需用到 O(1) 的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。
一般來說,插入排序都採用 in-place 在陣列上實現。具體演算法描述如下:
- 從第一個元素開始,該元素可以認為已經被排序
- 取出下一個元素(新元素),在已經排序的元素序列中從後向前掃描
- 如果掃描到的元素(已排序)大於新元素,將掃描到的元素移到下一位置
- 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
- 將新元素插入到該位置後
- 重複步驟2~5
(由Swfung8 - 自己的作品,CC BY-SA 3.0,連結)
3.2、程式碼
public class InsertionSort{
// 演算法類不允許產生任何例項
private InsertionSort(){}
public static void sort(Comparable[] arr){
int n = arr.length;
for (int i = 1; i < n; i++) {
// 尋找元素arr[i]合適的插入位置
// 寫法1
// for( int j = i ; j > 0 ; j -- )
// if( arr[j].compareTo( arr[j-1] ) < 0 )
// swap( arr, j , j-1 );
// else
// break;
// 寫法2
for( int j = i; j > 0 && more(arr[j-1], arr[j]); j--)
swap(arr, j, j-1);
}
}
private static boolean more(Comparable a, Comparable b){
return a.compareTo(b) > 0;
}
private static void swap(Comparable[] arr, int i, int j) {
Comparable t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
3.3、演算法優化
由於插入排序會有更多的交換操作,所以會導致其效能比選擇排序還要差。
優化思路(減少交換操作):
- 從第一個元素開始,該元素可以認為已經被排序
- 取出下一個元素(新元素),將其提取到一個臨時變數中;定義一個"指標",初始指向新元素,
- 在已經排序的元素序列中從後向前掃描,如果"指標"前的元素大於"指標"元素,將"指標"元素的值更改為前一個元素的值,並且"指標"前移
- 重複步驟3,直到不滿足步驟3,即說明當前"指標"處為新元素的插入位置,將"指標"元素的值更改為新元素的值
public class InsertionSort{
// 演算法類不允許產生任何例項
private InsertionSort(){}
public static void sort(Comparable[] arr){
int n = arr.length;
for (int i = 1; i < n; i++) {
// 尋找元素arr[i]合適的插入位置
Comparable e = arr[i];
// j 即是"指標"
int j = i;
for( ; j > 0 && more(arr[j-1], e); j--)
arr[j] = arr[j-1];
arr[j] = e;
}
}
private static boolean more(Comparable a, Comparable b){
return a.compareTo(b) > 0;
}
}
3.4、測試
// 測試InsertionSort
public static void main(String[] args) {
int N = 10000;
Integer[] arr = SortTestHelper.generateRandomArray(N, 0, 100000);
SortTestHelper.printArray(arr);
InsertionSort.sort(arr);
SortTestHelper.printArray(arr);
SortTestHelper.testSort("Ⅰ_sorting_basic.Ⅱ_insertion_sort.InsertionSort", arr);
}
3.5、複雜度分析
如果目標是把n個元素的序列升序排列,那麼採用插入排序存在最好情況和最壞情況。最好情況就是,序列已經是升序排列了,在這種情況下,需要進行的比較操作需 n-1 次即可。最壞情況就是,序列是降序排列,那麼此時需要進行的比較共有 1/2 n(n - 1) 次。插入排序的賦值操作是比較操作的次數減去 n-1 次,(因為 n-1 次迴圈中,每一次迴圈的比較都比賦值多一個,多在最後那一次比較並不帶來賦值)。平均來說插入排序演算法複雜度為 O(n2) 。
因而,插入排序不適合對於資料量比較大的排序應用。但是,如果需要排序的資料量很小,例如,量級小於千;或者若已知輸入元素大致上按照順序排列,那麼插入排序還是一個不錯的選擇。 插入排序在工業級庫中也有著廣泛的應用,在STL的sort演算法和stdlib的qsort演算法中,都將插入排序作為快速排序的補充,用於少量元素的排序(通常為8個或以下)。
複雜度 | |
---|---|
平均時間複雜度 | О(n²) |
最壞時間複雜度 | О(n²) |
最優時間複雜度 | О(n²) |
空間複雜度 | 總共О(n),需要輔助空間O(1) |
最佳解 | No |
4、歸併排序 O(nlogn)
4.1、簡介
歸併排序(英語:Merge sort,或mergesort),是建立在歸併操作上的一種有效的排序演算法,效率為 O(n logn)。1945年由約翰·馮·諾伊曼首次提出。該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用,且各層分治遞迴可以同時進行。
由Swfung8 - 自己的作品,CC BY-SA 3.0,連結
歸併操作(merge),也叫歸併演算法,指的是將兩個已經排序的序列合併成一個序列的操作。歸併排序演算法依賴歸併操作。
4.2、遞迴法(Top-down)
採用分治法:
- 分割:遞迴地把當前序列平均分割成兩半。
- 整合:在保持元素順序的同時將上一步得到的子序列整合到一起(歸併)。
- 申請空間,使其大小為兩個已經排序序列之和(最後一層的2個序列皆只有一個元素,即是排好序的),該空間儲存的是兩個已排序序列的值,用於合併後序列值的更新
- 設定3個指標 i 、j 、k ,其中 i ,j 最初位置分別為兩個已經排序序列的起始位置,k 指向合併序列中元素待放入的位置
- 比較 i 、j 所指向的元素,選擇相對小的元素放入到合併空間,並移動 i 或 j 到下一位置;同時 k 也要移動到下一位置
- 重複步驟3直到某一指標到達序列尾
- 將另一序列剩下的所有元素直接複製到合併序列尾
4.3、程式碼
public class MergeSort{
// 演算法類不允許產生任何例項
private MergeSort(){}
// 將arr[l...mid]和arr[mid+1...r]兩部分進行歸併
private static void merge(Comparable[] arr, int l, int mid, int r) {
// 臨時空間
Comparable[] aux = Arrays.copyOfRange(arr, l, r+1);
// 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
int i = l, j = mid+1;
for( int k = l ; k <= r; k ++ ){
if( i > mid ){ // 如果左半部分元素已經全部處理完畢
arr[k] = aux[j-l]; j ++;
}
else if( j > r ){ // 如果右半部分元素已經全部處理完畢
arr[k] = aux[i-l]; i ++;
}
else if( aux[i-l].compareTo(aux[j-l]) < 0 ){ // 左半部分所指元素 < 右半部分所指元素
arr[k] = aux[i-l]; i ++;
}
else{ // 左半部分所指元素 >= 右半部分所指元素
arr[k] = aux[j-l]; j ++;
}
}
}
// 遞迴使用歸併排序,對arr[l...r]的範圍進行排序
private static void sort(Comparable[] arr, int l, int r) {
// 遞迴終止條件
if (l >= r)
return;
// int mid = l + (r-l) / 2;
int mid = (l+r)/ 2;
sort(arr, l, mid);
sort(arr, mid + 1, r);
// 演算法優化:如果兩個序列已經有序,則不需要進行歸併
if(arr[mid].compareTo(arr[mid+1]) > 0)
merge(arr, l, mid, r);
}
public static void sort(Comparable[] arr){
int n = arr.length;
sort(arr, 0, n-1);
}
}
4.4、迭代法(Bottom-up)
原理如下(假設序列共有 n 個元素):
- 將序列每相鄰兩個數字進行歸併操作,形成 ceil(n/2) 個序列,排序後每個序列包含 兩/一 個元素
- 若此時序列數不是1個則將上述序列再次歸併,形成 ceil(n/4) 個序列,每個序列包含 四/三 個元素
- 重複步驟2,直到所有元素排序完畢,即序列數為1
4.5、程式碼
public class MergeSortBU{
// 我們的演算法類不允許產生任何例項
private MergeSortBU(){}
// 將arr[l...mid]和arr[mid+1...r]兩部分進行歸併
private static void merge(Comparable[] arr, int l, int mid, int r) {
Comparable[] aux = Arrays.copyOfRange(arr, l, r+1);
// 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
int i = l, j = mid+1;
for( int k = l ; k <= r; k ++ ){
if( i > mid ){ // 如果左半部分元素已經全部處理完畢
arr[k] = aux[j-l]; j ++;
}
else if( j > r ){ // 如果右半部分元素已經全部處理完畢
arr[k] = aux[i-l]; i ++;
}
else if( aux[i-l].compareTo(aux[j-l]) < 0 ){ // 左半部分所指元素 < 右半部分所指元素
arr[k] = aux[i-l]; i ++;
}
else{ // 左半部分所指元素 >= 右半部分所指元素
arr[k] = aux[j-l]; j ++;
}
}
}
public static void sort(Comparable[] arr){
int n = arr.length;
// Merge Sort Bottom Up 無優化版本
for (int sz = 1; sz < n; sz *= 2)
for (int i = 0; i < n - sz; i += sz+sz)
// 對 arr[i...i+sz-1] 和 arr[i+sz...i+2*sz-1] 進行歸併
merge(arr, i, i+sz-1, Math.min(i+sz+sz-1,n-1));
// Merge Sort Bottom Up 優化
// 對於規模較小的陣列, 使用插入排序優化
// for( int i = 0 ; i < n ; i += 16 )
// InsertionSort.sort(arr, i, Math.min(i+15, n-1) );
//
// for( int sz = 16; sz < n ; sz += sz )
// for( int i = 0 ; i < n - sz ; i += sz+sz )
// // 對於arr[mid] <= arr[mid+1]的情況,不進行merge
// if( arr[i+sz-1].compareTo(arr[i+sz]) > 0 )
// merge(arr, i, i+sz-1, Math.min(i+sz+sz-1,n-1) );
}
}
4.6、複雜度分析
比較操作的次數介於 (n log n)/2 和 n log n - n + 1 。 賦值操作的次數是 2n log n。歸併演算法的空間複雜度為:O(n)
複雜度 | |
---|---|
平均時間複雜度 | O(n logn) |
最壞時間複雜度 | O(n logn) |
最優時間複雜度 | O(n logn) |
空間複雜度 | O(n) |
最佳解 | 有時是 |
5、快速排序 O(nlogn)
5.1、簡介
快速排序(英語:Quicksort),又稱分割槽交換排序(partition-exchange sort),簡稱快排,一種排序演算法,最早由東尼·霍爾提出。在平均狀況下,排序 n 個專案要 O(nlog n) 次比較。在最壞狀況下則需要 O(n2) 次比較,但這種狀況並不常見。事實上,快速排序O(nlog n)通常明顯比其他演算法更快,因為它的內部迴圈(inner loop)可以在大部分的架構上很有效率地達成。
快速排序使用分治法策略來把一個序列分為較小和較大的2個子序列,然後遞迴地排序兩個子序列。
步驟為:
- 挑選基準值:從數列中挑出一個元素(可以選取第一個),稱為“基準”(pivot);,
- 分割:重新排序數列,所有比基準值小的元素擺放在基準前面,所有比基準值大的元素擺在基準後面(與基準值相等的數可以到任何一邊)。在這個分割結束之後,對基準值的排序就已經完成,
-
遞迴排序子序列:遞迴地將小於基準值元素的子序列和大於基準值元素的子序列排序。
-
遞迴到最底部的判斷條件是數列的大小是零或一,此時該數列顯然已經有序。
選取基準值有數種具體方法,此選取方法對排序的時間效能有決定性影響。
5.2、程式碼
public class QuickSort {
// 我們的演算法類不允許產生任何例項
private QuickSort(){}
// 對arr[l...r]部分進行partition操作
// 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
private static int partition(Comparable[] arr, int l, int r){
// 選擇當前序列的第一個元素作為基準值
Comparable v = arr[l];
// arr[l+1...j] < v ; arr[j+1...i) > v
// j初始等於l,則arr[l+1...j]不存在
int j = l;
// i初始等於 l+1,arr[j+1...i]也不存在
// 從當前序列的第二個元素遍歷
for( int i = l + 1 ; i <= r ; i ++ ) {
// 當前元素小於基準值,交換當前元素與arr[j+1]。
// 這樣arr[l+1...j]區間開始擴張,遍歷到最後剩餘的arr[j+1...i]即為大於基準值的區間
if (arr[i].compareTo(v) < 0) {
j++;
swap(arr, j, i);
}
}
// 最後交換 v 與 arr[l+1...j]區間的最後一個元素,使得基準值移動到正確的位置
swap(arr, l, j);
return j;
}
// 遞迴使用快速排序,對arr[l...r]的範圍進行排序
private static void sort(Comparable[] arr, int l, int r){
// 遞迴終止條件:序列長度為1或0
if( l >= r )
return;
// 在arr[l...r]中挑取基準值,並根據基準值進行分割
int p = partition(arr, l, r);
// 遞迴進行排序
sort(arr, l, p-1 );
sort(arr, p+1, r);
}
private static void swap(Object[] arr, int i, int j) {
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
public static void sort(Comparable[] arr){
int n = arr.length;
sort(arr, 0, n-1);
}
}
5.3、演算法優化
5.3.1、基準值選取隨機化
如果快速排序處理的序列為近乎有序的,選取當前序列固定位置的元素作為基準值,會導致一次分割操作生成的兩個序列極度不均衡的:
最差的情況,會退化為 O(n2):
public class QuickSort2Ways {
// 我們的演算法類不允許產生任何例項
private QuickSort2Ways(){}
// 雙路快速排序的partition
// 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
private static int partition(Comparable[] arr, int l, int r){
// 隨機在arr[l...r]的範圍中, 選擇一個數值作為標定點pivot
swap( arr, l , (int)(Math.random()*(r-l+1))+l );
Comparable v = arr[l];
// arr[l+1...i) <= v; arr(j...r] >= v
int i = l+1, j = r;
while( true ){
// 注意這裡的邊界, arr[i].compareTo(v) < 0, 不能是arr[i].compareTo(v) <= 0
// 思考一下為什麼?
while( i <= r && arr[i].compareTo(v) < 0 )
i ++;
// 注意這裡的邊界, arr[j].compareTo(v) > 0, 不能是arr[j].compareTo(v) >= 0
// 思考一下為什麼?
while( j >= l+1 && arr[j].compareTo(v) > 0 )
j --;
// 對於上面的兩個邊界的設定, 有的同學在課程的問答區有很好的回答:)
// 大家可以參考: http://coding.imooc.com/learn/questiondetail/4920.html
// 遍歷完成,退出遍歷
if( i > j )
break;
// 交換左邊 >=v 和右邊 <=v 的元素
swap( arr, i, j );
// 交換完成,繼續遍歷
i ++;
j --;
}
// 最後讓基準值處於正確的位置
swap(arr, l, j);
return j;
}
// 遞迴使用快速排序,對arr[l...r]的範圍進行排序
private static void sort(Comparable[] arr, int l, int r){
// 遞迴終止條件:序列長度為1或0
if( l >= r )
return;
int p = partition(arr, l, r);
sort(arr, l, p-1 );
sort(arr, p+1, r);
}
public static void sort(Comparable[] arr){
int n = arr.length;
sort(arr, 0, n-1);
}
private static void swap(Comparable[] arr, int i, int j) {
Comparable t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
5.3.2、雙路快排
如果當前序列含有大量重複的元素,即使基準值的選取實現了隨機化,但是在進行分割後,也有可能形成最差的情況,退化為 O(n2)。
優化思路:
- 挑選基準值 v 之後,設定兩個指標 i , j , i 指向當前遍歷到的元素(初始指向當前序列的第二個元素), j 指向最後一個元素
- i 不斷向後移動,遇到小於 v 的元素,繼續向後移動,遇到大於等於 v 的元素,則停止; j 不斷向前移動,遇到大於 v 的元素,繼續向前移動,遇到小於等於 v 的元素,則停止;此時交換 i 和 j 所指向的元素
- 重複步驟2,直到兩個指標遍歷完序列中的所有元素,即 i 大於 j
注意:此時分割出來兩個序列中均可能含有等於 v 的元素,並不是以 v 為分界點的兩個絕對有序的佇列 - 最後交換基準值 v 和 arr[ j ] ,完成這輪的快速排序
這樣進行遞迴分割,由於基準值 v 是隨機選取的,又因為等於 v 的其他元素被分散到兩個序列,不會造成等於 v 的元素集中分佈在一側,從一定程度上保證了兩個序列是均衡的。
public class QuickSort2Ways {
// 我們的演算法類不允許產生任何例項
private QuickSort2Ways(){}
// 雙路快速排序的partition
// 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
private static int partition(Comparable[] arr, int l, int r){
// 隨機在arr[l...r]的範圍中, 選擇一個數值作為標定點pivot
swap( arr, l , (int)(Math.random()*(r-l+1))+l );
Comparable v = arr[l];
// arr[l+1...i) <= v; arr(j...r] >= v
int i = l+1,
j = r;
while( true ){
while( i <= r && arr[i].compareTo(v) < 0 )
i ++;
while( j >= l+1 && arr[j].compareTo(v) > 0 )
j --;
// 遍歷完成,退出遍歷
if( i > j )
break;
// 交換左邊 >=v 和右邊 <=v 的元素
swap( arr, i, j );
// 交換完成,繼續遍歷
i ++;
j --;
}
// 最後讓基準值處於正確的位置
swap(arr, l, j);
return j;
}
// 遞迴使用快速排序,對arr[l...r]的範圍進行排序
private static void sort(Comparable[] arr, int l, int r){
// 遞迴終止條件:序列長度為1或0
if( l >= r )
return;
int p = partition(arr, l, r);
sort(arr, l, p-1 );
sort(arr, p+1, r);
}
public static void sort(Comparable[] arr){
int n = arr.length;
sort(arr, 0, n-1);
}
private static void swap(Comparable[] arr, int i, int j) {
Comparable t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
5.3.3、三路快排
雙路快排是為了解決序列中含有大量重複的元素,優化思路是將和基準值 v 重複的元素分散到基準值 v 左右兩邊的序列,避免集中。
而我們可以想到一個更高效的方式:將當前區間分割為三個區間:< v,= v,> v
優化思路:
- 挑選基準值 v 之後,設定三個指標 i, lt , gt , i 指向當前遍歷到的元素, lt 指向小於 v的最後一個元素, gt 指向大於 v 的第一個元素(從左看)
- i 不斷向後移動,遇到等於 v 的元素,繼續向後移動;遇到小於 v 的元素,交換 arr[lt] 和 arr[i] , lt 向後移動一個位置;遇到大於 v 的元素,交換 arr[gt] 和 arr[i] ,gt 向前移動一個位置
- 當 i = gt 時,遍歷完成
- 最後交換 arr[l] 和 arr[lt] ,使得基準值處於正確的位置
public class QuickSort3Ways {
// 我們的演算法類不允許產生任何例項
private QuickSort3Ways(){}
// 遞迴使用快速排序,對arr[l...r]的範圍進行排序
private static void sort(Comparable[] arr, int l, int r){
// 遞迴終止條件:序列長度為1或0
if(l > r)
return;
// partition
// 隨機在arr[l...r]的範圍中, 選擇一個數值作為標定點pivot
swap( arr, l, (int)(Math.random()*(r-l+1)) + l );
Comparable v = arr[l];
// 定義三個指標
int lt = l; // arr[l+1...lt] < v
int gt = r + 1; // arr[gt...r] > v
int i = l+1; // arr[lt+1...i) == v
while( i < gt ){
if( arr[i].compareTo(v) < 0 ){
swap( arr, i, lt+1);
i ++;
lt ++;
}
else if( arr[i].compareTo(v) > 0 ){
swap( arr, i, gt-1);
gt --;
}
else{ // arr[i] == v
i ++;
}
}
swap( arr, l, lt );
// 最後基準值 v 歸位後, 三個區間更新為 arr[l+1...lt-1] < v, arr[gt...r] > v, arr[lt...gt-1] == v
// 遞迴
sort(arr, l, lt-1);
sort(arr, gt, r);
}
public static void sort(Comparable[] arr){
int n = arr.length;
sort(arr, 0, n-1);
}
private static void swap(Comparable[] arr, int i, int j) {
Comparable t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
5.4、測試
測試比對歸併排序、雙路快排、三路快排的效能
public class Main {
public static void main(String[] args) {
int N = 1000000;
// 測試1 一般性測試
System.out.println("Test for random array, size = " + N + " , random range [0, " + N + "]");
Integer[] arr1 = SortTestHelper.generateRandomArray(N, 0, N);
Integer[] arr2 = Arrays.copyOf(arr1, arr1.length);
Integer[] arr3 = Arrays.copyOf(arr1, arr1.length);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅰ_merge_sort.MergeSort", arr1);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅳ_quick_sort_2ways.QuickSort2Ways", arr2);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅴ_quick_sort_3ways.QuickSort3Ways", arr3);
System.out.println();
// 測試2 測試近乎有序的陣列
int swapTimes = 100;
assert swapTimes >= 0;
System.out.println("Test for nearly ordered array, size = " + N + " , swap time = " + swapTimes);
arr1 = SortTestHelper.generateNearlyOrderedArray(N, swapTimes);
arr2 = Arrays.copyOf(arr1, arr1.length);
arr3 = Arrays.copyOf(arr1, arr1.length);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅰ_merge_sort.MergeSort", arr1);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅳ_quick_sort_2ways.QuickSort2Ways", arr2);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅴ_quick_sort_3ways.QuickSort3Ways", arr3);
System.out.println();
// 測試3 測試存在包含大量相同元素的陣列
System.out.println("Test for random array, size = " + N + " , random range [0,10]");
arr1 = SortTestHelper.generateRandomArray(N, 0, 10);
arr2 = Arrays.copyOf(arr1, arr1.length);
arr3 = Arrays.copyOf(arr1, arr1.length);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅰ_merge_sort.MergeSort", arr1);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅳ_quick_sort_2ways.QuickSort2Ways", arr2);
SortTestHelper.testSort("Ⅱ_sorting_advance.Ⅴ_quick_sort_3ways.QuickSort3Ways", arr3);
}
}
總結如下:
- 對於包含有大量重複資料的陣列, 三路快排有巨大的優勢
- 對於一般性的隨機陣列和近乎有序的陣列, 三路快排的效率雖然不是最優的, 但是是在非常可以接受的範圍裡。因此, 在一些語言中, 三路快排是預設的語言庫函式中使用的排序演算法,比如Java
6、堆排序 O(nlogn)
6.1、簡介
堆排序(Heapsort)是指利用堆這種資料結構所設計的一種排序演算法。堆是一個近似完全二叉樹的結構,並同時滿足堆的性質:即子節點的鍵值或索引總是不小於(或者不大於)它的父節點。
6.2、最大堆
此處貼出liyubobobo老師這版的最大堆程式碼
// 在堆的有關操作中,需要比較堆中元素的大小,所以Item需要extends Comparable
public class MaxHeap<Item extends Comparable> {
protected Item[] data;
protected int count;
protected int capacity;
// 建構函式, 構造一個空堆, 可容納capacity個元素
public MaxHeap(int capacity){
data = (Item[])new Comparable[capacity+1];
count = 0;
this.capacity = capacity;
}
// 返回堆中的元素個數
public int size(){
return count;
}
// 返回一個布林值, 表示堆中是否為空
public boolean isEmpty(){
return count == 0;
}
// 像最大堆中插入一個新的元素 item
public void insert(Item item){
assert count + 1 <= capacity;
data[count+1] = item;
count ++;
siftUp(count);
}
// 從最大堆中取出堆頂元素, 即堆中所儲存的最大資料
public Item extractMax(){
assert count > 0;
Item ret = data[1];
swap( 1 , count );
count --;
siftDown(1);
return ret;
}
// 獲取最大堆中的堆頂元素
public Item getMax(){
assert( count > 0 );
return data[1];
}
// 交換堆中索引為i和j的兩個元素
private void swap(int i, int j){
Item t = data[i];
data[i] = data[j];
data[j] = t;
}
//********************
//* 最大堆核心輔助函式
//********************
private void siftUp(int k){
while( k > 1 && data[k/2].compareTo(data[k]) < 0 ){
swap(k, k/2);
k /= 2;
}
}
private void siftDown(int k){
while( 2*k <= count ){
int j = 2*k; // 在此輪迴圈中,data[k]和data[j]交換位置
if( j+1 <= count && data[j+1].compareTo(data[j]) > 0 )
j ++;
// data[j] 是 data[2*k]和data[2*k+1]中的最大值
if( data[k].compareTo(data[j]) >= 0 ) break;
swap(k, j);
k = j;
}
}
// 測試 MaxHeap
public static void main(String[] args) {
MaxHeap<Integer> maxHeap = new MaxHeap<Integer>(100);
int N = 100; // 堆中元素個數
int M = 100; // 堆中元素取值範圍[0, M)
for( int i = 0 ; i < N ; i ++ )
maxHeap.insert( new Integer((int)(Math.random() * M)) );
Integer[] arr = new Integer[N];
// 將maxheap中的資料逐漸使用extractMax取出來
// 取出來的順序應該是按照從大到小的順序取出來的
for( int i = 0 ; i < N ; i ++ ){
arr[i] = maxHeap.extractMax();
System.out.print(arr[i] + " ");
}
System.out.println();
// 確保arr陣列是從大到小排列的
for( int i = 1 ; i < N ; i ++ )
assert arr[i-1] >= arr[i];
}
}
也可以使用我之前《資料結構》博文中中實現的最大堆,二者無本質區別。
6.3、程式碼
實現思路:
- 將所有的元素依次新增到堆中
- 再將所有元素從堆中依次取出來,即完成了排序(升序還是降序,由取出時遍歷的方向決定)
無論是建立堆的過程,還是從堆中依次取出元素的過程,時間複雜度均為O(nlogn),整個堆排序的整體時間複雜度為O(nlogn)
public class HeapSort1 {
//演算法類不允許產生任何例項
private HeapSort1(){}
public static void sort(Comparable[] arr){
int n = arr.length;
MaxHeap<Comparable> maxHeap = new MaxHeap<Comparable>(n);
for( int i = 0 ; i < n ; i ++ )
maxHeap.insert(arr[i]);
for( int i = n-1 ; i >= 0 ; i -- )
arr[i] = maxHeap.extractMax();
}
}
上一版的堆排序,是將陣列中的元素逐個的新增進最大堆中,即建立待排序陣列對應的最大堆,複雜度為 O(nlogn) ,這一過程可以優化
6.4、heapify
定義:將任意陣列整理成堆的形狀
實現思路:
-
通過迴圈將陣列中的元素逐個新增進堆物件中。複雜度為O(nlogn)
-
從最後一個非葉子結點(最後一個結點的父結點),從下之上、從右至左(對應到陣列中也就是從後向前),逐個地對每一個元素元素進行Sift Down。複雜度為O(n)
-
直到對根結點完成 Sift Down
定義:將任意陣列整理成堆的形狀
實現思路(這裡選擇思路2):
-
通過迴圈將陣列中的元素逐個新增進堆物件中。複雜度為O(nlogn)
-
從最後一個非葉子結點(最後一個結點的父結點),從下之上、從右至左(對應到陣列中也就是從後向前),逐個地對每一個元素元素進行Sift Down。複雜度為O(n)
-
直到對根結點完成 Sift Down
// 新的建構函式, 通過一個給定陣列建立一個最大堆
// 該構造堆的過程, 時間複雜度為O(n)
public MaxHeap(Item arr[]){
int n = arr.length;
data = (Item[])new Comparable[n+1];
capacity = n;
for( int i = 0 ; i < n ; i ++ )
data[i+1] = arr[i];
count = n;
for( int i = count/2 ; i >= 1 ; i -- )
siftDown(i);
}
6.5、演算法優化
6.5.1、使用heapify建立堆
HeapSort2,藉助我們的 heapify
過程建立堆。
此時, 建立堆的過程時間複雜度為O(n),將所有元素依次從堆中取出來, 實踐複雜度為 O(nlogn) ,堆排序的總體時間複雜度依然是 O(nlogn) , 但是比HeapSort1效能更優, ,因為建立堆的效能更優。
public class HeapSort2 {
// 我們的演算法類不允許產生任何例項
private HeapSort2(){}
public static void sort(Comparable[] arr){
int n = arr.length;
MaxHeap<Comparable> maxHeap = new MaxHeap<Comparable>(arr);
for( int i = n-1 ; i >= 0 ; i -- )
arr[i] = maxHeap.extractMax();
}
}
6.5.2、原地堆排序
之前實現的堆排序,還需要使用 O(n) 的輔助空間來建立堆。
- 現在的建立最大堆的操作只是將待排序的陣列拷貝到堆類的內部進行
heapify
,但我們可以直接對待排序的陣列進行heapify
操作使其成為一個形式意義上的最大堆,(滿足最大堆的性質而沒有堆的API)
- 交換當前最大堆的首個元素 v 和當前最大堆中的最後一個元素 w
因為是最大堆,所以首個元素 v 即是最大值,交換之後 v 就排好序了 - 經過步驟2後,除去 v 後的剩餘序列已經不再滿足最大堆的性質了, 再對交換後排在首位的 w 進行 Sift Down 操作,使除去 v 後的剩餘序列繼續是一個最大堆
- 重複步驟2,直到當前最大堆只剩一個元素,排序完成
// 不使用一個額外的最大堆, 直接在原陣列上進行原地的堆排序
public class HeapSort3 {
// 我們的演算法類不允許產生任何例項
private HeapSort3(){}
public static void sort(Comparable[] arr){
int n = arr.length;
// 將 arr 陣列構建成為一個最大堆(從形式上看是這樣的)
// 注意,此時我們的堆是從0開始索引的
// 從(最後一個元素的索引-1)/2開始
// 最後一個元素的索引 = n-1
for( int i = (n-1-1)/2 ; i >= 0 ; i -- )
siftDown2(arr, n, i);
for( int i = n-1; i > 0 ; i-- ){
swap( arr, 0, i);
siftDown2(arr, i, 0);
}
}
// 交換堆中索引為i和j的兩個元素
private static void swap(Object[] arr, int i, int j){
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
// 原始的siftDown過程
private static void siftDown(Comparable[] arr, int n, int k){
while( 2*k+1 < n ){
int j = 2*k+1;
if( j+1 < n && arr[j+1].compareTo(arr[j]) > 0 )
j += 1;
if( arr[k].compareTo(arr[j]) >= 0 )break;
swap( arr, k, j);
k = j;
}
}
// 優化的siftDown過程, 使用賦值的方式取代不斷的swap,
// 該優化思想和我們之前對插入排序進行優化的思路是一致的
private static void siftDown2(Comparable[] arr, int n, int k){
Comparable e = arr[k];
while( 2*k+1 < n ){
int j = 2*k+1;
if( j+1 < n && arr[j+1].compareTo(arr[j]) > 0 )
j += 1;
if( e.compareTo(arr[j]) >= 0 )
break;
arr[k] = arr[j];
k = j;
}
arr[k] = e;
}
}
7、排序演算法總結
8、工具類SortTestHelper
(我迷了,反射都快忘完了)
package Ⅰ_sorting_basic.util;
import java.lang.reflect.Method;
import java.lang.Class;
import java.util.Random;
public class SortTestHelper {
// SortTestHelper不允許產生任何例項
private SortTestHelper(){}
// 生成有n個元素的隨機陣列,每個元素的隨機範圍為[rangeL, rangeR]
public static Integer[] generateRandomArray(int n, int rangeL, int rangeR) {
assert rangeL <= rangeR;
Integer[] arr = new Integer[n];
for (int i = 0; i < n; i++)
arr[i] = new Integer((int)(Math.random() * (rangeR - rangeL + 1) + rangeL));
return arr;
}
// 生成一個近乎有序的陣列
// 首先生成一個含有[0...n-1]的完全有序陣列, 之後隨機交換swapTimes對資料
// swapTimes定義了陣列的無序程度:
// swapTimes == 0 時, 陣列完全有序
// swapTimes 越大, 陣列越趨向於無序
public static Integer[] generateNearlyOrderedArray(int n, int swapTimes){
Integer[] arr = new Integer[n];
for( int i = 0 ; i < n ; i ++ )
arr[i] = new Integer(i);
for( int i = 0 ; i < swapTimes ; i ++ ){
int a = (int)(Math.random() * n);
int b = (int)(Math.random() * n);
int t = arr[a];
arr[a] = arr[b];
arr[b] = t;
}
return arr;
}
// 列印arr陣列的所有內容
public static void printArray(Object[] arr) {
for (int i = 0; i < arr.length; i++){
System.out.print( arr[i] );
System.out.print( ' ' );
}
System.out.println();
return;
}
// 判斷arr陣列是否有序
public static boolean isSorted(Comparable[] arr){
for( int i = 0 ; i < arr.length - 1 ; i ++ )
if( arr[i].compareTo(arr[i+1]) > 0 )
return false;
return true;
}
// 測試sortClassName所對應的排序演算法排序arr陣列所得到結果的正確性和演算法執行時間
public static void testSort(String sortClassName, Comparable[] arr){
// 通過Java的反射機制,通過排序的類名,執行排序函式
try{
// 通過sortClassName獲得排序函式的Class物件
Class sortClass = Class.forName(sortClassName);
// 通過排序函式的Class物件獲得排序方法
Method sortMethod = sortClass.getMethod("sort",new Class[]{Comparable[].class});
// 排序引數只有一個,是可比較陣列arr
Object[] params = new Object[]{arr};
long startTime = System.currentTimeMillis();
// 呼叫排序函式
sortMethod.invoke(null,params);
long endTime = System.currentTimeMillis();
assert isSorted( arr );
System.out.println( sortClass.getSimpleName()+ " : " + (endTime-startTime) + "ms" );
}
catch(Exception e){
e.printStackTrace();
}
}
}