排序算法深度解析:从基础到优化的完整指南

0 阅读6分钟

排序算法是计算机科学的基石之一,每个算法都有其独特的设计哲学和适用场景。本文将深入分析六种经典排序算法的实现原理、性能特征和工程实践要点。

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次赋值操作

这解释了为什么在相同时间复杂度下,插入排序通常比冒泡排序性能更好。

算法选择指南

  1. 小规模数据(n < 50):插入排序

  2. 需要稳定排序:归并排序或插入排序

  3. 内存限制严格:堆排序或原地快排

  4. 平均性能优先:快速排序

  5. 特殊数据类型:计数排序(整数)、基数排序(字符串)

混合策略

现代排序实现通常采用混合策略:

  • Introsort:快排+堆排序+插入排序

  • Timsort:归并排序+插入排序,针对实际数据特征优化

  • PDQsort:模式击败快排,结合多种优化技术


核心观点:算法选择应基于具体的应用场景、数据特征和性能要求。深入理解每种算法的设计思想和特性,才能在实际开发中做出最优决策。