文章目錄
排序算法總結
概念
- 穩定性:
假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序後的序列中,r[i]仍在r[j]之前,則稱這種排序算法是穩定的;否則稱爲不穩定的。
排序算法分類
時間複雜度
排序算法 | (平均)時間複雜度 | (最好)時間複雜度 | (最壞)時間複雜度 | 空間複雜度 | 穩定性 |
---|---|---|---|---|---|
插入排序 | O(n2) | O(n) (基本有序) | n2 | O(1) | 穩定 |
希爾排序 | 不穩定 | ||||
選擇排序 | O(n2) | O(n2) | O(n2) | O(1) | 不穩定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不穩定 |
冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | 穩定 |
快速排序 | O(nlogn) | O(nlogn) | O(n2) | O(nlogn) | 不穩定 |
歸併排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 穩定 |
計數排序 | O(n + k) | O(n + k) | O(n + k) | O(n + k) | 穩定 |
桶排序 | O(n + k) | O(n) | O(n2) | O(n + k) | 穩定 |
基數排序 | O(n * k) | O(n * k) | O(n * k) | O(n + k) | 穩定 |
Top10 排序算法講解
1. 直接插入排序
-
算法思想:
第 i 趟插入排序爲:在含有 i − 1個元素的中插入一個元素,使之成爲含有 i 個元素的有序子序列。在查找插入位置的過程中,同時後移元素,所以適合從後向前掃描。
整個過程爲進行 n − 1 趟插入, 即先將整個序列的第 1個元素看成是有序的,然後從第 2個元素起,逐個進行插入,直到整個序列有序 爲止。 -
算法實現:
/**
* 插入排序實現函數
*
* @param arr 待排序序列
* @return
*/
public static int[] insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int current = arr[i];
int preIndex = i - 1;
while (preIndex >= 0 && arr[preIndex] > current) {
// 後移元素
arr[preIndex + 1] = arr[preIndex];
preIndex--;
}
// 找到插入位置之後,將第 i 個元素插入到有序序列中
arr[preIndex + 1] = current;
}
return arr;
}
- 算法優化:
注意到插入排序的過程中,每次是將第 i 個元素插入到前面有序序列中,既然是插入到有序序列中,那麼可以採用二分查找的思想來尋找第i個元素插入到前面有序序列中的位置,這樣優化之後的算法時間複雜度爲O(nlogn)。優化之後的算法:
public int[] insertSortV2(int[] nums) {
for (int i = 1; i < nums.length; i++) {
int current = nums[i];
// 採用二分查找,查找插入排序第i個元素在前面有序序列中的位置 pivot
int low = 0, mid = 0, high = i - 1;
int pivot = 0;
while (low <= high) {
mid = (low + high) / 2;
if (current < nums[mid]) {
high = mid - 1;
}
if (current >= nums[mid]) {
low = mid + 1;
}
}
// 找到插入位置之後,將第 i 個元素插入到有序序列中
pivot = low;
for (int j = i; j > pivot; j--) {
nums[j] = nums[j - 1];
}
nums[pivot] = current;
}
return nums;
}
測試平臺:LeetCode:912. 排序數組
1是使用最原始的插入排序的算法,2是使用優化之後的插入排序的算法
2.希爾排序
- 算法思想:
希爾排序的思想是採用插入排序的方法,先讓數組中任意間隔爲 h 的元素有序,剛開始 h 的大小可以是 h = n / 2,接着讓 h = n / 4,讓 h 一直縮小,當 h = 1 時,也就是此時數組中任意間隔爲1的元素有序,此時的數組就是有序的了。 - 過程演示:
3. 算法實現:
/**
* 希爾排序算法實現
*
* @param arr 待排序序列
* @return
*/
public static int[] shellSort(int[] arr) {
int len = arr.length;
// 設置默認增量爲:n/2
int gap = len / 2;
while (gap > 0) {
for (int i = gap; i < len - 1; i++) {
int current = arr[i];
int preIndex = i - gap;
while (preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex + gap] = arr[preIndex];
preIndex -= gap;
}
arr[preIndex + gap] = current;
}
gap /= 2;
}
return arr;
}
3. 選擇排序
-
算法思想:
首先 在中找到最小(大)元素,存放到排序序列的,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到。以此類推,直到所有元素均排序完畢。 -
算法實現:
/**
* 選擇排序實現函數
*
* @param arr 待排序序列
* @return
*/
public static int[] selectSort(int arr[]) {
// min記錄集合元素最小值的下標
int minIndex = 0;
for (int i = 0; i < arr.length; i++) {
minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
// 未排序的序列中有值比已排序中的最小值還小,更新最小值的位置
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 一趟排序結束,選擇最小的值放到已排序的第 i 的位置上
if (minIndex != i) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
return arr;
}
4. 堆排序
- 概念:
-
堆排序:
堆排序是利用堆這種數據結構而設計的一種排序算法,堆排序是一種選擇排序,它的最壞,最好,平均時間複雜度均爲O(nlogn),它也是不穩定排序 -
堆:
堆是具有以下性質的:每個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆;或者每個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆。
構建的 (大/小) 頂堆,二叉樹的根是整個序列的 最(大/小)值,堆排序正是利用的是這個特性。
- 算法思想:
- 步驟1:將初始待排序關鍵字序列(R1,R2….Rn)構建成大頂堆,此堆爲初始的無序區
- 步驟2:將與交換,此時得到新的無序區(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,則整個排序過程完成。
- 算法實現:
5. 冒泡排序
-
算法思想:
首先將第 1個元素和第 2個元素進行比較,若前者大於後者,則兩者交換位置,然後比較 第 2個元素和第 3個元素。依此類推,直到第 n − 1個元素和第 n個元素進行過比較或交換爲止。上 述過程稱爲,其結果是使得 n個元素中值最大的那個元素被安排在後一個元素的位置 上。然後進行第二趟排序,即對前 n − 1個元素進行同樣的操作,使得前 n − 1個元素中值最大的那 個元素被安排在第 n − 1個位置上。
一般地,第 i 趟冒泡排序是從前 n − i + 1個元素中的第 1個元素 開始,,若前者大於後者,則交換,結果使得前 n − i + 1個元素中最大的元素被安排在第 n − i + 1個位置上。 -
優化:
顯然,判斷冒泡排序結束的條件是“在一趟排序中沒有進行過交換元素的操作”, 爲此,設立一個標誌變量 flag,flag = 1表示有過交換元素的操作,flag = 0表示沒有過交換元素的操 作,在每一趟排序開始前,將 flag置爲 0,在排序過程中,只要有交換元素的操作,就及時將 flag置 爲 1。因爲至少要執行一趟排序操作,故第一趟排序時,flag = 1。 -
算法實現:
以下提供的兩個函數,皆爲冒泡算法的實現。
bubbleSort 爲 原始的冒泡排序,bubbleSortV2爲加入flag優化過之後的冒泡排序。
大家可以在第二個循環內加入count來測試以下優化前後兩個算法的差異,可以感受到第二個函數循環執行的次數要少於第一個。
/**
* 冒泡排序函數實現
*
* @param arr 待排序序列
* @return
*/
public static int[] bubbleSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j + 1] < arr[j]) {
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
/**
* 冒泡排序函數實現,通過判斷一趟冒泡排序中,位置是否發生變化來優化,減少循環的次數
*
* @param arr 待排序數組
* @return
*/
public static int[] bubbleSortV2(int[] arr) {
// 判斷一趟冒泡排序過程中,是否發生交換,如果沒有發生交換,則代表序列已經有序。
// flag = 1:發生交換,flag = 0:無交換
int flag = 1;
for (int i = 0; i < arr.length && flag == 1; i--) {
flag = 0;
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = 1;
}
}
}
return arr;
}
6. 快速排序
- 算法思想:
在序列中任意選擇一個元素(通常稱爲分界元素或元素),把小於或等於基準的所有元素都移到基準的前面,把大於基準的所有元素都移到基準的後面, 這樣,當前序列就被劃分成前後兩個子序列,其中前一個子序列中的所有元素都小於後一個子序列的所有元素,並且基準正好處於排序的最終位置上。然後分別對這兩個子序列遞歸 地進行上述排序過程,直到所有元素都處於排序的最終位置上,排序結束。
快速排序的本質:每趟排序將選擇的基準放到正確的位置。 - 算法實現:
/**
* 快速排序算法實現
*
* @param arr 待排序序列
* @return
*/
private static int[] quickSort(int[] arr, int low, int high) {
if (low < high) {
int pivotIndex = getPivotIndex(arr, low, high);
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
return arr;
}
/**
* 快速排序算法 -- 確定基準正確位置
*
* @param arr
* @param low
* @param high
* @return
*/
private static int getPivotIndex(int[] arr, int low, int high) {
// 獲取基準數據
int pivot = arr[low];
while (low < high) {
// 尾指針向前遍歷,當後面元素大於等於基準,high--
if (low < high && arr[high] >= pivot) {
high--;
}
arr[low] = arr[high];
// 頭指針向後遍歷,當前面元素小於等於基準,low++
if (low < high && arr[low] <= pivot) {
low++;
}
arr[high] = arr[low];
}
// 當頭指針和尾指針重合時,這個位置便是這一趟排序基準的正確位置
arr[low] = pivot;
// 返回基準的正確位置
return low;
}
7. 歸併排序
和選擇排序一樣,歸併排序的性能不受輸入數據的影響,但表現比選擇排序好的多,因爲始終都是O(n log n)的時間複雜度。代價是需要額外的內存空間。
-
算法思想:
歸併排序是建立在歸併操作上的一種有效的排序算法。該算法是採用分治法的一個非常典型的應用。歸併排序算法有兩個基本的操作,一個是分,也就是把原數組劃分成兩個子數組的過程。另一個是治,它將兩個有序數組合併成一個更大的有序數組。將數組平均分成兩部分: center = (left + right)/2,當數組分得足夠小時—數組中只有一個元素時,只有一個元素的數組自然而然地就可以視爲是有序的,此時就可以進行合併操作了。因此,上面講的合併兩個有序的子數組,是從 只有一個元素 的兩個子數組開始合併的。
8. 計數排序
-
算法思想:
計數排序不是基於比較的排序算法,其核心在於將輸入的數據值轉化爲存儲在額外開闢的數組空間中。 作爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是。當輸入的元素是 n 個0到k之間的整數時,它的運行時間是 O(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序算法。由於用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於數據範圍很大的數組,需要大量時間和內存。
-
算法演示:
/**
* 計數排序算法實現
*
* @param arr
* @return
*/
public static int[] countingSort(int[] arr) {
if (arr.length == 0) {
return arr;
}
// min,max:確定用於計數的數組的大小,計數數組大小爲:(max - min + 1)
int min = arr[0], max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] < min) {
min = arr[i];
}
if (arr[i] > max) {
max = arr[i];
}
}
int[] bucket = new int[max - min + 1];
// 統計序列中各個元素出現的次數count
for (int i = 0; i < arr.length; i++) {
int bias = arr[i] - min;
// 第 i 個元素頻次加1
bucket[bias]++;
}
// 把計數數組統計好的數據彙總到原數組
int index = 0;
for (int i = 0; i < bucket.length; i++) {
for (int j = 0; j < bucket[i]; j++) {
arr[index] = min + i;
index++;
}
}
return arr;
}
9. 桶排序
- 算法思想:
桶排序就是把最大值和最小值之間的數進行瓜分,例如分成 10 個區間,10個區間對應10個桶,我們把各元素放到對應區間的桶中去,再對每個桶中的數進行排序,可以採用歸併排序,也可以採用快速排序之類的。
之後每個桶裏面的數據就是有序的了,我們在進行合併彙總。 - 算法演示:
10. 基數排序
-
算法思想:
基數排序也是非比較的排序算法,對每一位進行排序,從最低位開始排序,時間複雜度爲O(kn), n爲數組長度,k爲數組中的數的最大的位數;基數排序是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。基數排序基於分別排序,分別收集,所以是穩定的。
-
算法實現:
/**
* 基數排序算法實現
*
* @param arr 待排序序列
* @return
*/
public static int[] radioSort(int[] arr) {
if (arr == null || arr.length < 2) {
return arr;
}
int n = arr.length;
int max = arr[0];
// 找出最大值
for (int i = 1; i < n; i++) {
max = Math.max(max, arr[i]);
}
// 計算最大值的位數
int num = 1;
while (max / 10 > 0) {
num++;
max = max / 10;
}
// 創建10個桶
ArrayList<LinkedList<Integer>> bucketList = new ArrayList<>(10);
// 初始化桶
for (int i = 0; i < 10; i++) {
bucketList.add(new LinkedList<Integer>());
}
// 進行每一趟的排序,從個位數開始排
for (int i = 1; i <= num; i++) {
for (int j = 0; j < n; j++) {
// 獲取每個數最後第 i 位是數組
int radio = (arr[j] / (int) Math.pow(10, i - 1)) % 10;
//放進對應的桶裏
bucketList.get(radio).add(arr[j]);
}
//合併放回原數組
int k = 0;
for (int j = 0; j < 10; j++) {
for (Integer t : bucketList.get(j)) {
arr[k++] = t;
}
//取出來合併了之後把桶清光數據
bucketList.get(j).clear();
}
}
return arr;
}