排序算法是计算机科学的基石之一,每个算法都有其独特的设计哲学和适用场景。本文将深入分析六种经典排序算法的实现原理、性能特征和工程实践要点。
1. 冒泡排序:交换排序的朴素实现
冒泡排序通过相邻元素的重复比较和交换来实现排序。虽然算法简单,但其O(n²)的时间复杂度限制了实际应用。
const bubbleSort = (nums: number[]): number[] => {
const len = nums.length
for (let i = 0; i < len; i++) {
let flag = false // 优化:提前终止标志
for (let j = 0; j < len - i - 1; j++) {
if (nums[j] > nums[j + 1]) {
// 相邻元素交换
let temp = nums[j]
nums[j] = nums[j + 1]
nums[j + 1] = temp
flag = true
}
}
if (!flag) break // 无交换发生,序列已有序
}
return nums
}
算法特征:
-
空间复杂度:O(1) - 原地排序算法
-
稳定性:稳定 - 相等元素的相对顺序不变
-
时间复杂度:最优O(n),平均/最坏O(n²)
-
优化要点:添加标志位可在最优情况下提前终止
2. 插入排序:增量构建有序序列
插入排序采用增量方法,逐个将未排序元素插入到已排序部分的正确位置。其核心思想是维护一个有序的前缀数组。
function insertionSort(arr: number[]): number[] {
for (let i = 1; i < arr.length; i++) {
const key = arr[i]; // 当前待插入元素
let j = i - 1;
// 向后移动大于key的元素
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j]; // 元素后移
j--;
}
arr[j + 1] = key; // 插入到正确位置
}
return arr;
}
算法特征:
-
空间复杂度:O(1) - 原地排序
-
稳定性:稳定
-
时间复杂度:最优O(n),平均/最坏O(n²)
-
工程优势:在线算法,对小规模或部分有序数据效率高
-
实际应用:常用作混合排序算法的组件(如Timsort)
3. 选择排序:最值选择策略
选择排序每次从未排序部分选择最小元素,与已排序部分的末尾元素交换。其特点是交换次数固定为n-1次。
const selectionSort = (nums: number[]): number[] => {
for (let i = 0; i < nums.length; i++) {
let j = i
let min = i
// 在未排序部分寻找最小值
while (j < nums.length) {
if (nums[j] < nums[min]) {
min = j // 更新最小值索引
}
j++
}
// 将最小值交换到当前位置
[nums[i], nums[min]] = [nums[min], nums[i]]
}
return nums
}
算法特征:
-
空间复杂度:O(1) - 原地排序
-
稳定性:不稳定 - 长距离交换可能改变相等元素的相对位置
-
时间复杂度:固定O(n²) - 与输入数据无关
-
交换次数:最多n-1次,适合交换成本高的场景
4. 归并排序:分治策略的典型实现
归并排序基于分治思想,将问题分解为子问题递归解决,然后合并结果。其稳定的O(n log n)性能使其在需要稳定排序的场景中广泛应用。
const mergeSort = (nums: number[]): number[] => {
if (nums.length <= 1) return nums // 基准情况
const mid = Math.floor(nums.length / 2)
const left = mergeSort(nums.slice(0, mid)) // 递归排序左半部分
const right = mergeSort(nums.slice(mid)) // 递归排序右半部分
return merge(left, right) // 合并有序子数组
}
const merge = (left: number[], right: number[]): number[] => {
const res: number[] = []
let i = 0, j = 0
// 双指针合并两个有序数组
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
res.push(left[i])
i++
} else {
res.push(right[j])
j++
}
}
// 处理剩余元素
return res.concat(left.slice(i)).concat(right.slice(j));
}
算法特征:
-
空间复杂度:O(n) - 需要额外的合并空间
-
稳定性:稳定
-
时间复杂度:固定O(n log n) - 性能可预测
-
并行化:天然支持并行处理
-
外部排序:适合处理大规模数据的外部排序
5. 快速排序:基于划分的高效算法
快速排序通过选择基准元素将数组划分成两部分,然后递归处理。平均情况下具有优秀的O(n log n)性能,是实际应用中最常用的排序算法之一。
const quickSort = (nums: number[]): number[] => {
if (nums.length <= 1) return nums
const pivot = nums[0] // 选择基准元素
const greater = nums.filter(item => item > pivot) // 大于基准的元素
const lesser = nums.filter(item => item < pivot) // 小于基准的元素
// 递归处理子数组并合并结果
return [...quickSort(lesser), pivot, ...quickSort(greater)]
}
算法特征:
-
空间复杂度:O(log n) - 递归栈空间(原地版本)
-
稳定性:不稳定
-
时间复杂度:平均O(n log n),最坏O(n²)
-
基准选择:随机化基准可避免最坏情况
-
实际性能:由于良好的缓存局部性,实际运行速度通常很快
6. 计数排序:非比较排序的线性算法
计数排序不基于元素间的比较,而是通过统计每个元素的出现次数来确定排序结果。在特定条件下可以达到线性时间复杂度。
const countingSort = (nums: number[]): number[] => {
if (nums.length <= 1) return nums
// 确定数据范围
let max = nums[0]
for (let i = 1; i < nums.length; i++) {
if (nums[i] > max) max = nums[i]
}
let count = new Array(max + 1).fill(0) // 计数数组
// 统计每个元素的出现次数
for (let i = 0; i < nums.length; i++) {
count[nums[i]]++
}
// 计算累积计数,确定每个元素的最终位置
for (let i = 1; i < count.length; i++) {
count[i] = count[i - 1] + count[i]
}
let result = new Array(nums.length)
// 从后向前处理,保证稳定性
for (let i = nums.length - 1; i >= 0; i--) {
let index = count[nums[i]] - 1
result[index] = nums[i]
count[nums[i]]--
}
// 将结果复制回原数组
for (let i = 0; i < nums.length; i++) {
nums[i] = result[i]
}
return nums
}
算法特征:
-
空间复杂度:O(k) - k为数据范围
-
稳定性:稳定(通过反向遍历实现)
-
时间复杂度:O(n+k) - 线性时间
-
适用条件:数据范围k相对较小的非负整数
-
扩展应用:基数排序的基础组件
算法对比与选择策略
算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
---|---|---|---|---|
冒泡排序 | O(n²) | O(1) | 稳定 | 教学演示,数据量极小 |
插入排序 | O(n²) | O(1) | 稳定 | 小数据量,部分有序数据 |
选择排序 | O(n²) | O(1) | 不稳定 | 内存受限,不关心稳定性 |
归并排序 | O(n log n) | O(n) | 稳定 | 大数据量,要求稳定 |
快速排序 | O(n log n) | O(log n) | 不稳定 | 一般情况的首选 |
计数排序 | O(n+k) | O(k) | 稳定 | 整数排序,范围不大 |
工程实践考量
性能分析要点
操作复杂度对比:
-
冒泡排序:每次交换需要3次赋值操作
-
插入排序:每次移动只需要1次赋值操作
这解释了为什么在相同时间复杂度下,插入排序通常比冒泡排序性能更好。
算法选择指南
-
小规模数据(n < 50):插入排序
-
需要稳定排序:归并排序或插入排序
-
内存限制严格:堆排序或原地快排
-
平均性能优先:快速排序
-
特殊数据类型:计数排序(整数)、基数排序(字符串)
混合策略
现代排序实现通常采用混合策略:
-
Introsort:快排+堆排序+插入排序
-
Timsort:归并排序+插入排序,针对实际数据特征优化
-
PDQsort:模式击败快排,结合多种优化技术
核心观点:算法选择应基于具体的应用场景、数据特征和性能要求。深入理解每种算法的设计思想和特性,才能在实际开发中做出最优决策。