0. 算法複雜度
排序算法 | 時間複雜度(平均) | 空間複雜度 | 穩定性 |
---|---|---|---|
冒泡排序 | O(n2) | O(1) | 穩定 |
快速排序 | O(nlog2n) | O(log2n) | 不穩定 |
簡單插入排序 | O(n2) | O(1) | 穩定 |
shell 排序 | O(n1.3) | O(1) | 不穩定 |
簡單選擇排序 | O(n2) | O(1) | 不穩定 |
堆排序 | O(nlog2n) | O(1) | 不穩定 |
二路歸併排序 | O(nlog2n) | O(n) | 穩定 |
計數排序 | O(n + k) | O(n + k) | 穩定 |
桶排序 | O(n + k) | O(n + k) | 穩定 |
基數排序 | O(n * k) | O(n + k) | 穩定 |
TIP:
以下算法的說明與實現,均以升序排列爲例。
1. 冒泡排序
基本思想:
- 從數組第一個元素開始,重複比較前後相鄰的 2 個數組元素,如果前面一個元素大於後面一個元素,則交換 2 個元素的位置;
- 經過步驟 1 描述的一輪循環後,無序數列中的最大值將被放置在無序數列的末尾(此時,該值已有序)。至此,無序數列長度減 1;
- 對無序數列重複步驟 1、2,直至整個數列有序(無序數列長度爲 0)。
複雜度分析:
- 時間複雜度:
循環最內層語句的執行次數爲:(n - 1)*(n - 1 - i)。保留最高指數,得到時間複雜度爲:O(n2)。
- 空間複雜度:
該算法中,對數組的操作都是就地操作(in-place),沒有用到額外的存儲空間,因此空間複雜度爲:O(1)。
- 穩定性
在對相鄰數據進行比較時,只有在 arr[i] > arr[j](i > j) 的情況下,才進行交互位置的操作,這樣就可以保證相等的兩個元素在完成比較後,依舊保留原本的前後相對位置。因此,冒泡排序是穩定的。
代碼實現:
// 冒泡排序(升序)
function bubbleSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
bubbleSort([3, 1, 6, 5, 7]);
2. 快速排序
基本思想:
- 選擇數列中的一個元素(在這選擇待比較數列第一個元素)作爲基準(pivot);
- 把小於基準值的元素放到基準的左邊,把大於基準值的元素放到基準的右邊,與基準值大小相等的元素放置在左邊還是右邊都可以,這一操作稱爲分區(partition)。至此,可以得到左邊和右邊的 2 個子數列;
- 對得到的子數列重複進行步驟 1、2,直至每個分區操作的數列都只剩下一個元素,這時候,整個數列就有序了。
複雜度分析:
時間複雜度:
對應一個長度爲 n 的數組,需要進行 log2n 次分區操作;完成每次分區操作,需要對每個數據都進行比較操作,總的要進行 n 次比較。因此,快速排序的時間複雜度爲:O(nlog2n)。
空間複雜度:
對數組元素的操作是就地操作,不會消耗額外的存儲空間。因此,快速排序對存儲空間的消耗就只有 log2n 次的分區操作了。所以,快速排序的空間複雜度爲:O(log2n)。
穩定性:
對於數組 [2,5,5,3,1,7],使用快速排序來排序後,2 個 5 的前後相對位置變了。因此,快速排序是不穩定的。
代碼實現:
function exchange(arr, i, j) {
[arr[i], arr[j]] = [arr[j], arr[i]];
}
function partition(arr, left, right) {
let pivot = arr[left];
let pivotIndex = left;
if (left >= right) {
return;
}
while (left < right) {
while (arr[right] > pivot && left < right) {
right--;
}
while (arr[left] <= pivot && left < right) {
left++;
}
if (left < right) {
exchange(arr, left, right);
}
}
// left === right
exchange(arr, pivotIndex, left);
return left;
}
function quickSort(arr, left, right) {
left = typeof left !== 'number' || Number.isNaN(left) ? 0 : left;
right = typeof right !== 'number' || Number.isNaN(right) ? arr.length - 1 : right;
if (left < right) {
let pivotIndex = partition(arr, left, right);
quickSort(arr, left, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, right);
}
return arr;
}
quickSort([3, 1, 2]);
3. 簡單插入排序
基本思想:
- 默認地,將數組中的第一個元素作爲有序數組, 剩下的元素作爲無序數組;
- 從後往前遍歷有序數組,找到合適的位置,插入無序數組的首個元素。至此,有序數組中增加一個元素,無序數組中減少一個元素;
- 重複進行步驟 2,直至無序數組中的全部元素都插入到了有序數組中。
複雜度分析:
- 時間複雜度:
算法中出現了嵌套 2 層的循環。因此,簡單插入排序的時間複雜度爲:O(n2)。
- 空間複雜度:
算法中對數組是就地操作的,沒有消耗額外的空間。因此,簡單插入排序的空間複雜度爲:O(1)。
- 穩定性:
遍歷有序數組的過程中,是將比待插入數組元素大的元素向後移動,否則就將待插入元素插至比較元素之後(這樣的話,滿足:比較元素 <= 待插入元素),這樣的話,可以保證相同元素之間前後相對順序。因此,插入排序是穩定的。
代碼實現:
function insertionSort(arr) {
// 遍歷無序數列
for (let i = 1; i < arr.length; i++) {
// 待插入的一個無序數據
let current = arr[i];
// 默認第一個數據爲有序的
let j = i - 1;
// 從後往前遍歷有序數列
while (j >= 0 && current < arr[j]) {
// 將有序數列中大於無序數據的部分從後往前依次向後移動
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = current;
}
return arr;
}
insertionSort([3, 1, 6, 5, 7]);
4. shell 排序(縮小增量排序)
基本思想:
- 選擇一個增量序列t1,t2,…,tk,其中 ti > tj,tk = 1;
- 按增量序列個數 k,對序列進行 k 趟排序;
- 每趟排序,根據對應的增量ti,將待排序列分割成若干長度爲m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲1 時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。
複雜度分析:
- 時間複雜度:
與增量序列的選擇有關,太複雜了,本人是推不出,就記一個 O(n1.3) 好了。
- 空間複雜度:
在這個算法中,對數組的操作都是就地完成的,不會消耗額外的存儲空間。因此,shell 排序的空間複雜度爲:O(1)。
- 穩定性:
不穩定
代碼實現:
function shellSort(arr) {
for (let gap = Math.floor(arr.length / 2); gap > 0; gap = Math.floor(gap / 2)) {
for (let i = gap; i < arr.length; i++) {
let current = arr[i];
let j = i;
while (j - gap >= 0 && arr[j - gap] > arr[j]) {
arr[j - gap + 1] = arr[j - gap];
j = j - gap;
}
arr[j] = current;
}
}
return arr;
}
shellSort([3, 1, 6, 5, 7]);
5. 簡單選擇排序
基本思想:
- 初始時,認爲:數組中有序數列爲空,無序數列爲整個數組。
- 繼續遍歷無序數列,找出其中的最小值,並把它與有序數列的末尾後面的一個元素交換位置;
- 重複進行步驟 2,經過 n - 1 輪的循環後,整個數組就有序了。
換成一句話概況: 選擇未排序的數列中的最小值,放在未排序數列的首位。
複雜度分析:
- 時間複雜度:
2 層嵌套的循環。因此,選擇排序的時間複雜度爲:O(n2)。
- 空間複雜度:
數組是就地操作的。因此,選擇排序的空間複雜度爲:O(1)。
- 穩定性:
對於數組 [5, 8, 5, 2, 9],經過選擇排序進行排序之後,2 個 5 的前後相對順序就被破壞了。因此,選擇排序是不穩定的。
代碼實現:
function selectionSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
let minIndex = i;
for (let j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
}
return arr;
}
selectionSort([3, 1, 6, 5, 7, 6]);
6. 堆排序
基本思想:
- 將初始待排序關鍵字序列 (R1,R2….Rn) 構建成大頂堆,此堆爲初始的無序區;
- 將堆頂元素R[1]與最後一個元素R[n]交換,此時得到新的無序區 (R1,R2,……Rn-1) 和新的有序區 (Rn) ,且滿足 R[1,2…n-1]<=R[n] ;
- 由於交換後新的堆頂 R[1] 可能違反大頂堆的性質,因此需要對當前無序區 (R1,R2,……Rn-1) 調整爲新堆;
- 然後再次將 R[1] 與無序區最後一個元素交換,得到新的無序區 (R1,R2….Rn-2) 和新的有序區 (Rn-1,Rn) 。不斷重複此過程直到有序區的元素個數爲 n-1 ,則整個排序過程完成。
複雜度分析:
- 時間複雜度:
O(nlog2n)
- 空間複雜度:
數組是就地操作的。因此,選擇排序的空間複雜度爲:O(1)。
- 穩定性:
不穩定
代碼實現:
function exchange(arr, i, j) {
[arr[i], arr[j]] = [arr[j], arr[i]];
}
function shiftDown(arr, i, length) {
for (let j = 2 * i + 1; j < length; j = 2 * j + 1) {
if (j + 1 < length && arr[j] < arr[j + 1]) {
j++;
}
if (arr[j] > arr[i]) {
exchange(arr, i, j);
i = j;
} else {
break;
}
}
}
function heapSort(arr) {
// 從下至上,從右至左將數據初始化爲大頂堆
for (let i = Math.floor(arr.length / 2 - 1); i >= 0; i--) {
// i 爲非葉子節點
shiftDown(arr, i, arr.length);
}
// 將棧頂元素與未排序數列最後一個數據交換,交換後需要將剩下數據組成的數調整爲大頂堆
for (let i = arr.length - 1; i > 0; i--) {
exchange(arr, 0, i);
shiftDown(arr, 0, i);
}
return arr;
}
heapSort([4, 6, 8, 5, 9, 1, 2, 5, 3, 2]);
7. 二路歸併排序
基本思想:
- 將一個長度爲 n 的數組分割爲 2 個長度爲 n / 2 的數組;
- 對 2 個數組分別遞歸地調用歸併排序;
- 將排好序的 2 個數組合併爲 1 個數組。
複雜度分析:
- 時間複雜度:
數列的歸併樹高度爲 log2n,每層總的需要進行約 n 次比較。因此,歸併排序的時間複雜度爲 O(nlog2n)。
- 空間複雜度:
代碼執行過程中,需要一個長度爲 n 的數組來存儲排序結果。因此,歸併排序的空間複雜度爲:O(n)。
- 穩定性:
合併排序好的 2 個數組的時候,判斷條件 left[0] <= right[0] 爲 true 時,說明 left 數組和 right 數組的首元素相同,這時候 選擇先將 left 數組的首元素移入結果數組中, 就能保證這 2 個相同元素的前後相對位置與原來的一致。因此,歸併排序是穩定的。
代碼實現:
function merge(left, right) {
let result = [];
while (left.length > 0 && right.length > 0) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
return result.concat(left, right);
}
function mergeSort(arr) {
if (arr.length <= 1) {
return arr;
}
let middleIndex = Math.floor(arr.length / 2);
return merge(mergeSort(arr.slice(0, middleIndex)), mergeSort(arr.slice(middleIndex)));
}
mergeSort([3, 1, 6, 5, 7]);
8. 計數排序
基本思想:
- 找到數組中的最大的元素,記爲 maxValue;
- 創建一個長度爲 maxValue + 1 的數據,用作後續的計數數組 countingArr;
- 遍歷數組 arr,累計 countArr[arr[i]] 的值;
- 遍歷 countingArr,反向填充得到結果數組 result。
當k不是很大並且序列比較集中時,計數排序是一個很有效的排序算法
複雜度分析:
將數組的最大值記爲 k
- 時間複雜度:
n + (n + k) => O(n + k)
- 空間複雜度:
算法需要創建一個長度爲 n + k 的計數數組 和一個長度爲 n 的結果數組。因此,空間複雜度爲 O(n + k)。
- 穩定性:
該算法不會改變原數組的元素的位置。因此,計數排序是穩定的。
代碼實現:
function countingSort(arr, maxValue) {
maxValue = maxValue || Math.max(...arr);
let countingArr = new Array(maxValue + 1);
let result = [];
for (let i = 0; i < arr.length; i++) {
if (!countingArr[arr[i]]) {
// 初始化鍵值對應的計數值
countingArr[arr[i]] = 0;
}
// 鍵值對應的數據出現,計數值加一
countingArr[arr[i]]++;
}
for (let i = 0; i < countingArr.length; i++) {
while (countingArr[i] > 0) {
result.push(i);
countingArr[i]--;
}
}
return result;
}
countingSort([4, 6, 8, 5, 9, 1, 2, 5, 3, 2], 9);
9. 桶排序
基本思想:
- 將數據分配到桶中;
- 對桶中的數據進行排序;
- 將排序好的數據合併到結果數組中。
複雜度分析:
- 時間複雜度:
O(n + k)
- 空間複雜度:
O(n + k)
- 穩定性:
穩定,理由同計數排序。
代碼實現:
function insertionSort(arr) {
// 遍歷無序數列
for (let i = 1; i < arr.length; i++) {
// 待插入的一個無序數據
let current = arr[i];
// 默認第一個數據爲有序的
let j = i - 1;
// 從後往前遍歷有序數列
while (j >= 0 && current < arr[j]) {
// 將有序數列中大於無序數據的部分從後往前依次向後移動
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = current;
}
return arr;
}
function bucketSort(arr, size) {
let bucketSize = size || 5;
let result = [];
if (arr.length <= 1) {
return arr;
}
let minValue = arr[0];
let maxValue = arr[0];
// 找到數組中的最大值和最小值
for (let i = 1; i < arr.length; i++) {
if (minValue > arr[i]) {
minValue = arr[i];
} else if (maxValue < arr[i]) {
maxValue = arr[i];
}
}
let bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
let buckets = new Array(bucketCount);
// 初始化桶
for (let i = 0; i < bucketCount; i++) {
buckets[i] = [];
}
// 將數據分配到桶中
for (let i = 0; i < arr.length; i++) {
let bucketIndex = Math.floor((arr[i] - minValue) / bucketSize);
buckets[bucketIndex].push(arr[i]);
}
// 使用插入排序,對桶內數據進行排序
for (let i = 0; i < buckets.length; i++) {
insertionSort(buckets[i]);
// 將排序完成的數據放進 result 數組中
result = [...result, ...buckets[i]];
}
return result;
}
bucketSort([4, 6, 8, 5, 9, 1, 2, 5, 3, 2]);
10. 基數排序
基本思想:
- 取得數組中的最大數,並取得位數;
- arr爲原始數組,從最低位開始取每個位組成 radix 數組;
- 對 radix 進行計數排序(利用計數排序適用於小範圍數的特點);
複雜度分析:
- 時間複雜度:
O(n * k)
- 空間複雜度:
O(n + k)
- 穩定性:
穩定,理由同計數排序。
代碼實現:
let counter = [];
function radixSort(arr, maxDigit) {
let mod = 10;
let dev = 1;
for (let i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
for (let j = 0; j < arr.length; j++) {
let bucket = parseInt((arr[j] % mod) / dev);
if (counter[bucket] == null) {
counter[bucket] = [];
}
counter[bucket].push(arr[j]);
}
let pos = 0;
for (let j = 0; j < counter.length; j++) {
let value = null;
if (counter[j] != null) {
while ((value = counter[j].shift()) != null) {
arr[pos++] = value;
}
}
}
}
return arr;
}
radixSort([4, 6, 8, 5, 9, 1, 2, 5, 3, 2], 1);