🥰🥰🥰來都來了,不妨點個關注叭!
👉博客主頁:歡迎各位大佬!👈
歡迎來到排序算法的學習,恭喜你!本期內容主要介紹排序算法,一起來探索吧~
(ps:真的一直都想寫有關排序的文章,奈何每天尊嘟好忙,終于開寫啦!)
文章目錄
- 1. 排序的基本概念
- 1.1 排序預備知識
- 1.2 內部排序與外部排序
- 1.3 排序的穩定性
- 1.4 排序的分類
- 2. 插入類排序
- 2.1 直接插入排序
- 2.2 希爾排序
- 3. 選擇類排序
- 3.1 直接選擇排序
- 3.2 堆排序
- 4. 交換類排序
- 4.1 冒泡排序
- 4.2 快速排序
- 4.2.1 三種實現方式
- 4.2.1.1 hoare 法
- 4.2.1.2 挖坑法
- 4.2.1.3 前后指針法
- 4.2.1.4 快排優化
- 4.2.2 快排非遞歸實現
- 5. 歸并排序
- 6. 特性總結
1. 排序的基本概念
1.1 排序預備知識
排序:排序的目的是使一串記錄,將一組“無序”的記錄序列調整為"有序"的記錄序列,按照其中的某個或某些關鍵字的大小,遞增或遞減的排列起來的操作。
1.2 內部排序與外部排序
內部排序:數據元素全部放在內存中的排序,內部排序的過程是一個逐步擴大記錄的有序序列長度的過程。
外部排序:數據元素數量非常大,不能同時放在內存中,整個過程不可能在內存中完成。
1.3 排序的穩定性
穩定性:假定在待排序的記錄序列中,存在兩個或者兩個以上的具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,則稱為穩定,否則為不穩定
1.4 排序的分類
插入類排序:將待排序的值插入到前面已經有序的序列中
選擇類排序:每次排序選出序列的最大值/最小值,放到序列的最后面
交換排序:兩兩比較待排序的序列,并交換不滿足序列的那對數
以上為經典排序,本文主要介紹常見的 7 種排序算法,我們需要掌握~ 一起來正式進入下面的學習吧!
2. 插入類排序
2.1 直接插入排序
【基本思想】 將記錄的值,插入到已經排序好的有序序列中,直到所有記錄的值全部插入,則該過程完成。
【基本思路】 僅有一個數據時,則認為該數據為已經排好序的,則我們可以這樣思考:
- 第1個元素已排好序,從第2個元素開始,下標為i,取出該元素放在變量 tmp 中,從已排好序序列中從后往前使用 j 遍歷比較
- 如果下標 j 的值大于 tmp,則 j+1 的值替換為 j 下標的值,繼續遍歷
- 如果下標 j 的值小于 tmp,則 break,跳出遍歷
- 最后 j+1 的值替換成臨時變量 tmp 存儲的值
- 此時前兩個元素已經是有序的了,再用 i 繼續遍歷(詳細解釋可以看示意圖~)
有沒有點像打撲克牌時候呢?拿到一張牌就在以往排好序的牌中插入~
【示意圖】
【具體代碼】
/*** Created with IntelliJ IDEA.* Description:* User: wlx* Date: 2025-06-01* Time: 23:58*/
public class InsertSort {public static void main(String[] args) {int[] array = {1,4,2,9,6,7};insertSort(array);System.out.println(Arrays.toString(array));}public static void insertSort(int[] array) {for(int i = 1; i < array.length; i++) {int tmp = array[i];int j = i-1;for(; j >= 0; j--) {if(array[j] > tmp) {array[j+1] = array[j];}else {//array[j+1] = tmp;break;}}array[j+1] = tmp;}}
}
【特性總結】
- 時間復雜度:O(n2),當數據本身就有序的時候,時間復雜度為O(n)
- 空間復雜度:O(1)
- 穩定性:穩定
- 適用場景:當數據基本趨于有序時,建議使用直接插入排序
2.2 希爾排序
【基本思想】希爾排序又稱為縮小增量法,其基本思想是先選定一個整數 gap,將待排序的數據分為多個組,每組距離 gap,對每一組進行排序,每一組兩個元素,接著 gap/2 ,縮小每組的距離,當 gap = 1 時,所有數據就排好序了
簡單理解就是按照 gap 分組,組內進行插入排序,當 gap = 1 時,則為直接插入排序
【基本思路】與直接插入排序思路一致,希爾排序是直接插入排序的優化,先用 gap 分組,讓數組預有序,當 gap 為 1 時,數組也基本有序了,此時就是直接插入排序~
【示意圖】
【具體代碼】
/*** Created with IntelliJ IDEA.* Description:* User: wlx* Date: 2025-06-02* Time: 10:28*/
public class ShellSort {public static void main(String[] args) {int[] array = {1,4,2,9,6,7};int gap = array.length;while(gap > 1) {shellSort(array,gap);gap /= 2;}shellSort(array,1);System.out.println(Arrays.toString(array));}public static void shellSort(int[] array,int gap) {for(int i = gap; i < array.length; i++) {int tmp = array[i];int j = i-gap;for(; j >= 0; j-=gap) {if(array[j] > tmp) {array[j+gap] = array[j];}else {//array[j+1] = tmp;break;}}array[j+gap] = tmp;}}
【特性總結】
- 時間復雜度:O(n^1.3) - O(n^1.5) ,當數據本身就有序的時候,時間復雜度為O(n),且希爾排序的時間復雜度取決于增量值 gap 的選取,即時間復雜度并不是一個定值
- 空間復雜度:O(1)
- 穩定性:不穩定
- 希爾排序是直接插入排序的優化,gap >1 的排序是預排序,使數據趨于有序,gap=1 時則是直接插入排序
3. 選擇類排序
3.1 直接選擇排序
【基本思想】每次從待排序的序列中選出最小/最大元素,放在序列的起始位置,直到全部待排序的數據排完
【基本思路】定義一個 minIndex 下標用來存儲最小值的下標,第1次 minIndex = 0 下標,遍歷整個待排序序列,當有更小數據時,更新 minIndex 下標,再交換起始位置 i 與 minIndex 下標值,這樣就找到第1小的元素,接著再遍歷,minIndex = 1,遍歷后面的待排序元素,找到第2小數據,并進行交換,以此類推…
【示意圖】
【具體代碼】
/*** Created with IntelliJ IDEA.* Description:* User: wlx* Date: 2025-06-02* Time: 10:53*/
public class SelectSort {public static void main(String[] args) {int[] array = {1,4,2,9,6,7};selectSort(array);System.out.println(Arrays.toString(array));}public static void selectSort(int[] array) {for(int i = 0; i < array.length; i++) {int minIndex = i;for(int j = i+1; j < array.length; j++) {if(array[j] < array[minIndex]) {minIndex = j;}}swap(array,i,minIndex);}}public static void swap(int[] array,int i, int j) {int tmp = array[i];array[i] = array[j];array[j] = tmp;}
}
【特性總結】
- 時間復雜度:O(n2)
- 空間復雜度:O(1)
- 穩定性:不穩定
3.2 堆排序
【基本思想】堆排序是指利用堆這種數據結構所設計的一種算法,它是選擇排序的一種,通過堆進行選擇數據,排升序是建大堆,排降序建立小堆
大根堆:每個節點的值都 >= 其子節點的值,用于升序排列
小根堆:每個節點的值都 <= 其子節點的值,用于降序排列
【基本思路】堆排序就是先將我們的數組進行一次建堆,循環交換堆頂元素(最大值)與最后一個元素,即交換數組頭和尾的元素,然后重新建堆,再進行交換堆頂元素與最后一個元素
【示意圖】
【具體代碼】
import java.util.Arrays;/*** Created with IntelliJ IDEA.* Description:* User: wlx* Date: 2025-06-22* Time: 23:52*/
public class HeapSort {public static void heapSort(int[] array) {createBigHeap(array);int end = array.length - 1;while (end > 0) {swap(array, 0, end);shiftDown(array, 0, end);end--;}}public static void createBigHeap(int[] array) {for (int parent = (array.length - 1 - 1) / 2; parent >= 0; parent--) {shiftDown(array, parent, array.length);}}//建大堆private static void shiftDown(int[] array, int parent, int len) {int child = 2 * parent + 1;while (child < len) {if (child + 1 < len && array[child] < array[child + 1]) {child++;}if (array[child] > array[parent]) {swap(array, child, parent);parent = child;child = 2 * parent + 1;} else {break;}}}public static void swap(int[] arr,int i,int j) {int tmp = arr[i];arr[i] = arr[j];arr[j] = tmp;}public static void main(String[] args) {int[] array = {1,4,2,9,6,7};heapSort(array);System.out.println(Arrays.toString(array));}
}
【特性總結】
- 時間復雜度:O(N*logN)
- 空間復雜度:O(1)
- 穩定性:不穩定
4. 交換類排序
4.1 冒泡排序
【基本思想】將待排序的數據兩兩進行比較,按比較結果來交換序列位置,將值大/值小的數據放到序列的尾部,將值小/值大的數據放到序列的首部位置,依次比較放置,最后得到有序序列
【基本思路】冒泡排序的每一次循環比較就是找出最大值(最小值),將最大值(最小值)放在數組尾部(首部),經過n次比較后,數據變成有序的,這里 flag 變量是優化作用,如果一輪下來沒交換,則數組已經有序~
【示意圖】
【具體代碼】
/*** Created with IntelliJ IDEA.* Description:* User: wlx* Date: 2025-06-03* Time: 23:00*/
public class BubbleSort {public static void main(String[] args) {int[] array = {1,4,2,9,6,7};bubbleSort(array);for(int i = 0; i < array.length; i++) {System.out.print(array[i]+" ");}}private static void bubbleSort(int[] array) {for(int i = 0; i < array.length-1; i++) {boolean flag = false;for(int j = 0; j < array.length-1-i;j++) {if(array[j+1] < array[j]) {swap(array,j,j+1);flag = true;}}if(flag == false) {break;}}}public static void swap(int[] array,int i,int j) {int tmp = array[i];array[i] = array[j];array[j] = tmp;}
}
【特性總結】
- 時間復雜度:O(N^2)
- 空間復雜度:O(1)
- 穩定性:穩定
4.2 快速排序
【基本思想】先從數組中取一個數作為基準數,進行分區,將比這個大的數全放到它的右邊,小于或等于它的數全部放到它的左邊,直到所有元素在它的位置上,此時就有序了~
【主體框架】
這是快速排序的主框架,我們可以看到這和二叉樹的前序遍歷遞歸實現的方式很類似,因此,我們在寫快速排序的時候,可以聯想到二叉樹的前序遍歷是如何寫的,接下來,介紹快速排序的幾種方式:
public class quickSort {public void quickSort(int[] num) {int left = 0;int right = num.length-1;quick(num,left,right);}private void quick(int[] num, int left, int right) {if(left >= right) {return;}// 按照基準值對數組的[left,right]劃分int pivot = partition(num,left,right);// 劃分區間:[left,pivot-1] [pivot,right]// 遞歸排序[left,pivot-1]quick(num,left,pivot-1);// 遞歸排序[pivot,right]quick(num,pivot+1,right);}private int partition(int[] num, int left, int right) {// 根據一定的規則返回基準值return -1;}
}
4.2.1 三種實現方式
4.2.1.1 hoare 法
【基本思路】
hoare法就是定義兩個標志位,right 從右往左找到比基準值小的值停下,left 從左往右找到比基準值大的值停下,交換 left 和 right 位置的值,循環繼續,直到 left 和 right 相遇,最后再交換基準值和相遇處的值,再對左右子序列重復此過程,直到數據變成有序
1)先取最左側即第一個元素為基準值
2)兩個指針 left 和 right,left 從最左側向右走,找到比基準大的元素停下,right 從最右側向左走,找到比基準小的元素停下,交換 left 下標的值和 right 下標的值
3)當 left >= right 兩個指針不再移動,此時 left = right 的下標即為基準值對應的下標,交換基準值初始下標和left(right)下標
【示意圖】
【具體代碼】
import java.util.Arrays;/*** Created with IntelliJ IDEA.* Description:* User: wlx* Date: 2025-06-22* Time: 0:05*/
public class quickSort {public static void quickSort(int[] num) {int left = 0;int right = num.length-1;quick(num,left,right);}public static void quick(int[] num, int left, int right) {if(left >= right) {return;}int pivot = partition(num,left,right);quick(num,left,pivot-1);quick(num,pivot+1,right);}// 1.hoare法public static int partition(int[] num, int left, int right) {// 根據一定的規則返回基準值int i = left+1;int j = right;int tmp = num[left];while(true) {while(i <= j && num[i] < tmp) {i++;}while(i <= j && num[j] > tmp) {j--;}if(i >= j) {break;}swap(num,i,j);}swap(num,left,j);return left;}public static void swap(int[] num,int i,int j) {int tmp = num[i];num[i] = num[j];num[j] = tmp;}public static void main(String[] args) {int[] array = {1,4,2,9,6,7};quickSort(array);System.out.println(Arrays.toString(array));}
}
4.2.1.2 挖坑法
【基本思路】
1)先取最左側即第一個元素為基準值
2)兩個指針 left 和 right,left 從最左側向右走,找到比基準大的元素停下,將 right 下標處的值賦值給 left 下標的值,right 從最右側向左走,找到比基準小的元素停下,將 left 下標處的值賦值給 right 下標的值,如此循環
3)當 left >= right 兩個指針不再移動,此時 left = right 的下標即為基準值對應的下標,交換基準值初始下標和left(right)下標
【示意圖】
【具體代碼】
import java.util.Arrays;/*** Created with IntelliJ IDEA.* Description:* User: wlx* Date: 2025-06-22* Time: 0:10*/
public class quickSort {public static void quickSort(int[] num) {int left = 0;int right = num.length-1;quick(num,left,right);}public static void quick(int[] num, int left, int right) {if(left >= right) {return;}int pivot = partition(num,left,right);quick(num,left,pivot-1);quick(num,pivot+1,right);}// 2.挖坑法public static int partition(int[] arr,int left,int right) {int tmp = arr[left];while(left < right) {// 找到比基準tmp小的while(left < right && arr[right] >= tmp) {right--;}arr[left] = arr[right];// 找到比基準tmp大的while(left < right && arr[left] <= tmp) {left++;}arr[right] = arr[left];}arr[left] = tmp;return left;}public static void main(String[] args) {int[] array = {1,4,2,9,6,7};quickSort(array);System.out.println(Arrays.toString(array));}
}
4.2.1.3 前后指針法
【基本思路】
1)先取最左側即第一個元素為基準值
2)前后指針 prev 和 cur,prev初始為left,cur初始為right,如果 cur 下標值小于基準值并且前指針prev++后的值與cur下標值不相等,前后指針至少一個間隔,并且值不相等,則交換前后指針的值
3)最后cur走到right,循環結束,交換prev和left值,此時prev就是基準值的位置
【示意圖】
【具體代碼】
import java.util.Arrays;/*** Created with IntelliJ IDEA.* Description:* User: wlx* Date: 2025-06-22* Time: 0:30*/
public class quickSort {public static void quickSort(int[] num) {int left = 0;int right = num.length-1;quick(num,left,right);}public static void quick(int[] num, int left, int right) {if(left >= right) {return;}int pivot = partition(num,left,right);quick(num,left,pivot-1);quick(num,pivot+1,right);}//3.前后指針法private static int partition(int[] array,int left,int right) {int prev = left ;int cur = left+1;while (cur <= right) {if(array[cur] < array[left] && array[++prev] != array[cur]) {swap(array,cur,prev);}cur++;}swap(array,prev,left);return prev;}public static void swap(int[] arr,int i,int j) {int tmp = arr[i];arr[i] = arr[j];arr[j] = tmp;}public static void main(String[] args) {int[] array = {1,4,2,9,6,7};quickSort(array);System.out.println(Arrays.toString(array));}
}
4.2.1.4 快排優化
快排的基準是隨機選取的,一般情況下,快排的時間復雜度為 O(nlog(n)),比如數據是有序的1,2,3,4,5… 會變成 O(n2)
時間復雜度我們可以用二叉樹來理解,如下:
一般情況下,基準到達對應的位置后,序列被分為了左右子序列,基準元素為根結點,左邊都比根節點的值小,右邊都比根節點的值大,此時時間復雜度為 O(nlog(n))
存在特殊情況,數據是有序的1,2,3,4,5…
只有一個分支,此時樹的高度就是結點的個數,時間復雜度則為O(n2),而當數據量足夠大的時候,上述代碼就無法跑通了~
【優化方法】
1)基準優化:三數取中法
即基準值取 left,mid,right 三個值中間的值,而不再是單純取下標為 left 的值
import java.util.Arrays;/*** Created with IntelliJ IDEA.* Description:* User: wlx* Date: 2025-06-22* Time: 0:40*/
public class quickSort {public static void quickSort(int[] num) {int left = 0;int right = num.length-1;quick(num,left,right);}public static void quick(int[] num, int left, int right) {if(left >= right) {return;}// 基準值優化int index = midThree(num,left,right);swap(num,index,left);int pivot = partition(num,left,right);quick(num,left,pivot-1);quick(num,pivot+1,right);}private static int midThree(int[] array,int left,int right) {int mid = (left + right) / 2;if (array[left] < array[right]) {if (array[mid] < array[left]) {return left;} else if (array[mid] > array[right]) {return right;} else {return mid;}} else {if (array[mid] < array[right]) {return right;} else if (array[mid] > array[left]) {return left;} else {return mid;}}}public static int partition(int[] arr,int left,int right) {int tmp = arr[left];while(left < right) {// 找到比基準tmp小的while(left < right && arr[right] >= tmp) {right--;}arr[left] = arr[right];// 找到比基準tmp大的while(left < right && arr[left] <= tmp) {left++;}arr[right] = arr[left];}arr[left] = tmp;return left;}public static void swap(int[] arr,int i,int j) {int tmp = arr[i];arr[i] = arr[j];arr[j] = tmp;}public static void main(String[] args) {int[] array = {1,4,2,9,6,7};quickSort(array);System.out.println(Arrays.toString(array));}
}
2) 遞歸至較小的子區間時,使用插入排序:
遞歸至較小區間的時間,數據漸漸趨于有序,當數據有序的時候,建議直接使用插入排序,這樣效率是比較高的
4.2.2 快排非遞歸實現
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;/*** Created with IntelliJ IDEA.* Description:* User: wlx* Date: 2025-06-22* Time: 22:41*/// 非遞歸實現快排
public class quickSort {public static void quickSort(int[] array) {Deque<Integer> stack = new LinkedList<>();int left = 0;int right = array.length - 1;int pivot = partition(array,left,right);if (pivot > left + 1) {stack.push(left);stack.push(pivot - 1);}if (pivot < right-1) {stack.push(pivot+1);stack.push(right);}while (!stack.isEmpty()) {right = stack.pop();left = stack.pop();pivot = partition(array,left,right);if (pivot > left + 1) {stack.push(left);stack.push(pivot - 1);}if (pivot < right-1) {stack.push(pivot+1);stack.push(right);}}}public static int partition(int[] arr,int left,int right) {int tmp = arr[left];while(left < right) {// 找到比基準tmp小的while(left < right && arr[right] >= tmp) {right--;}arr[left] = arr[right];// 找到比基準tmp大的while(left < right && arr[left] <= tmp) {left++;}arr[right] = arr[left];}arr[left] = tmp;return left;}public static void main(String[] args) {int[] array = {1,4,2,9,6,7};quickSort(array);System.out.println(Arrays.toString(array));}}
5. 歸并排序
【基本思想】歸并排序是建立在歸并操作上的一種有效算法,該算法是分治法的一種典型應用,將已有序的子序列合并,得到完全有序的序列,即先使每個子序列有序,將兩個有序序列合并為一個有序序列,稱為二路歸并
【基本思路】使用遞歸的方式,先分解,再進行合并
【示意圖】
【具體代碼】
import java.util.Arrays;/*** Created with IntelliJ IDEA.* Description:* User: wlx* Date: 2025-06-21* Time: 22:53*/
public class Mergesort {public static void mergeSort(int[] arr) {mergeFunc(arr,0,arr.length-1);}public static void mergeFunc(int[] arr,int left,int right) {if(left >= right) {return;}int mid = (left+right) / 2;mergeFunc(arr,left,mid);mergeFunc(arr,mid+1,right);merge(arr,left,right,mid);}public static void merge(int[] arr,int left,int right,int mid) {int start1 = left;int start2 = mid+1;int k = 0;int[] tmp = new int[right-left+1];while(start1 <= mid && start2 <= right) {if(arr[start1] < arr[start2]) {tmp[k++] = arr[start1++];} else {tmp[k++] = arr[start2++];}}while(start1 <= mid) {tmp[k++] = arr[start1++];}while(start2 <= right) {tmp[k++] = arr[start2++];}for(int i = 0; i < k; i++) {arr[left+i] = tmp[i];}}public static void main(String[] args) {int[] arr = {1,4,2,9,6,7};mergeSort(arr);System.out.println(Arrays.toString(arr));}
}
【特性總結】
- 時間復雜度:O(O(N*logN))
- 空間復雜度:O(1)
- 穩定性:穩定
- 應用場景:適合數據特別大的時候,當待排序數據特別大的時候,比如內存只要 10G,但是待排序的數據有 100G,此時可以將待處理的數據分為 20份,每一份 512M,利用歸并排序分別對這 512M 的數據進行排序,同時進行二路歸并,最后使數據變為有序,這就是文章一開頭介紹的外部排序~
6. 特性總結
排序名稱 | 平均時間復雜度 | 最好情況 | 最壞情況 | 空間復雜度 | 穩定性 |
---|---|---|---|---|---|
直接插入排序 | O(n2) | O(n) | O(n2) | O(1) | 穩定 |
希爾排序 | O(n^1.3) - O(n^1.5) | O(n log2n) | O(n log2n) | O(1) | 不穩定 |
直接選擇排序 | O(n2) | O(n2) | O(n2) | O(1) | 不穩定 |
堆排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(1) | 不穩定 |
冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | 穩定 |
快速排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(logN) | 不穩定 |
歸并排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(1) | 穩定 |
【注意】這里的時間復雜度為最壞時間復雜度
???本期內容到此結束啦~