一、3 个基础排序算法
1、冒泡排序
外层循环确定最后落点:end 从 arr.length - 1 开始减少 到 1
内层循环从头 0 遍历到 end - 1,大的往后交换
每次内层循环确定一个最终位置
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 稳定
function bubbleSort(arr) {
if (!arr || arr.length < 2) {
return
}
for (let end = arr.length - 1; end > 0; end--) {
for (let i = 0; i < end; i++) {
if (arr[i] > arr[i + 1]) {
exchange(arr, i, i + 1)
}
}
}
}
2、选择排序
外层循环指定最后落点,从 0 到 arr.length - 2
内层循环从外层指针到 arr.length - 1,找出最小元素的下标,跟外层指针落脚点交换
每次内层循环确定一个最终位置
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 稳定
function selectSort(arr) {
if (!arr || arr.length < 2) {
return
}
for (let i = 0; i < arr.length - 1; i++) {
let minIndex = i
for (let j = i + 1; j < arr.length; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex
}
if (minIndex !== i) {
exchange(arr, i, minIndex)
}
}
}
3、插入排序
外层循环下标前是排好序的
内层循环从外层下标开始往前遍历,小则往前交换,否则跳出内层循环
如果数组基本有序,时间复杂度可以达到 O(n)
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 稳定
function insertSort(arr) {
if (!arr || arr.length < 2) {
return
}
for (let i = 1; i < arr.length; i++) {
for (let j = i; j > 0; j--) {
if (arr[j] < arr[j - 1]) {
exchange(arr, j, j - 1)
} else {
// 由于前面已经有序,不需要再比较
break
}
}
}
}
二、2 个高级排序
1、归并排序
递归的时间复杂度分析:T(n) = aT(n/b) + O(n^d)
- log(b,a) > d ==> O(nlog(b,a))
- log(b,a) = d ==> O(n^dlgn)
- log(b,a) < d ==> O(n^d)
采用递归,类似二叉树的后序遍历过程(递归、分治:将原问题划分为更小规模合并的过程)
将数组从中间划分为两部分,先分别对两部分递归排序,再 merge 合并两个有序数组(需要辅助数组,由于 javascript 数组的特殊性,可以不用辅助数组)
T(n) = 2T(n/2) + O(n):a = b = 2,d = log(b,a) = 1
- 时间复杂度:O(nlgn)
- 空间复杂度:O(n)
- 稳定
function sort(arr) {
if (!arr || arr.length < 2) {
return
}
// 递归
mergeSort(arr, 0, arr.length - 1)
}
function mergeSort(arr, left, right) {
// 如果区间里只有一个数,不需要排序
if (left === right) {
return
}
let mid = left + Math.floor((right - left) / 2)
// 递归排左边
mergeSort(arr, left, mid)
// 递归排右边
mergeSort(arr, mid + 1, right)
// 合并
merge(arr, left, mid, right)
}
// 两个有序数组的合并,利用额外空间
function merge(arr, left, mid, right) {
const helper = []
let i = 0
let p1 = left
let p2 = mid + 1
while (p1 <= mid && p2 <= right) {
helper[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++]
}
// 两个必有且只有一个越界,两个 while 只有一个会执行
while (p1 <= mid) {
helper[i++] = arr[p1++]
}
while (p2 <= right) {
helper[i++] = arr[p2++]
}
for (i = 0; i < helper.length; i++) {
arr[left + i] = helper[i]
}
}
2、快速排序
经典快排:一次只确定一个数的最终位置,受数组情况的影响,有可能导致时间复杂度退化成 O(n²),空间复杂度退化成 O(n)
随机快排:利用概率得到时间复杂度 O(nlgn),空间复杂度 O(lgn)
采用递归,类似二叉树的先序遍历过程
- 先将数组进行 partition(时间复杂度 O(n),空间复杂度 O(1))
- 再对左右部分递归进行快排(时间复杂度 O(nlgn),空间复杂度 O(lgn))
与归并相比,虽然都是 O(nlgn),但是快排的常数项小,所以更快一点
- 时间复杂度:O(nlgn)
- 空间复杂度:O(lgn):每次划分都要记录下划分的中间位置
- 不稳定
function sort(arr) {
if (!arr || arr.length < 2) {
return
}
quickSort(arr, 0, arr.length - 1)
}
function quickSort(arr, left, right) {
if (left < right) {
// 随机快排:随机选取一个数字,跟 right 处交换
exchange(arr, left + Math.floor(Math.random() * (right - left + 1)), right)
const p = partition(arr, left, right)
quickSort(arr, left, p[0])
quickSort(arr, p[1], right)
}
}
function partition(arr, left, right) {
let less = left - 1
let more = right + 1
let cur = left
while (cur < more) { // cur = more 时停止
if (arr[cur] < arr[right]) {
exchange(arr, ++less, cur++)
} else if (arr[cur] > arr[right]) {
exchange(arr, --more, cur)
} else {
cur++
}
}
return [less, more] // =num 的区域
}