排序算法
排序算法概览
| 排序算法 | 平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
| 插入排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
| 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
| 归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
| 快速排序 | O(nlogn) | O(nlogn) | O(n²) | O(logn) | 不稳定 |
| 堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
| 希尔排序 | O(n^1.3) | O(n) | O(n²) | O(1) | 不稳定 |
说明:
- 时间复杂度:描述算法执行时间随数据规模增长的变化趋势
- 空间复杂度:描述算法所需额外空间随数据规模增长的变化趋势
- 稳定性:相等元素的相对顺序在排序后是否保持不变
1. 冒泡排序 (Bubble Sort)
冒泡排序是最基础的排序算法之一,其核心思想是通过多次遍历数组,每次比较相邻的两个元素,如果它们的顺序错误就交换它们。这样每一轮遍历都会将当前未排序部分的最大元素”冒泡”到正确的位置。
算法原理
- 从数组的第一个元素开始,依次比较相邻的两个元素
- 如果前一个元素大于后一个元素,则交换它们的位置
- 对每一对相邻元素重复上述操作,直到数组的末尾
- 重复以上步骤,每次遍历后,未排序部分的长度减 1
- 当某次遍历中没有发生任何交换时,说明数组已经有序,可以提前结束
算法特点
- 原地排序:不需要额外的存储空间
- 稳定排序:相等元素的相对顺序不会改变
- 自适应:对于基本有序的数组,可以提前结束排序
1 | const bubbleSort = function(arr) { |
复杂度分析:
- 时间复杂度:最好情况 O(n) - 数据已经有序,只需一次遍历;最坏情况 O(n²) - 数据完全逆序;平均情况 O(n²)
- 空间复杂度:O(1) - 只需常数级别的额外空间
- 稳定性:稳定排序 - 相等元素不会改变相对顺序
应用场景:
- 数据量较小且基本有序时效率较高
- 教学演示排序算法基本原理
- 对稳定性有要求的简单排序需求
- 适合链表排序 (只需交换节点值,不需要额外空间)
2. 插入排序 (Insertion Sort)
插入排序的工作原理类似于整理扑克牌:将未排序的元素逐个插入到已排序序列的适当位置。它将数组分为已排序和未排序两部分,初始时已排序部分只包含第一个元素。
算法原理
- 将数组分为已排序区间和未排序区间,初始时已排序区间只包含第一个元素
- 从第二个元素开始,将其作为当前要插入的元素
- 在已排序区间中从后向前扫描,找到合适的插入位置
- 将大于当前元素的元素依次向后移动一位
- 将当前元素插入到找到的位置
- 重复步骤 2-5,直到所有元素都被插入到已排序区间
算法特点
- 原地排序:只需要常数级别的额外空间
- 稳定排序:相等元素的相对顺序不会改变
- 自适应:对于基本有序的数组,时间复杂度接近 O(n)
- 在线算法:可以在接收数据的同时进行排序
1 | const insertSort = function(arr) { |
复杂度分析:
- 时间复杂度:最好情况 O(n²) - 即使数据有序也需要完整遍历;最坏情况 O(n²);平均情况 O(n²)
- 空间复杂度:O(1) - 只需常数级别的额外空间
- 稳定性:稳定排序 - 相等元素不会改变相对顺序
应用场景:
- 数据量较小且基本有序时效率较高
- 小规模数据排序,如对部分有序数据进行插入
- 链表排序 (只需修改指针,不需要额外空间)
- 作为其他复杂排序算法的子过程 (如快速排序的优化)
- 在线排序:一边接收数据一边排序
3. 选择排序 (Selection Sort)
选择排序是一种简单直观的排序算法,它的核心思想是在未排序序列中找到最小 (或最大) 的元素,存放到排序序列的起始位置,然后再从剩余未排序元素中继续寻找最小 (或最大) 元素,放到已排序序列的末尾。
算法原理
- 将数组分为已排序区间和未排序区间,初始时已排序区间为空
- 在未排序区间中找到最小 (或最大) 的元素
- 将该元素与未排序区间的第一个元素交换
- 已排序区间长度加 1,未排序区间长度减 1
- 重复步骤 2-4,直到未排序区间为空
算法特点
- 原地排序:只需要常数级别的额外空间
- 不稳定排序:交换操作可能改变相等元素的相对顺序
- 非自适应:无论数据是否有序,都需要完整遍历
- 交换次数固定:最多进行 n-1 次交换
1 | const selectSort = function(arr) { |
复杂度分析:
- 时间复杂度:最好情况 O(n²);最坏情况 O(n²);平均情况 O(n²) - 无论数据是否有序都需要完整遍历
- 空间复杂度:O(1) - 只需常数级别的额外空间
- 稳定性:不稳定排序 - 相等元素可能改变相对顺序 (如:5,8,5,2,9,第一次交换第一个 5 和 2)
应用场景:
- 数据量较小且对稳定性没有要求时
- 内存受限的场景 (空间复杂度 O(1))
- 简单实现,适合教学演示
- 当交换操作成本很高时不适合 (交换次数多)
- 不适合对大量数据进行排序
4. 归并排序 (Merge Sort)
归并排序是分治法 (Divide and Conquer) 的典型应用。它采用递归的方式将数组分成两半,分别排序后再合并。归并排序的核心思想是将大问题分解为小问题,解决小问题后再合并结果。
分治思想
分治算法一般分为三个过程:
- **分解 (Divide)**:将原问题分解成一系列子问题
- **解决 (Conquer)**:递归求解各个子问题,若子问题足够小,则直接求解
- **合并 (Combine)**:将子问题的结果合并成原问题的解
归并排序的处理过程是由下到上的,先处理子问题,然后再合并。
算法原理
- 将数组从中间分成两个子数组
- 递归地对这两个子数组进行归并排序
- 将两个已排序的子数组合并成一个有序数组
- 重复以上步骤,直到子数组长度为 1(天然有序)
算法特点
- 非原地排序:需要 O(n) 的额外空间
- 稳定排序:合并时保持相等元素的相对顺序
- 时间稳定:最好、最坏、平均时间复杂度都是 O(nlogn)
- 适合并行:子问题独立,适合多线程/并行处理
- 适合外部排序:可以处理无法全部装入内存的大规模数据
1 | const mergeSort = function(arr) { |
复杂度分析:
- 时间复杂度:最好情况 O(nlogn) - 每次分区都能将数组均匀分割;最坏情况 O(n²) - 每次分区极不平衡 (如数组已有序);平均情况 O(nlogn)
- 空间复杂度:O(n) - 需要额外的临时数组存储合并结果
- 稳定性:稳定排序 - 合并时保持相等元素的相对顺序
应用场景:
- 大规模数据排序,特别是外部排序 (数据量大无法全部装入内存)
- 需要稳定排序的场景
- 链表排序 (空间复杂度 O(1) 的链表归并排序)
- 多线程/并行排序 (子问题独立,适合并行处理)
- 逆序度计算、求逆序对数量等扩展应用
5. 快速排序 (Quick Sort)
快速排序也是分治法的应用,但与归并排序不同,它的处理过程是由上到下的,先分区,然后再处理子问题。快速排序通过选择一个基准元素 (pivot),将数组分成两部分,左边部分的元素都小于基准,右边部分的元素都大于基准,然后递归地对这两部分进行排序。
算法原理
- **选择基准 (pivot)**:从数组中选择一个元素作为基准
- **分区 (Partition)**:将数组分成两部分,使左边所有元素小于基准,右边所有元素大于基准
- 递归排序:对左右两个子数组分别递归执行快速排序
- 合并:由于分区后左右两部分已经有序,无需额外合并操作
基准选择策略
- 随机选择:随机选择一个元素作为基准,避免最坏情况
- 三数取中:选择首、中、尾三个元素的中位数作为基准
- 首元素/尾元素:简单但容易在已排序数组上产生最坏情况
算法特点
- 原地排序:只需要 O(logn) 的递归栈空间
- 不稳定排序:分区过程可能改变相等元素的相对顺序
- 平均性能最优:在实际应用中通常是最快的排序算法
- 最坏情况:当分区极度不平衡时,时间复杂度退化为 O(n²)
- 适合内存排序:对数组排序效率很高
1 | function quickSort(arr) { |
复杂度分析:
- 时间复杂度:最好情况 O(nlogn);最坏情况 O(n²) - 每次分区都极度不平衡;平均情况 O(nlogn)
- 空间复杂度:O(logn) - 递归调用栈的平均深度 (最坏情况 O(n))
- 稳定性:不稳定排序 - 分区过程可能改变相等元素的相对顺序
应用场景:
- 大规模数据排序,平均性能最优
- 内存排序,特别是数组排序
- 不需要稳定排序的场景
- 可以通过随机选择 pivot 或三数取中法避免最坏情况
- 常作为标准库排序算法的实现 (如 C++ 的 std::sort)
- 适合数据分布未知的情况
6. 堆排序 (Heap Sort)
堆排序利用堆这种数据结构进行排序。堆是一种特殊的完全二叉树,分为大顶堆和小顶堆。堆排序的核心思想是先构建一个大顶堆,然后依次取出堆顶元素 (最大值),放到数组末尾,再调整堆结构,重复此过程直到排序完成。
堆的基本概念
堆是一种特殊的树,满足以下两点:
- 完全二叉树:除了最后一层,其他层的节点数都达到最大,且最后一层的节点都集中在左边
- 堆序性:堆中每个节点的值都大于等于 (或小于等于) 其子树中每个节点的值
堆的分类:
- 大顶堆:每个节点的值都大于等于其子节点的值,根节点是最大值
- 小顶堆:每个节点的值都小于等于其子节点的值,根节点是最小值
数组表示堆
堆可以用数组来表示,对于下标为 i 的节点 (从 0 开始):
- 父节点:
(i - 1) / 2 - 左子节点:
2 * i + 1 - 右子节点:
2 * i + 2
算法原理
- 建堆:将无序数组构建成一个大顶堆
- 排序:
- 将堆顶元素 (最大值) 与堆的最后一个元素交换
- 堆的大小减 1
- 对新的堆顶元素进行堆化 (下沉),使其重新满足堆的性质
- 重复上述步骤,直到堆的大小为 1
算法特点
- 原地排序:只需要 O(1) 的额外空间
- 不稳定排序:堆化过程可能改变相等元素的相对顺序
- 时间稳定:最好、最坏、平均时间复杂度都是 O(nlogn)
- 适合 Top K 问题:堆结构天然适合解决 Top K 问题
- 实时系统友好:不会出现最坏情况,时间复杂度稳定
1 | function heapSort(arr) { |
复杂度分析:
- 时间复杂度:最好情况 O(nlogn);最坏情况 O(nlogn);平均情况 O(nlogn) - 时间复杂度稳定,不受数据初始状态影响
- 空间复杂度:O(1) - 原地排序,只需常数级别额外空间
- 稳定性:不稳定排序 - 堆化过程可能改变相等元素的相对顺序
应用场景:
- 需要原地排序且对稳定性没有要求的场景
- 内存受限的环境 (空间复杂度 O(1))
- 优先队列、Top K 问题 (堆的核心应用)
- 海量数据排序 (外部排序的归并阶段)
- 实时系统 (时间复杂度稳定,不会出现最坏情况)
- 操作系统调度器 (如进程调度)
7. 希尔排序 (Shell Sort)
希尔排序又称缩小增量排序,是插入排序的一种更高效的改进版本,属于非稳定排序算法。它通过将数组分成多个子序列,对每个子序列进行插入排序,逐步减小增量,最终对整个数组进行插入排序。
算法原理
希尔排序的基本思想是:先将整个待排序的序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录”基本有序”时,再对全体记录进行依次直接插入排序。
具体步骤:
- 选择增量序列:选择一个增量序列 t1, t2, …, tk,其中 ti > tj,tk = 1
- 分组排序:按增量序列个数 k,对序列进行 k 趟排序
- 逐步缩小增量:每趟排序根据对应的增量,将序列分割成若干子序列,分别进行插入排序
- 最终排序:当增量为 1 时,对整个序列进行一次插入排序
增量序列选择
增量序列的选择对希尔排序的性能影响很大,常见的增量序列有:
- Shell 原始序列:1, 2, 4, 8, 16, … (2 的幂次)
- Hibbard 序列:1, 3, 7, 15, 31, … (2^k - 1)
- Knuth 序列:1, 4, 13, 40, 121, … ((3^k - 1) / 2)
- Sedgewick 序列:1, 5, 19, 41, 109, … (复杂公式)
算法特点
- 原地排序:只需要 O(1) 的额外空间
- 不稳定排序:跨间隔的插入操作可能改变相等元素的相对顺序
- 性能优于简单排序:时间复杂度介于 O(n) 和 O(n²) 之间
- 增量序列敏感:性能很大程度上取决于增量序列的选择
- 适合中等规模数据:在小规模数据上不如插入排序,在大规模数据上不如快速排序
1 | function shellSort(arr) { |
复杂度分析:
- 时间复杂度:最好情况 O(n) - 增量序列合适时;最坏情况 O(n²);平均情况 O(n^1.3) - 取决于增量序列的选择
- 空间复杂度:O(1) - 原地排序,只需常数级别额外空间
- 稳定性:不稳定排序 - 跨间隔的插入操作可能改变相等元素的相对顺序
应用场景:
- 中等规模数据排序,性能优于简单排序
- 对稳定性没有要求的场景
- 内存受限的环境 (空间复杂度 O(1))
- 适合部分有序的数据
- 作为插入排序的改进版本,在插入排序效率不足时使用
排序算法关联分析
算法分类体系
1. 按实现方式分类
交换排序
- 冒泡排序:相邻元素比较交换
- 快速排序:基于分治的交换排序
插入排序
- 插入排序:逐个插入已排序序列
- 希尔排序:跨间隔的插入排序
选择排序
- 选择排序:每次选择最小元素
- 堆排序:利用堆结构选择最值
归并排序
- 归并排序:分治合并
2. 按时间复杂度分类
O(n²) 类 - 适合小规模数据
- 冒泡排序、插入排序、选择排序
- 优点:实现简单,常数因子小
- 缺点:大规模数据效率低
O(nlogn) 类 - 适合大规模数据
- 归并排序、快速排序、堆排序
- 优点:大规模数据效率高
- 缺点:实现相对复杂
介于两者之间
- 希尔排序:性能介于 O(n) 和 O(n²) 之间
3. 按空间复杂度分类
原地排序 (O(1))
- 冒泡排序、插入排序、选择排序、堆排序、希尔排序
非原地排序
- 归并排序:O(n)
- 快速排序:O(logn) (递归栈)
4. 算法演进关系
1 | 插入排序 → 希尔排序 (引入增量优化) |
算法选择决策树
1 | 开始 |
算法选择策略表
| 场景特征 | 推荐算法 | 备选方案 | 理由 |
|---|---|---|---|
| 小规模数据 (<100) | 插入排序 | 选择排序 | 简单高效,常数因子小 |
| 基本有序数据 | 插入排序 | 冒泡排序 | 接近最优时间复杂度 O(n) |
| 大规模数据 | 快速排序 | 归并排序 | 平均性能最优 |
| 内存受限 | 堆排序 | 希尔排序 | 空间复杂度 O(1) |
| 需要稳定排序 | 归并排序 | 插入排序 | 保持相等元素相对顺序 |
| 外部排序 | 归并排序 | - | 适合处理海量数据 |
| 平均性能优先 | 快速排序 | - | 实际应用中性能最佳 |
| 实时系统 | 堆排序 | - | 时间复杂度稳定,无最坏情况 |
| 大量重复元素 | 三向快速排序 | 归并排序 | 优化分区策略 |
| 链表排序 | 归并排序 | 插入排序 | 适合链表结构 |
算法优化技巧
1. 混合排序策略
快速排序优化
1 | // 小数组切换为插入排序 |
归并排序优化
1 | // 小数组切换为插入排序 |
Timsort
- Python、Java 等语言的标准排序算法
- 结合归并排序和插入排序的优点
- 对部分有序数据特别高效
2. 快速排序优化
三数取中法选择 pivot
1 | function medianOfThree(arr, left, right) { |
三向切分
- 将数组分为三部分:小于、等于、大于基准
- 适合处理大量重复元素的情况
3. 归并排序优化
原地归并
- 通过巧妙的索引操作减少空间开销
- 实现较为复杂,但可以将空间复杂度降低
并行归并
- 利用多核处理器并行处理子问题
- 适合大规模数据排序
总结
排序算法是计算机科学中最基础也最重要的算法之一,理解各种排序算法的原理、特点和适用场景对于编程能力的提升至关重要。排序算法不仅在实际开发中应用广泛,也是培养算法思维和问题解决能力的重要途径。
核心要点
1. 没有绝对最优的排序算法
每种排序算法都有其适用的场景和局限性,选择算法时需要综合考虑:
数据规模
- 小规模数据 (<100):插入排序、选择排序
- 中等规模数据:希尔排序
- 大规模数据:快速排序、归并排序、堆排序
数据特征
- 基本有序:插入排序、冒泡排序
- 完全随机:快速排序
- 大量重复:三向快速排序
- 部分有序:Timsort
资源限制
- 内存受限:堆排序、希尔排序
- CPU 受限:归并排序 (可并行)
- 稳定性要求:归并排序、插入排序
2. 算法设计思想的重要性
排序算法体现了多种重要的算法设计思想:
分治思想
- 将大问题分解为小问题,递归解决后再合并
- 典型应用:归并排序、快速排序
- 扩展应用:二分查找、快速傅里叶变换等
交换思想
- 通过交换元素的位置来达到排序目的
- 典型应用:冒泡排序、快速排序
- 扩展应用:图的最小生成树算法等
插入思想
- 将元素插入到已排序序列的适当位置
- 典型应用:插入排序、希尔排序
- 扩展应用:哈希表、符号表等
选择思想
- 每次选择最值元素放到正确位置
- 典型应用:选择排序、堆排序
- 扩展应用:优先队列、Top K 问题等
3. 时间复杂度与空间复杂度的权衡
| 算法 | 时间复杂度 | 空间复杂度 | 权衡特点 |
|---|---|---|---|
| 归并排序 | O(nlogn) | O(n) | 时间优,空间差 |
| 堆排序 | O(nlogn) | O(1) | 时间稳定,空间优 |
| 快速排序 | O(nlogn) | O(logn) | 平均最优,最坏差 |
| 插入排序 | O(n²) | O(1) | 简单高效,适合小规模 |
实际应用中需要根据场景权衡:
- 内存充足时优先考虑时间效率
- 内存受限时优先考虑空间效率
- 实时系统需要避免最坏情况
4. 稳定性的重要性
稳定排序
- 归并排序、插入排序、冒泡排序
- 保持相等元素的相对顺序
- 适用于:多关键字排序、对象排序等场景
不稳定排序
- 快速排序、堆排序、选择排序、希尔排序
- 可能改变相等元素的相对顺序
- 适用于:数值排序、不需要保持原始顺序的场景
实际应用中的排序算法
标准库排序算法
JavaScript
- Array.prototype.sort()
- 大多数实现使用快速排序或 Timsort
Python
- list.sort() 和 sorted()
- 使用 Timsort 算法
Java
- Arrays.sort() 对基本类型使用快速排序
- 对对象类型使用 Timsort
C++
- std::sort() 通常使用快速排序的变种 (Introsort)
工程实践建议
优先使用标准库
- 标准库的排序算法经过充分优化
- 考虑了各种边界情况和性能优化
特殊场景自定义
- 特殊数据分布
- 特殊性能要求
- 特殊稳定性要求
性能测试验证
- 不同算法在不同数据上的表现差异很大
- 实际测试比理论分析更重要
学习路径建议
第一阶段:掌握基础
理解基本概念
- 时间复杂度、空间复杂度
- 稳定性、原地排序
- 最好、最坏、平均情况
学习简单排序
- 冒泡排序:理解交换思想
- 插入排序:理解插入思想
- 选择排序:理解选择思想
动手实现
- 从零实现每个算法
- 理解每个步骤的作用
- 测试不同数据集的表现
第二阶段:深入高级
分治思想
- 归并排序:自底向上的分治
- 快速排序:自顶向下的分治
- 理解两种分治方式的区别
堆结构
- 理解堆的性质和操作
- 掌握堆排序的实现
- 扩展到优先队列、Top K 问题
优化技巧
- 混合排序策略
- 快速排序的各种优化
- 归并排序的并行化
第三阶段:实践应用
实际项目应用
- 根据场景选择合适算法
- 处理特殊数据情况
- 性能优化和调优
扩展应用
- 外部排序
- 多关键字排序
- 排序相关问题的解决
深入研究
- 研究标准库实现
- 学习最新研究成果
- 参与开源项目
第四阶段:举一反三
思想迁移
- 将排序思想应用到其他算法问题
- 分治思想在动态规划中的应用
- 堆结构在图算法中的应用
问题解决
- 逆序对计数
- 最接近的 K 个数
- 合并 K 个有序链表
算法设计
- 设计新的排序算法
- 改进现有算法
- 解决特定问题的排序变体
进阶学习资源
推荐书籍
- 《算法导论》- 排序算法章节
- 《算法 (第 4 版)》- 排序与优先队列
- 《编程珠玑》- 排序相关章节
在线资源
- 各大 OJ 平台的排序题目
- 可视化排序算法网站
- 开源项目的排序实现
排序算法的学习是一个循序渐进的过程,从理解基本概念到掌握高级技巧,从理论分析到实践应用,每一步都需要扎实的基础和持续的练习。通过深入学习和实践,不仅可以掌握排序算法本身,更能培养算法思维和问题解决能力,为后续学习更复杂的算法打下坚实基础。
排序算法的学习不仅是掌握算法本身,更是培养算法思维和问题解决能力的过程。通过深入理解这些算法,我们可以更好地应对各种编程挑战。