根据时间复杂度分类,常见的排序算法有:
- O(n^2): 冒泡、插入、选择;
- O(nlogn): 快排、归并;
- O(n): 桶、计数;
冒泡排序
- 时间复杂度: 平均O(n^2)
- 稳定性: 稳定
// 普通版
function bubbleSort(arr) {
let n = arr.length
for (let i = 0; i < n; i++) {
for (let j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j+1]) {
let temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
}
}
}
return arr
}
// 优化版
function bubbleSort(arr) {
const n = arr.length
if (n <= 1) return
for (const i = 0; i < n; i++) {
// 是否交换标志
let change = false
for (const j = 0; j < n-i-1; j++) {
if (arr[j] > arr[j+1]) {
[arr[j], arr[j+1]] = [arr[j+1], arr[j]]\
change = true
}
}
// 若没有修改,说明排序已经完成
if (!change) break
}
}
插入排序
- 时间复杂度: 平均O(n^2)
- 稳定性: 稳定
function insertSort(arr) {
let n = arr.length
if (n <= 1) return
for (let i = 1; i < n; i++) {
let temp = arr[i]
for (let j = i - 1; j >= 0; j--) {
// 大于插入元素,则后移
if (arr[j] > temp) {
arr[j+1] = arr[j]
} else { // 小于,则插入
arr[j+1] = temp
break
}
}
}
}
选择排序
- 时间复杂度: 平均O(n^2)
- 稳定性: 稳定
function selectSort(arr) {
let n = arr.length
if (n <= 1) return
for (let i = 0; i < n; i++) {
let min = i
for (let j = i + 1; j < n; j++) {
if (arr[j] < arr[min]) {
min = j
}
}
// 找到最小值,交换
[arr[min], arr[i]] = [arr[i], arr[min]]
}
}
快速排序
- 时间复杂度: 平均O(nlogn)
- 稳定性: 不稳定
// 基础版
function quickSort(arr) {
let n = arr.length
if (n <= 1) return arr
const left = [], right =[]
let temp = arr[0]
for (let i = 1; i < n; i++) {
if (arr[i] < temp) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
return [...quickSort(left), temp, ...quickSort(right)]
}
// 原地排序版
function quickSort(arr, start, end) {
if (start >= end) return
// 获取分区下标
const p = getPartition(arr, start, end)
quickSort(arr, start, p-1)
quickSort(arr, p+1, end)
}
// 获取分区下标
function getPartition(arr, start, end) {
const base = start // 第一个作为基数
let p = start + 1 // 分区下标
for (let i = p; i <= end; i++) {
if (arr[i] < arr[base]) {
// 小于基数值,交换位置
[arr[i], arr[p]] = [arr[p], arr[i]]
p++
}
}
// 交换基数与分区
p = p - 1
[arr[p], arr[base]] = [arr[base], arr[p]]
return p
}
// 获取分区下标
function getPartition2(arr, start, end) {
const base = end // 最后一个作为基数
let p = start // 分区下标
for (let i = p; i < end; i++) {
if (arr[i] < arr[base]) {
// 小于基数值,交换位置
[arr[i], arr[p]] = [arr[p], arr[i]]
p++
}
}
// 交换基数与分区
[arr[p], arr[base]] = [arr[base], arr[p]]
return p
}
归并排序
- 时间复杂度: 平均O(nlogn)
- 稳定性: 稳定
function mergeSort(arr, start, end) {
if (start >= end) return
// 分
const mid = Math.floor((start + end) / 2)
mergeSort(arr, start, mid)
mergeSort(arr, mid + 1, end)
// 合
const left = arr.slice(start, mid + 1)
const right = arr.slice(mid + 1, end + 1)
const mergeArr = merge(left, right)
// 替换原数组
for (let i = 0, n = mergeArr.length; i < n; i++) {
arr[start + i] = mergeArr[i]
}
// 合并两个有序数组
function merge(arr1, arr2) {
const temp = []
while (arr1.length && arr2.length) {
if (arr1[0] < arr2[0]) {
temp.push(arr1.shift())
} else {
temp.push(arr2.shift())
}
}
temp = [...temp, ...arr1, ...arr2]
return temp
}
}
桶排序
- 时间复杂度: 平均O(n)
- 稳定性: 看桶内排序算法而定
function bucketSort(arr) {
let newArr = []
let min = Math.min(...arr)
let max = Math.max(...arr)
const size = 2 // 桶大小
const num = Math.floor((max - min) / 2) + 1 // 桶数量
const bucketArr = new Array(num) // 二维数组,存放桶
for (let i = 0; i < num; i++) {
bucketArr[i] = []
}
// 遍历数组,放入桶内
for (let i = 0, n = arr.length; i < n; i++) {
const index = Math.floor((arr[i] - min) / size)
bucketArr[index].push(arr[i])
}
// 桶内排序
for (let i = 0, n = bucketArr.length; i < n; i++) {
// 内置排序函数, 也可以用自己写的快速排序
bucketArr[i].sort((a, b) => { return a - b })
// 原数组排序
newArr.push(...bucketArr[i])
}
return newArr
}
计数排序
- 时间复杂度: 平均O(n)
- 稳定性: 稳定
- 适用场景:数组的取值范围远小于数组长度; 如50w考生,分数范围0-750;
function countingSort(arr) {
const max = Math.max(...arr)
let countArr = new Array(max + 1).fill(0)
// 统计个数, 有点类似哈希表的味道
for (let item of arr) {
if (countArr[item] !== void 0) {
countArr[item] += 1
} else {
countArr[item] = 1
}
}
// 排序, 从小到大填充
for (let i = 0, k = 0, n = countArr.length; i < n; i++) {
for (let j = 0; j < countArr[i]; j++) {
arr[k] = i
k++
}
}
}
排序相关的算法题
(1) 无序数组中,求第k大元素,时间复杂度要求O(n);
分析:
- 由于时间复杂度要求,先排序后取值是行不通的;
- 快速排序中,获取分区点p,如果k>p, 说明目标元素在右边,否则在左边;
// 无序数组中,第K大元素
function fn (arr, k) {
let start = 0
let end = arr.length - 1
if (k > end) {
return -1
}
let p = getPartition(arr, start, end)
while (p !== k) {
console.log('p', p)
if (p < k) {
start = p + 1
} else {
end = p - 1
}
p = getPartition(arr, start, end)
}
return arr[p]
function getPartition(arr, start, end) {
let base = end
let p = start
for (let i = p; i < end; i++) {
if (arr[i] < arr[base]) {
[arr[i], arr[p]] = [arr[p], arr[i]]
p++
}
}
[arr[p], arr[base]] = [arr[base], arr[p]]
return p
}
}
let arr = [5, 6, 7, 2, 1, 4]
let k = 88
let result = fn(arr, k - 1)
console.log('result', result)
(2) 无序数组中,求最小k个数,时间复杂度要求O(n);
// 最小k个数
function fn(arr, k) {
let start = 0, end = arr.length - 1
if (k > end) return []
let p = getPartition(arr, start, end)
while (p !== k) {
if (p < k) {
start = p + 1
} else {
end = p - 1
}
p = getPartition(arr, start, end)
}
return arr.slice(0, k)
function getPartition(arr, start, end) {
let base = end
let p = start
for (let i = start; i < end; i++) {
if (arr[i] < arr[base]) {
[arr[i], arr[p]] = [arr[p], arr[i]]
p++
}
}
[arr[p], arr[base]] = [arr[base], arr[p]]
return p
}
}
let arr = [2,3,1,2,4,5,6,7]
let k = 77
let result = fn(arr, k)
console.log('result', result)