排序算法的總結
首先,對於所有的排序做一個總結,隨後是所有排序的實現方式
排序算法的穩定性
穩定性是指同樣大小的樣本再排序不會改變次序
對於基礎類型的排序
對於非基礎類型的排序來說,穩定性有重要的意義(比如結果需要年齡有序並且年齡相等身高有序)
有些排序算法可以實現穩定性,有的排序算法無論如何都無法實現穩定
速記 | 時間複雜度 | 額外空間複雜度 | 穩定性 | |
---|---|---|---|---|
選擇排序 | 最小最前 | O(N²) | O(1) | 無 |
冒泡排序 | 最大最後 | O(N²) | O(1) | 有 |
插入排序 | 逐步有序 | O(N²) | O(1) | 有 |
歸併排序 | 二分遞歸 | O(logN*N ) |
O(N) | 有 |
隨機快排 | 確認中點 | O(logN*N ) |
O(logN ) |
無 |
堆排序 | 建立堆結構 | O(logN*N ) |
O(1) | 無 |
桶排序實現 | ||||
計數排序 | 幾個元素幾個桶 | O(N) | O(M) | 有 |
基數排序 | O(N) | O(1) | 有 |
- 不基於比較的排序(桶排序),對樣本的數據有嚴格的要求,不易改寫
- 基於比較的排序,只要規定兩個樣本怎麼比較大小就能夠複用
- 基於比較的排序,算法的時間複雜度的極限是N*logN
- 時間複雜度爲O(logN*N),空間複雜度低於O(N),且具有穩定性的算法是不存在的
- 爲了絕對的速度,選擇快速排序,爲了省空間(常數時間操作長),選擇堆排序,爲了穩定性,選擇歸併排序
選擇排序
- arr[0-N-1]範圍上,找到最小值所在的位置,然後把最小值交換到0位置
- arr[1-N-1]範圍上,找到最小值所在的位置,然後把最小值交換到1位置
- …
- arr[N-1~N-1]範圍上,找到最小值位置,然後把最小值交換到N-1位置。
時間複雜度估計
總操作數爲等差數列
算出快排的時間複雜度爲 n²/2 +n/2 其中忽略低階項和高階項係數得出時間負載度爲O(N²)
代碼實現
public static void solution(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int index = i;
for (int j = i; j < arr.length; j++) {
if (arr[j] < arr[index]) {
index = j;
}
}
swap(arr, i, index);
}
}
冒泡排序
在arr[0~N-1]範圍上:
-
arr[0]和arr[1],誰大誰來到1位置;arr[1]和arr[2],誰大誰來到2位置…arr[N-2]和arr[N-1],誰大誰來到N-1位置
-
在arr[0~N-2]範圍上,重複上面的過程,但最後一步是arr[N-3]和arr[N-2],誰大誰來到N-2位置
-
在arr[0~N-3]範圍上,重複上面的過程,但最後一步是arr[N-4]和arr[N-3],誰大誰來到N-3位置
-
…
-
最後在arr[0~1]範圍上,重複上面的過程,但最後一步是arr[0]和arr[1],誰大誰來到1位置
public static void solution(int[] arr) {
//在 0 -> N-1 看自己和下一個數那個小,大的往後面移動
//0 ->N-2
//0 ->N-3.....
for (int i = arr.length - 1; i >= 0; i--) {
for (int j = 0; j < i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
}
}
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
插入排序
- 想讓arr[0~0]上有序,這個範圍只有一個數,當然是有序的。
- 想讓arr[0~1]上有序,所以從arr[1]開始往前看,如果arr[1]<arr[0],就交換。否則什麼也不做。
- …
- 想讓arr[0~i]上有序,所以從arr[i]開始往前看,arr[i]這個數不停向左移動,一直移動到左邊的數字不再比自己大,停止移動。
- 最後一步,想讓arr[0~N-1]上有序, arr[N-1]這個數不停向左移動,一直移動到左邊的數字不再比自己大,停止移動。
估算時發現這個算法流程的複雜程度,會因爲數據狀況的不同而不同。
如果數據是{1,2,3,4,5,6,7}這種本來就有序的數據集,那麼時間複雜度是O(N),但是最差情況的時間複雜度是等差數列,所以時間複雜度爲O(N²)
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 0~0 有序的
// 0~i 想有序
for (int i = 1; i < arr.length; i++) { // 0 ~ i 做到有序
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
swap(arr, j, j + 1);
}
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
歸併排序
- 整體是遞歸,左邊先排好序+右邊排好序+merge讓整體有序
- 讓整體有序的過程採用了排外序的方法
- 利用master公式來求時間複雜度
- 當然可以用非遞歸實現
時間複雜度爲O(N*logN)
// 遞歸方法實現
public static void mergeSort1(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process(arr, 0, arr.length - 1);
}
// arr[L...R]範圍上,變成有序的
// L...R N T(N) = 2*T(N/2) + O(N) ->
public static void process(int[] arr, int L, int R) {
if (L == R) { // base case
return;
}
int mid = L + ((R - L) >> 1);
process(arr, L, mid);
process(arr, mid + 1, R);
merge(arr, L, mid, R);
}
public static void merge(int[] arr, int L, int M, int R) {
int[] help = new int[R - L + 1];
int i = 0;
int p1 = L;
int p2 = M + 1;
while (p1 <= M && p2 <= R) {
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
// 要麼p1越界了,要麼p2越界了
while (p1 <= M) {
help[i++] = arr[p1++];
}
while (p2 <= R) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
快速排序
partition過程
給定一個數組arr,和一個整數num。請吧小於等於num的數放在數組的左邊,大於num的數放在數組的右邊。
在arr[L…R]範圍上,進行快速排序的過程
- 在這個範圍上,隨機選一個數爲num
- 用num對該範圍進行partition。==num的範圍爲[a,b]
- 對arr[L…a-1]進行快速排序(遞歸)
- 對arr[b+1…R]進行快速排序(遞歸)
隨機快排時間複雜度
- 通過分析知道,劃分值約靠近中間,性能越好,越靠近兩邊,性能越差
- 隨機選一個數劃分的目的是把好和差變成概率事件
- 每一種情況都列出,會有每種情況的時間複雜度,概率都爲1/N
- 所有情況都考慮,那麼這種模型的長期期望就是時間複雜度
時間複雜度爲O(N*logN) 額外的空間複雜度爲O(logN)
public static void quickSort3(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process3(arr, 0, arr.length - 1);
}
public static void process3(int[] arr, int L, int R) {
if (L >= R) {
return;
}
swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
int[] equalArea = netherlandsFlag(arr, L, R);
process3(arr, L, equalArea[0] - 1);
process3(arr, equalArea[1] + 1, R);
}
// <arr[R] ==arr[R] > arr[R]
public static int[] netherlandsFlag(int[] arr, int L, int R) {
if (L > R) {
return new int[] { -1, -1 };
}
if (L == R) {
return new int[] { L, R };
}
int less = L - 1; // < 區 右邊界
int more = R; // > 區 左邊界
int index = L;
while (index < more) {
if (arr[index] == arr[R]) {
index++;
} else if (arr[index] < arr[R]) {
swap(arr, index++, ++less);
} else { // >
swap(arr, index, --more);
}
}
swap(arr, more, R);
return new int[] { less + 1, more };
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
堆排序
堆結構
- 堆結構就是用數組實現的一顆完全二叉樹
- 完全二叉樹中如果每顆字樹的最大值都在頂部就是大根堆
- 完全二叉樹中如果每棵子樹的最小值都在頂部就是小根堆
- 堆結構的heapInsert與heapify操作
- 堆結構的增大和減少
- 優先級隊列結構就是堆結構
public static void solution(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
//時間複雜度O(N*logN)
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int heapSize = arr.length;
swap(arr,0,--heapSize);
//最大的數一直在數組第一個,然後把她與最後一個數交換,堆的大小減一
while (heapSize > 0) { // O(N)
heapify(arr, 0, heapSize); // O(logN)
swap(arr, 0, --heapSize); // O(1)
}
}
// arr[index]位置的數,能否往下移動
public static void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1; // 左孩子的下標
while (left < heapSize) { // 下方還有孩子的時候
// 兩個孩子中,誰的值大,把下標給largest
// 1)只有左孩子,left -> largest
// 2) 同時有左孩子和右孩子,右孩子的值<= 左孩子的值,left -> largest
// 3) 同時有左孩子和右孩子並且右孩子的值> 左孩子的值, right -> largest
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
// 父和較大的孩子之間,誰的值大,把下標給largest
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
// arr[index]剛來的數,往上
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public static void printArrat(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] +"\t");
}
}
桶排序
桶排序思想下的排序:計數排序和基數排序
- 桶排序思想下的排序都是不基於比較的排序
- 時間複雜度爲O(N),額外空間複雜度爲O(M)
- 應用範圍有限,需要更具樣本的數據狀況滿足桶的劃分
計數排序
根據年齡排序,建立1-200 的桶,將對應的元素放入對應的桶中。通過保證放入順序保持穩定性
/**
* 計數排序只適合值的範圍比較小的排序
* 根據需要排序的數組的最大值,生成一個數組,將原數組中的值放入生成數組中對應索引的位置
* 適合場景,排序年齡,根據年齡排序(使用對應多個桶)
*
* @param arr
*/
public static void solution(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int max = 0;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
}
int[] help = new int[max + 1];
for (int i = 0; i < arr.length; i++) {
help[arr[i]] += 1;
}
int index = 0;
for (int i = 0; i < help.length; i++) {
for (int j = 0; j < help[i]; j++) {
arr[index++] = i;
}
}
}
基數排序
對10進制的數字進行排序
public static void solution(int[] arr, int L, int R, int dight) {
int i = 0, j = 0;
int[] help = new int[R - L + 1];
//數字有多少位就使用多少次循環
for (int d = 1; d <= dight; d++) {
int[] cout = new int[10];
//使用cout記錄每一位是對應數字的有多少
for (i = L; i < R + 1; i++) {
j = getDigit(arr[i], d);
cout[j]++;
}
//把cout變爲小於等於該值的有多少
for (int k = 1; k < cout.length; k++) {
cout[k] += cout[k - 1];
}
//從後往前,如果該位是對應數字的話,使用cout中的值,把該數放在最大的位置上,因爲是從後往前,所以保證了穩定性
for (int k = R; k >= L; k--) {
int a = getDigit(arr[k], d);
help[cout[a] - 1] = arr[k];
cout[a]--;
}
//把help的值賦給原數組
for (int k = L, c = 0; k <= R; k++, c++) {
arr[k] = help[c];
}
}
}