十大排序算法
複雜度:
不穩定的排序:“快選希堆”
1. 直接插入排序
枚舉一個元素分別與前面的元素比較,直到遇到比自己更小的
function insertSort(arr) {
for (let i = 1; i < arr.length; i++) {
if (arr[i] < arr[i - 1]) {
let tmp = arr[i];
for (let j = i; j >= 0; j--) {
/* 前面的值還是大於tmp則繼續向前找 */
if (tmp < arr[j - 1]) {
arr[j] = arr[j - 1];
} else {
arr[j] = tmp; break;
}
}
}
}
}
2. 希爾排序
原理:將數組不斷分塊,將塊與塊的對應元素進行比較後排序,重複進行直到每塊只含有一個數,相當於間隔不爲1的直接插入排序
時間複雜度:平均O(n1.3),已經排好序的情況下O(n),最差O(n2);
空間複雜度:常數級空間O(1)
穩定性:由於多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂,所以,Shell排序是不穩定的。
function shellSort(arr) {
let len = Math.floor(arr.length / 2);
while (len) {
/* len 2*len 3*len ... */
for (let i = len; i < arr.length; i += len) {
if (arr[i] < arr[i - len]) {
let tmp = arr[i];
/* 查找前面可以插入的位置 */
for (let j = i; j >= 0; j -= len) {
if (tmp < arr[j - len]) {
arr[j] = arr[j - len];//後移一位
} else {
arr[j] = tmp; break;//找到可賦值的位置
}
}
}
}
/* 折半 */
len = Math.floor(len / 2);
}
}
/* 測試 */
let arr = [1, 39, 3, 2, 43, 23, 55];
shellSort(arr);
console.log(arr);// [1, 2, 3, 23, 39, 43, 55]
3. 簡單選擇排序
每次選出後面最小值放到當前位置
function selectSort(arr) {
for (let i = 0; i < arr.length; i++) {
let k = i;
// 找出後面最小值
for (let j = i + 1; j < arr.length; j++)
if (arr[j] < arr[k]) k = j;
//交換
[arr[k], arr[i]] = [arr[i], arr[k]];
}
}
4. 堆排序
原理:構建大頂堆,將堆頂放到數組最後,每次減少數組中用來建堆的元素個數
實現方法:
- 對數組區間
[0,n]
構建大頂堆,此時父元素都比子元素大 - 將第一個元素與最後一個元素互換,最後一個就是最大的
- 堆的元素個數減少1,更新第一個元素下的堆,形成新的大頂堆
- 重複2-3步驟,直到堆的元素個數爲0
時間複雜度:初始化執行n/2次heapify
,更新執行n-1次,每次heapfy最多更新logn次(堆的高度),時間複雜度O(nlogn)
空間複雜度:所需空間主要用於遞歸,爲O(logn)
穩定性:在堆頂與堆尾交換的時候兩個相等的記錄在序列中的相對位置就可能發生改變,所以堆排序是不穩定的
講解視頻:堆排序
/* 負責構建和更新堆 */
function heapify(arr, root, n) {
let max = root,
left = root * 2 + 1,
right = root * 2 + 2;
if (left < n && arr[left] > arr[max])
max = left;
if (right < n && arr[right] > arr[max])
max = right;
/* 存在子元素大於root則更新 */
if (root !== max) {
//交換值並更新max下的堆
[arr[root], arr[max]] = [arr[max], arr[root]];
heapify(arr, max, n);
}
}
/* 堆排序 */
function heapSort(arr) {
let n = arr.length;
/* 初始化,對含有子節點的節點構建*/
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
heapify(arr, i, n);
}
/* 首尾交換 */
[arr[0], arr[n - 1]] = [arr[n - 1], arr[0]];
/* 更新根被替換的堆 */
for (let i = n - 1; i > 0; i--) {
//每次替換的都是頭節點,所以更新頭節點,長度爲i
heapify(arr, 0, i);
[arr[0], arr[i - 1]] = [arr[i - 1], arr[0]];
}
}
/* 測試 */
let arr = [1, 39, 3, 2, 43, 23, 55];
heapSort(arr);
console.log(arr);// [1, 2, 3, 23, 39, 43, 55]
5. 冒泡排序
每輪將最大的數移動到最後即可
function bubbleSort(arr) {
let len = arr.length;
for (let i = 0; i < len; i++) {
/* 對[0,n-i-1]區域進行排序 */
for (let j = 0; j < len - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
/* 交換 */
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
}
6. 快速排序
方法1:
- 每次找一個值
p
作爲參考,設置左右指針i,j
從兩邊往中間遍歷 - 若左邊
i
位置遇到小於參考值停止遍歷,右邊j
位置遇到大於參考值停止遍歷 - 兩個停止的位置
i,j
進行交換數值 - 再次進行同樣的步驟,最終左右指針的交點
i==j
就是參考值的最終位置 - 將參考值與交點互換數值,然後對交點左邊區域
[start,i-1]
和右邊區域[i+1,end]
分別進行快速排序
方法2:
- 每次找一個值
p
作爲參考值,設置i指針記錄第一個大於等於p
的位置 - 設置值
j
遍歷數組,當遇到小於參考值p
的位置時,將第一個大於等於p
的值與arr[j]
互換,i
指向下一個位置 - 最終將
i
位置與p
位置對應值交換,再對兩邊區域進行快速排序
可以看出快速排序一般情況下分爲logn
層,平均時間複雜度O(nlogn)
,最壞是逆序的時候遞歸n
層時間複雜度O(n^2)
,空間複雜度主要爲logn
層遞歸佔用內存O(logn)
,最壞情況O(n)
,由於關鍵字的比較是跳躍性的,所以快速排序不穩定
/* 方法1:從兩邊掃描 */
function quickSort(arr, start, end) {
if (start >= end) return;
let i = start, j = end;
let p = arr[end];
while (i < j) {
while (i < j && arr[i] < p) i++;
while (j > i && arr[j] >= p) j--;
/* 交換i,j位置的值 */
[arr[j], arr[i]] = [arr[i], arr[j]];
}
/* 將p移動到最終位置 */
[arr[end], arr[i]] = [arr[i], arr[end]];
quickSort(arr, start, i - 1);
quickSort(arr, i + 1, end);
}
/* 方法2:一個指針負責遍歷,另一個負責第一個大於等於p的位置 */
function quickSort(arr, start, end) {
if (start >= end) return;
let p = arr[end];
let i = start;
for (let j = start; j < end; j++) {
// 遇到小於p的就放到前面,並且個數+1
if (arr[j] < p) {
[arr[i], arr[j]] = [arr[j], arr[i]];
i++;//此時[start,i-1]內的元素都小於p
}
}
[arr[i], arr[end]] = [arr[end], arr[i]];
quickSort(arr, start, i - 1);
quickSort(arr, i + 1, end);
}
/* 測試 */
let arr = [1, 39, 3, 2, 43, 23, 55]
quickSort(arr, 0, arr.length - 1);
console.log(arr);//[ 1, 2, 3, 23, 39, 43, 55 ]
7. 歸併排序
將長度爲len
的數組一直二分至長度爲1的多個數組,然後進行自下而上排序合併
從上圖可以看出歸併排序層數logn+1,每一層合併的代價是n,時間複雜度O(nlogn),額外空間就是那個輔助數組,所以空間複雜度O(n),在合併時如果相等則把前面的數放進輔助數組,所以是穩定的
function merge(arr, start, mid, end) {
let res = [];
let i = start, j = mid + 1;
/* 比較,將小的值放到數組 */
while (i <= mid && j <= end) {
if (arr[i] <= arr[j])
res.push(arr[i++]);
else
res.push(arr[j++]);
}
/* 剩餘的直接賦值 */
while (i <= mid) res.push(arr[i++]);
while (j <= end) res.push(arr[j++]);
/* 賦值到arr */
for (let i = 0; i <= end - start; i++) {
arr[start + i] = res[i];
}
}
function mergeSort(arr, start, end) {
if (start >= end) return;
else {
/* 求中間值,分爲兩段進行處理 */
let mid = Math.floor(start + end >> 1);
mergeSort(arr, start, mid);
mergeSort(arr, mid + 1, end);
/* 將處理結果合併 */
merge(arr, start, mid, end);
}
}
let arr = [1, 39, 3, 2, 43, 23, 55]
mergeSort(arr, 0, arr.length - 1);
console.log(arr);//[ 1, 2, 3, 23, 39, 43, 55 ]
8. 計數排序
不基於元素比較,利用數組下標來確定元素的正確位置。
function countSort(arr) {
let max = 0, count = [];
for (let i = 0; i < arr.length; i++) {
max = arr[i] > max ? arr[i] : max;
count[arr[i]] = count[arr[i]] ? (count[arr[i]] + 1) : 1;
}
let k = 0;
for (let i = 0; i <= max; i++) {
while (count[i]--) {
arr[k++] = i;//多個相同的數
}
}
}
9. 桶排序
桶排序是對計數排序的優化,將最小值到最大值之間的每一個位置申請空間,更新爲最小值到最大值之間每一個固定區域申請空間,儘量減少了元素值大小不連續情況下的空間浪費情況
時間複雜度:O(n+k) k是遍歷桶所需時間,n是遍歷數組所需時間
空間複雜度:O(n+k) k記錄桶,n爲桶裏的元素
function bucketSort(arr) {
let len = arr.length;
let max = Number.MIN_SAFE_INTEGER, min = Number.MAX_SAFE_INTEGER;
/* 遍歷數組求總區間 */
for (let i = 0; i < len; i++) {
max = arr[i] > max ? arr[i] : max;
min = arr[i] < min ? arr[i] : min;
}
/* 設置每個桶代表的區間長度 */
let range = Math.ceil((max - min) / len);
/* 將元素放置到對應桶 */
let buckets = [];
for (let i = 0; i < len; i++) {
/* 桶編號 */
let num = Math.floor((arr[i] - min) / range);
if (!buckets[num]) buckets[num] = [arr[i]];
else buckets[num].push(arr[i]);
}
/* 對桶裏的元素進行再次排序,然後放到原數組 */
let k = 0;
for (let i = 0; i <= range; i++) {
if (Array.isArray(buckets[i])) {
/* 排序 */
buckets[i].sort((x, y) => x - y);
/* 放置到原數組 */
buckets[i].forEach(v => arr[k++] = v);
}
}
}
10. 基數排序
將整數按位數切割成不同的數字,然後按每個位數分別比較。
時間複雜度:每一位都要遍歷一次數組,複雜度爲O(n*k),k爲最大位數
空間複雜度:桶的個數加上存的元素個數O(n+k)
穩定性:穩定,相同順序的值進入順序和出去不變
function getMaxDigit(arr) {
let max = 0;
for (let i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i].toString().length);
}
return max;
}
function radixSort(arr) {
let maxLen = getMaxDigit(arr);//最大位數
/* 根據第i位數字排序 */
for (let i = 1; i <= maxLen; i++) {
let dev = Math.pow(10, i - 1), buckets = [];
/* 遍歷數組,根據當前位的值加入到對應桶 */
for (let j = 0; j < arr.length; j++) {
/* 獲取第i位數值 */
let num = Math.floor(arr[j] / dev) % 10;
buckets[num] = buckets[num] ? buckets[num] : [];
buckets[num].push(arr[j]);
}
/* 原數組重新賦值 */
arr = [];
for (let j = 0; j < 10; j++) {
if (Array.isArray(buckets[j]))
buckets[j].forEach(v => arr.push(v))
}
}
return arr;
}
/* 測試 */
let arr = [1, 39, 3, 2, 43, 231, 55];
arr = radixSort(arr);
console.log(arr);//[1, 2, 3, 39, 43, 55, 231]