JavaScript排序算法
各個算法複雜度
冒泡排序
單向冒泡
function bubbleSort(nums) {
for (let i = 0, len = nums.length; i < len - 1; i++) {
let mark = true
for (let j = 0; j < len - i - 1; j++) {
if (nums[j] > nums[j + 1]) {
[nums[j], nums[j + 1]] = [nums[j + 1], nums[j]]
mark = false
}
}
if (mark) return
}
}
雙向冒泡
function bubbleSort_TwoWays(nums) {
let low = 0
let len = nums.length
while (low < len) {
let mark = true
for (let i = low; i < len; i++) {
if (nums[i] > nums[i + 1]) {
[nums[i], nums[i + 1]] = [nums[i + 1], nums[i]]
mark = false
}
}
len--
for (let i = len; i > low; i--) {
if (nums[i] > nums[i - 1]) {
[nums[i], nums[i - 1]] = [nums[i - 1], nums[i]]
mark = false
}
}
low++
if (mark) return
}
}
選擇排序
和冒泡排序相似, 區別在於選擇排序是將每一個元素和它後面的元素進行比較和交換。
數據規模越小越好
function selectSort(nums) {
for (let i = 0, len = nums.length;i<len; i++) {
for (let j = i + 1; j < len; j++) {
if (nums[i] > nums[j]) {
[nums[i], nums[j]] = [nums[j], nums[i]]
}
}
}
}
插入排序
以第一個元素作爲有序數組,其後的元素通過在這個已有序的數組中找到合適的位置並插入。
function insertSort(nums) {
for(let i=1,len=nums.length;i<len;i++){
let temp = nums[i]
let j=i
while(j>=0&&temp<nums[j-1]){
nums[j]=nums[j-1]
j--
}
nums[j]=temp
}
}
希爾排序
希爾排序是插入排序的一種更高效率的實現。
它與插入排序的不同之處在於, 它會優先比較距離較遠的元素。
希爾排序的核心在於間隔序列的設定。 既可以提前設定好間隔序列, 也可以動態的定義間隔序列。
function shellSort(nums) {
let len = nums.length
// 初始步數
let gap = parseInt(len / 2)
// 逐漸縮小步數
while (gap) {
// 從第gap個元素開始遍歷
for (let i = gap; i < len; i++) {
// 逐步其和前面其他的組成員進行比較和交換
for (let j = i - gap; j >= 0; j -= gap) {
if (nums[j] > nums[j + gap]) {
[nums[j], nums[j + gap]] = [nums[j + gap], nums[j]]
} else {
break
}
}
}
gap = parseInt(gap / 2)
}
}
快速排序
- 從數組中選擇一個元素作爲基準點
- 排序數組,所有比基準值小的元素擺放在左邊,而大於基準值的擺放在右邊。每次分割結束以後基準值會插入到中間去。
- 最後利用遞歸,將擺放在左邊的數組和右邊的數組在進行一次上述的1和2操作。
方式一:
- 缺點:
- 首先我們每次執行都會使用到兩個數組空間, 產生空間複雜度。
- concat操作會對數組進行一次拷貝, 而它的複雜度也會是O(n)
- 對大量數據的排序來說相對會比較慢
- 優點
- 代碼簡單明瞭, 可讀性強, 易於理解
- 非常適合用於面試筆試題
function quickSort(arr) {
if (arr.length <= 1) {
return arr
}
let pivotIndex = Math.floor(arr.length / 2);
let pivot = arr.splice(pivotIndex, 1)[0];
let left = []
let right = []
for (var i = 0; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
return quickSort(left).concat([pivot], quickSort(right))
}
方式二:(優化)
/**
* 分割
* @param {*} A 數組
* @param {*} p 起始下標
* @param {*} r 結束下標 + 1
*
*/
function divide(A, p, r) {
// 基準點
const pivot = A[r - 1]
// i初始化是-1,也就是起始下標的前一個
let i = p - 1
// 循環
for (let j = p; j < r - 1; j++) { // 如果比基準點小就i++,然後交換元素位置
if (A[j] <= pivot) {
i++
[A[i], A[j]] = [A[j], A[i]]
}
}
// 最後將基準點插入到i+1的位置
[A[i + 1], A[r - 1]] = [A[r - 1], A[i + 1]]
// 返回最終指針i的位置
return i + 1
}
//排序
function qsort(A, p = 0, r) {
r = r || A.length
if (p < r - 1) {
const q = divide(A, p, r);
qsort(A, p, q)
qsort(A, q + 1, r)
}
return A
}
歸併排序
歸併排序是一種非常穩定的排序方法,它的時間複雜度無論是平均,最好,最壞都是NlogN。
- 先拆分,一直拆分到只有一個數
- 拆分完成後,開始遞歸合併
function mergeSort(nums) {
// 有序合併兩個數組
function merge(l1, r1, l2, r2) {
let arr = []
let index = 0
let i = l1
j = l2
while (i <= r1 && j <= r2) {
arr[index++] = nums[i] < nums[j] ? nums[i++] : nums[j++]
}
while (i <= r1) arr[index++] = nums[i++]
while (j <= r2) arr[index++] = nums[j++]
// 將有序合併後的數組修改回原數組
for (let t = 0; t < index; t++) {
nums[l1 + t] = arr[t]
}
}
// 遞歸將數組分爲兩個序列
function recursive(left, right) {
if (left >= right) return
// 比起(left+right)/2,更推薦下面這種寫法,可以避免數溢出
let mid = parseInt((right - left) / 2) + left
recursive(left, mid)
recursive(mid + 1, right)
merge(left, mid, mid + 1, right)
return nums
}
recursive(0, nums.length - 1)
}
桶排序
取 n 個桶, 根據數組的最大值和最小值確認每個桶存放的數的區間, 將數組元素插入到相應的桶裏, 最後再合併各個桶。
圖解:
function bucketSort(nums) {
// 桶的個數,只要是正數即可
let num = 5
let max = Math.max(...nums)
let min = Math.min(...nums)
// 計算每個桶存放的數值範圍,至少爲1,
let range = Math.ceil((max - min) / num) || 1
// 創建二維數組,第一維表示第幾個桶,第二維表示該桶裏存放的數
let arr = Array.from(Array(num)).map(() => Array().fill(0))
nums.forEach(val => {
// 計算元素應該分佈在哪個桶
let index = parseInt((val - min) / range)
// 防止index越界,例如當[5,1,1,2,0,0]時index會出現5
index = index >= num ? num - 1 : index
let temp = arr[index]
// 插入排序,將元素有序插入到桶中
let j = temp.length - 1
while (j >= 0 && val < temp[j]) {
temp[j + 1] = temp[j]
j--
}
temp[j + 1] = val
})
// 修改回原數組
let res = [].concat.apply([], arr)
nums.forEach((val, i) => {
nums[i] = res[i]
})
}
基數排序
使用十個桶 0 - 9, 把每個數從低位到高位根據位數放到相應的桶裏, 以此循環最大值的位數次。
但只能排列正整數, 因爲遇到負號和小數點無法進行比較。
function radixSort(nums) {
// 計算位數
function getDigits(n) {
let sum = 0;
while (n) {
sum++;
n = parseInt(n / 10);
}
return sum;
}
// 第一維表示位數即0-9,第二維表示裏面存放的值
let arr = Array.from(Array(10)).map(() => Array());
let max = Math.max(...nums);
let maxDigits = getDigits(max);
for (let i = 0, len = nums.length; i < len; i++) {
// 用0把每一個數都填充成相同的位數
nums[i] = (nums[i] + '').padStart(maxDigits, 0);
// 先根據個位數把每一個數放到相應的桶裏
let temp = nums[i][nums[i].length - 1];
arr[temp].push(nums[i]);
}
// 循環判斷每個位數
for (let i = maxDigits - 2; i >= 0; i--) {
// 循環每一個桶
for (let j = 0; j <= 9; j++) {
let temp = arr[j]
let len = temp.length;
// 根據當前的位數i把桶裏的數放到相應的桶裏
while (len--) {
let str = temp[0];
temp.shift();
arr[str[i]].push(str);
}
}
}
// 修改回原數組
let res = [].concat.apply([], arr);
nums.forEach((val, index) => {
nums[index] = +res[index];
})
}
計數排序
計數排序的核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。
作爲一種線性時間複雜度的排序, 計數排序要求輸入的數據必須是有確定範圍的整數。
function countingSort(nums) {
let arr = []
let max = Math.max(...nums)
let min = Math.min(...nums)
// 加上最小值的相反數來縮小數組範圍
let add = -min
for (let i = 0, len = nums.length; i < len; i++) {
let temp = nums[i]
temp += add
arr[temp] = arr[temp] + 1 || 1
}
let index = 0
for (let i = min; i <= max; i++) {
let temp = arr[i + add]
while (temp > 0) {
nums[index++] = i
temp--
}
}
}
堆排序
堆排序可以說是一種利用堆的概念來排序的選擇排序。 分爲兩種方法:
- 大頂堆: 每個節點的值都大於或等於其子節點的值, 在堆排序算法中用於升序排列
- 小頂堆: 每個節點的值都小於或等於其子節點的值, 在堆排序算法中用於降序排列
function heapSort(nums) {
// 調整最大堆,使index的值大於左右節點
function adjustHeap(nums, index, size) {
// 交換後可能會破壞堆結構,需要循環使得每一個父節點都大於左右結點
while (true) {
let max = index
let left = index * 2 + 1 // 左節點
let right = index * 2 + 2 // 右節點
if (left < size && nums[max] < nums[left]) max = left
if (right < size && nums[max] < nums[right]) max = right
// 如果左右結點大於當前的結點則交換,並再循環一遍判斷交換後的左右結點位置是否破壞了堆結構(比左右結點小了)
if (index !== max) {
[nums[index], nums[max]] = [nums[max], nums[index]]
index = max
} else {
break
}
}
}
// 建立最大堆
function buildHeap(nums) {
// 注意這裏的頭節點是從0開始的,所以最後一個非葉子結點是 parseInt(nums.length/2)-1
let start = parseInt(nums.length / 2) - 1
let size = nums.length
// 從最後一個非葉子結點開始調整,直至堆頂。
for (let i = start; i >= 0; i--) {
adjustHeap(nums, i, size)
}
}
buildHeap(nums)
// 循環n-1次,每次循環後交換堆頂元素和堆底元素並重新調整堆結構
for (let i = nums.length - 1; i > 0; i--) {
[nums[i], nums[0]] = [nums[0], nums[i]]
adjustHeap(nums, 0, i)
}
}