排序算法的稳定性
在待排序的文件中,若存在多个关键字相同的记录,经过排序后这些具有相同关键字的记录之间的相对次序保持不变,该排序方法是稳定的
若具有相同关键字的记录之间的相对次序发生变化,则称这种排序方法是不稳定的。
交换排序类
冒泡排序
比较相邻元素,反序则交换
function bubbleSort(arr) {
const len = arr.length - 1
for (let i = 0; i < len; i++) {
// 将最大值排到最后
for (let j = 1; j < len - i - 1; j++) {
// swap
if (arr[j] > arr[j + 1]) {
;[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
}
快速排序
是对冒泡排序算法的一种改进
最坏的情况,待排序的序列是正序 / 反序,时间复杂度:O(n^2)
- 首先设定一个基准,将小于基准的值放到数组的左边,大于基准的值放到数组的右边。这个称为分区操作
- 递归左右序列进行快速排序,最终排序完成
- 分而治之
- 自顶向下拆分排序
理解快速排序的思想
- 时间复杂度 O(n logn)
- 空间复杂度 O(n logn)
function quickSort(arr) {
if (arr.length < 2) return arr
const pivot = arr[0] // 基准元素
const left = []
const right = []
// 左右分开
for (let i = 1; i < arr.length; i++) {
const item = arr[i]
if (item <= pivot) {
left.push(item)
} else {
right.push(item)
}
}
return [...quickSort(left), pivot, ...quickSort(right)]
}
原地排序 + 双指针
- 时间复杂度 O(n logn)
- 空间复杂度 O(logn)
function quickSort(arr, low = 0, high = arr.length - 1) {
if (low < high) {
// 划分 [low..hight] 算出枢轴
const pivot = partition(arr, low, high)
// 对低子序列递归排序
quickSort(arr, low, pivot - 1)
// 对高子序列递归排序
quickSort(arr, pivot + 1, high)
}
}
function partition(arr, low, high) {
const pivotKey = arr[low] // 基准
let i = low + 1 // 左指针
let j = high // 右指针
while (i <= j) {
// 移动左指针 - 找到一个比基准大的值
while (i <= j && arr[i] < pivotKey) {
i++
}
// 移动右指针 - 找到一个比基准小的值
while (i <= j && arr[j] > pivotKey) {
j--
}
// swap
if (i <= j) {
;[arr[i], arr[j]] = [arr[j], arr[i]]
// 移动指针
i++
j--
}
}
// 最后一个 <= pivotKey 的位置
// 将基准放到正确位置
;[arr[low], arr[j]] = [arr[j], arr[low]]
return j
}
1. 优化选取基准
如果我们选取的基准是处干整个序列的中间位置,那么我们可以将整个小数集合和大数集合了
排序速度的快慢取决干的基准处在整个序列中的位置,太小或者太大,都会影响性能
1.1 随机选取
const index = Math.floor(Math.random() * (high - low + 1) + low)
;[arr[index], arr[low]] = [arr[low], arr[index]]
const pivot = arr[low]
1.2 三数取中
取三个关键字先进行排序,将中间数作为枢轴
const mid = (low + high) >> 1
// 交换左端 / 右端,保证左端较小
if (arr[low] > arr[high]) {
;[arr[low], arr[high]] = [arr[high], arr[low]]
}
// 交换中间 / 右端,保证中间较小
if (arr[mid] > arr[high]) {
;[arr[mid], arr[high]] = [arr[high], arr[mid]]
}
// 交换中间 / 左端,保证左端为中间值
if (arr[mid] > arr[low]) {
;[arr[mid], arr[low]] = [arr[low], arr[mid]]
}
// arr[low] 为整个序列左、中、右三个关键字的中间值
const pivotKey = arr[low]
1.3 九数取中
2. 优化小数组时的排序方案
数组非常小时使用直接插入排序
原因:快速排序用到了递归操作,在数据量较大时可以忽略
// 7 / 50 都可以,实际应用可适当调整
const MAX_LENGTH_INSERT_SOFT = 7
function QSort(arr, low, high) {
// 快速排序
if (high - low > MAX_LENGTH_INSERT_SOFT) {
let pivot = partition(arr, low, high)
QSort(arr, low, pivot - 1)
QSort(arr, pivot + 1, high)
}
// 插入排序
else {
insertSort(arr)
}
}
选择排序类
选择排序
从待排序中选出最小的值,放到已排序的末尾
- 不稳定:待排序中找到最小值之后,和已排序的末尾元素交换
function selectSort(arr) {
const len = arr.length
for (let i = 0; i < len - 1; i++) {
// 找到最小值
let index = i
for (let j = i + 1; j < len; j++) {
if (arr[j] < arr[index]) {
index = j
}
}
// swap
if (index !== i) {
;[arr[index], arr[i]] = [arr[i], arr[index]]
}
}
}
堆排序
插入排序类
(直接)插入排序
将待排序元素,从后向前扫描插入到已排序中
优势:
- 数据量较少时
- 基本有序
function insertSort(arr) {
for (let i = 1; i < arr.length; i++) {
const target = arr[i]
let j = i
// 反序
while (j > 0 && arr[j - 1] > target) {
// 大于目标元素后移一位
arr[j] = arr[j - 1]
j--
}
// 插入目标元素
arr[j] = target
}
}
希尔排序
也称为缩小增量排序,是一种改进的插入排序算法
- 通过定义一个增量序列,根据增量将数组分割成多个子序列,对每个子序列进行插入排序。
- 随着增量的逐渐缩小,子序列中的元素越来越有序,最终当增量缩小到1时,整个数组将变得基本有序,此时只需进行一次插入排序即可完成排序
function shellSort(arr) {
let gap = arr.length >> 1 // 初始化增量
// 确保增量最终缩小到 1
while (gap > 0) {
// 分组
for (let i = gap; i < arr.length; i++) {
// 在当前增量的子序列中进行插入排序
const temp = arr[i]
let j = i
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap]
j -= gap
}
arr[j] = temp
}
// 缩小增量
gap >>= 1
}
}
归并排序
- 分而治之
- 自顶向下二分拆分,自底向上排序合并
二分:先将待排序数组递归地拆分成两个子序列,直到单个元素
排序:然后对每个子序列进行排序
合并:最后将两个有序子序列合并成一个有序序列
function mergeSort(arr) {
const len = arr.length
// 单个元素,结束递归
if (len < 2) return arr
// 二分
const mid = len >> 1
const left = arr.slice(0, mid)
const right = arr.slice(mid)
return merge(arguments.callee(left), arguments.callee(right))
}
function merge(left, right) {
const stack = []
// 将两个数组进行排序、合并
while (left.length && right.length) {
if (left[0] < right[0]) {
stack.push(left.shift())
} else {
stack.push(right.shift())
}
}
return stack.concat(left, right)
}
非比较类排序
计数排序
将序列中的元素作为键、个数作为值存储在额外的数组空间中,通过遍历该数组排序。
- 作为一种线性时间复杂度的排序,必须是一定范围内的整数
- 牺牲空间换取时间
基础版
function countingSort(array) {
// 1. 找出最小值,作为偏移值
const min = Math.min(...array)
// 2. 计数
const counts = []
for (const v of array) {
// 统计时减去偏移量
counts[v - min] = (counts[v - min] || 0) + 1
}
// 3. 取出排序
let index = 0
for (let i = 0; i < counts.length; i++) {
let count = counts[i]
while (count > 0) {
// 排序时加上偏移量
array[index] = i + min
count--
index++
}
}
}
进阶版,保证数组顺序
function countingSort(arr) {
// 1. 找出最大、最小值
const min = Math.min(...arr)
const max = Math.max(...arr)
// 2. 计数
// max-min+1 是计数数组的长度
// 再 +1 是为了 count[0] 永远为 0
const count = new Array(max - min + 1 + 1).fill(0)
for (const num of arr) {
count[num - min + 1]++
}
// 3. 累加之和的值 - 前缀和
for (let i = 1; i < count.length; i++) {
count[i] += count[i - 1]
}
// 4. 取出排序
const result = []
for (const num of arr) {
// count[num - min] 代表着比 num 小的数的数量
// 即当前 num 排序后的位置
result[count[num - min]] = num
count[num - min]++
}
return result
}
参考链接
桶排序
将待排序的元素划分为若干个有序的区间,称为桶(Bucket),然后分别对每个桶内的元素进行排序
- 适用于对有界范围的整数或浮点数进行排序
function bucketSort(array) {
const min = Math.min(...array)
const max = Math.max(...array)
// +1 确保能够容纳最大值
const size = Math.floor((max - min) / array.length) + 1 // 桶的大小
// const count = Math.floor((max - min) / size) + 1 // 桶的数量
const buckets = []
// 将元素分配到桶中
for (const x of array) {
const index = Math.floor((x - min) / size)
;(buckets[index] ??= []).push(x)
}
// 对每个桶中的元素进行插入排序,并合并为结果数组
const result = []
for (const bucket of buckets) {
if (bucket) {
// result.push(...bucket.sort((a, b) => a - b))
result.push(...insertionSort(bucket))
}
}
return result
}
基数排序
基数排序也是一个分布式排序算法,它根据数字的有效位或基数将整数分布到桶中进行排序。
function radixSort(arr) {
// 找到数组中的最大值,确定需要进行几轮排序
const maxNum = Math.max(...arr)
const maxDigits = String(maxNum).length
// 根据每个位上的数字进行排序
for (let i = 0; i < maxDigits; i++) {
// 此处是使用桶排序,当然也可以使用计数排序
arr = bucketSort(arr, i)
}
return arr
}
// 桶排序(按指定位进行排序)
function bucketSort(arr, digit) {
// 定义 10 个桶
// 对于十进制数,只会出现 0-9 的数字,所以使用的基数是 10
const buckets = Array.from({ length: 10 }, () => [])
// 将元素分配到对应的桶中
for (let j = 0; j < arr.length; j++) {
// 54321 / Math.pow(10, 3) 等于 54.321
const num = Math.floor(arr[j] / Math.pow(10, digit)) % 10
buckets[num].push(arr[j])
}
// 按照桶的顺序依次输出所有元素
return [].concat(...buckets)
}
基数排序适用于待排序元素为非负整数的情况
- 将所有的数加一个正数,使得所有的数变为正数进行基数排序; 排序完之后再减点加的正数值输出
const minValue = Math.min(...arr)
if (minValue < 0) {
arr = arr.map(i => i - minValue)
}
// 排序结束后
if (minValue < 0) {
arr = arr.map(i => i + minValue)
}