记录下 JavaScript 版本十个常用的排序算法。
算法名称 | 时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
冒泡排序 | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n) - O(n^2) | O(1) | 稳定 |
希尔排序 | O(n log n) | O(1) | 不稳定 |
快速排序 | O(n log n) | O(log n) | 不稳定 |
三向快速排序 | O(n) - O(n log n) | O(log n) | 不稳定 |
归并排序 | O(n log n) | O(n) | 稳定 |
堆排序 | O(n log n) | O(1) | 不稳定 |
计数排序 | O(n + k) | O(k) | 稳定 |
基数排序 | O(n + k) | O(n + k) | 稳定 |
桶排序 | O(n * k) | O(n * k) | 稳定 |
冒泡排序
- 每一次比较满足比较结果,都会替换位置
function bubbleSort(arr) {
let l = arr.length
for(let i = 0; i < l - 1; i++) {
for(let j = 0; j < l - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
}
选择排序
- 比冒泡排序好一点,去与后面未排序的值比较,每一次循环都记录最大值/最小值,在循环结束只会替换一次位置
function selectionSort(arr) {
let l = arr.length
let minIndex = 0
for(let i = 0; i < l - 1; i++) {
minIndex = i
for(let j = i + 1; j < l; j++) {
if (arr[minIndex] > arr[j]) {
minIndex = j
}
}
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
}
}
插入排序
- 与选择排序相反,去与前面已排序的值进行比较,每一次满足比较条件都会替换位置
function insertSort(arr) {
let l = arr.length
for(let i = 1; i < l; i++) {
for(let j = i; j > 0; j--) {
if (arr[j - 1] > arr[j]) {
[arr[j - 1], arr[j]] = [arr[j], arr[j - 1]]
}
}
}
}
希尔排序
- 希尔排序是插入排序的增强版插入排序,插入排序的颗粒度固定为1,而希尔排序的颗粒度是从 N 到 1 的过程。
function shellSort(arr) {
let len = arr.length
let h = 1
while(h < len / 3) h = 3 * h + 1 // 1, 4, 13, 40, 121, 364, 1093
while(h >= 1) {
// 将数组变为h有序
for(let i = h; i < len; i++) {
for(let j = i; j >= h; j -= h) {
if (arr[j] < arr[j - h]) {
[arr[j - h], arr[j]] = [arr[j], arr[j - h]]
}
}
}
h = Math.floor(h / 3)
}
}
快速排序
- 选择一个基准值,比较循环区间内的数据,将比基准值小的移到左侧,大的移到右侧
- 二切分会对重复的值进行再次比较
- 三切分不会对重复的值进行比较
二切分快速排序
function quickSort(arr, l, r) {
if (l >= r) return
let base = arr[l]
let i = l
let j = r + 1
while(true) {
while(i <= r && arr[++i] < base) {}
while(l <= j && arr[--j] > base) {}
if (i >= j) break;
[arr[i], arr[j]] = [arr[j], arr[i]]
};
[arr[l], arr[j]] = [arr[j], arr[l]]
quickSort(arr, l, j - 1)
quickSort(arr, j + 1, r)
}
三切分快速排序
function sQuickSort(arr, l, r) {
if (l >= r) return
let lf = l
let ri = r
let base = arr[lf]
let i = l + 1
while(i <= ri) {
if (base > arr[i]) {
[arr[lf++], arr[i++]] = [arr[i], arr[lf]]
} else if (base < arr[i]) {
[arr[i], arr[ri--]] = [arr[ri], arr[i]]
} else {
i++
}
}
sQuickSort(arr, l, lf - 1)
sQuickSort(arr, ri + 1, r)
}
快速排序是一个不稳定的排序方法,排序的好坏结果取决于原数组值的位置,快速排序最好的结果应该是每次切分都正好在最中间。所以可以在排序前打乱一下数组循序再排序:
for (let i = arr.length - 1; i > 0; i--) {
let random = Math.random() * i | 0;
[arr[i], arr[random]] = [arr[random], arr[i]]
}
归并排序
- 核心是对两个有序数组合并成一个新的有序数组,所以可以将一个无序数组分割成最小粒度(每个子数组1个元素),逐步将整个数组排成有序。
function mergeSort(arr) {
let aux = new Array(arr.length)
function merge(a, l, m, r) {
let i = l, j = m + 1
for (let k = l;k <= r; k++) {
aux[k] = a[k]
}
for (let k = l; k <= r; k++) {
if (i > m || aux[j] < aux[i]) {
a[k] = aux[j++]
} else {
a[k] = aux[i++]
}
}
return a
}
// 自顶向下
function sort_down(a, l, r) {
if (l >= r) return
let m = (l + r) >> 1
sort_down(a, l, m) // 左边排序
sort_down(a, m + 1, r) // 右边排序
if (a[m] > a[m + 1]) {
merge(a, l, m, r) // 合并
}
}
// 自底向上
function sort_up(a) {
let n = a.length
for (let i = 1; i < n; i += i) {
for (let j = 0; j < n - i; j += i + i) {
merge(a, j, i + j - 1, Math.min(j + i + i - 1, n - 1))
}
}
}
// sort_down(arr, 0, arr.length - 1)
// sort_up(arr, 0, arr.length - 1)
}
堆排序
- 利用有序堆的特性:堆顶元素必定是整个堆的最大/最小值,依此求出整个队列的有序性
// 下沉
function sink(arr, k, len) {
while(2 * k + 1 < len) {
let j = 2 * k + 1
if (j < len - 1 && arr[j] < arr[j + 1]) j++
if (arr[k] >= arr[j]) break
[arr[k], arr[j]] = [arr[j], arr[k]]
k = j
}
}
// 上浮
function swim(arr, len) {
let p = 0 // 父级节点
while(len > 0) {
p = (len - 1) >> 1
// (len & 1) 为0的情况下是有兄弟节点的,选出最大的与父级节点比较
if ((len & 1) === 0 && arr[len] < arr[len - 1]) len--
if (arr[len] <= arr[p]) break
[arr[len], arr[p]] = [arr[p], arr[len]]
len = p
}
}
function heapSort(arr) {
let len = arr.length
// 建立一个有序的堆
for (let i = (len - 1) >> 1; i >= 0; i--) {
sink(arr, i, len)
}
// 每次将堆顶元素与堆尾元素进行替换,再进行堆顶元素的下沉且堆长度减一,以此可以达到整体有序
while(len--) {
[arr[0], arr[len]] = [arr[len], arr[0]]
sink(arr, 0, len)
}
}
计数排序
- 计数排序的思想是直接以数组的值作为一个新数组的下标,然后按下标顺序取出一次放入新数组
function countingSort(arr) {
let maxValue = Math.max(...arr)
let bucket = new Array(maxValue + 1).fill(0)
let sortedIndex = 0
for (var i = 0; i < arr.length; i++) bucket[arr[i]]++;
for (var j = 0; j < maxValue + 1; j++) {
while(bucket[j]) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}
}
基数排序
- 对数据的相同位级(个位,百位,千位...)进行排序
function radixSort(nums, n) {
// 最大二进制位32位,分两次排序,2的16进制最大数为65536,即:1 << 16
let cnt = new Array(65536).fill(0)
let temp = new Array(nums.length)
// 低16位排序
for (let i = 0; i < nums.length; i++) {
cnt[nums[i] & 0xffff] += 1
}
// 每一个下标的前缀和
for (let i = 1; i < 65536; i++) {
cnt[i] += cnt[i - 1]
}
// 把数字按照记录的下标放到临时数组
for (let i = nums.length - 1; i >= 0; i--) {
temp[--cnt[nums[i] & 0xffff]] = nums[i]
}
// 重置
cnt.fill(0)
// 高16位排序
for (let i = 0; i < temp.length; i++) {
cnt[(temp[i] & 0xffff0000) >> 16] += 1
}
for (let i = 1; i < 65536; i++) {
cnt[i] += cnt[i - 1]
}
for (let i = nums.length - 1; i >= 0; i--) {
nums[--cnt[(temp[i] & 0xffff0000) >> 16]] = temp[i]
}
}
桶排序
- 桶排序思路
- 先求出输入数据的最大值与最小值
- 计算桶的大小区间,然后计算需要桶的数量
- 循环输入数据,计算每个数据对应的桶ID,然后将数据放入对应的桶
- 循环桶,用插入排序对每个桶进行排序
- 最后将桶内的数据有序放入原数组中
function bucketSort(arr) {
if (arr.length === 0) {
return arr
}
let i
let minValue = arr[0]
let maxValue = arr[0]
for (i = 1; i < arr.length; i++) {
if (arr[i] < minValue) {
minValue = arr[i] // 输入数据的最小值
} else if (arr[i] > maxValue) {
maxValue = arr[i] // 输入数据的最大值
}
}
//桶的初始化
let bucketSize = (arr.length / 10 | 0) + 1
let bucketCount = ((maxValue - minValue) / bucketSize | 0) + 1
let buckets = new Array(bucketCount).fill().map(() => [])
//利用映射函数将数据分配到各个桶中
for (i = 0; i < arr.length; i++) {
let bucketIndex = (arr[i] - minValue) / bucketSize | 0
buckets[bucketIndex].push(arr[i])
}
let arrIndex = 0
for (i = 0; i < buckets.length; i++) {
insertSort(buckets[i]) // 使用插入排序,对每个桶进行排序
for (let j = 0; j < buckets[i].length; j++) {
arr[arrIndex++] = buckets[i][j]
}
}
return arr
}