排序算法
基于比较的排序
排序的稳定性:关键字相等的节点排序后仍保持在排序前的相对顺序,则称排序是稳定的
稳定排序:插入排序,冒泡排序,归并排序
不稳定排序:选择排序,快速排序,堆排序
插入排序
如其名,把一个数插入到一个已经排好的序列中
1 | template <class Item> |
时间复杂度最佳
空间复杂度
是稳定排序
适用于元素较少时的排序
冒泡排序
依次比较相邻元素
1 | template <class Item> |
时间复杂度最佳
空间复杂度
是稳定排序
选择排序
选出最小的元素和第一个交换,再选次小的和第二个交换,依次完成排序
1 | template <class Item> |
时间复杂度最佳
空间复杂度
不是稳定排序
优点是交换次数少,适用于交换代价较高的场景,运行时间和记录顺序关系很小
归并排序及优化
基于分治思想
两种次序:自顶向下、自底向上
自顶向下归并排序:先划分再归并
1 | template <class Item> |
自底部向下归并排序:先归并再划分
1 | template <class Item> |
归并排序时间复杂度分析
时间复杂度最佳
共进行了
空间复杂度
是稳定排序
在数据只能按序访问的情况下(如链表)比快速排序快
归并排序的优化
- 对小规模数组使用插入排序
- 检测数组是否已经有序,若有序则不进行归并操作
- 去除原数组到辅助数组的复制操作,在递归过程中交替使用原数组和辅助数组
优化一:对小规模数组使用插入排序
在小数组下,使用插入排序比归并排序更快,可以设定一个阈值
1 | enum { k = 16 }; |
优化二:检测数组是否已经有序
根据归并特点,若
1 | template <class Item> |
优化三:去除复制操作
每次 merge 时都需要将原数组复制到辅助数组中,可以通过在递归过程中交替使用原数组和辅助数组来避免这一步
1 | template <class Item> |
综合三个优化后的归并排序
1 | enum { k = 16 }; |
std::stable_sort
这个库函数的实现使用了自底向上的归并排序,并结合了上述优化技术
如果可以申请到辅助空间,它的优化是一+二+三
若申请不到足够的额外空间,则会将序列进行递归分割,若分割后额外空间足以容纳待排序序列,则使用上一段的方法,否则使用 原地归并即 inplace_merge ,这使得整个排序算法的 时间复杂度增加
若完全申请不到额外空间,则时间复杂度会增加到
这里贴出 sgi-stl 的源码
源码: 点击查看代码
1 | template <class _RandomAccessIter> |
这里出现了一个 implace_merge 函数
inplace_merge 函数用于在不使用额外空间的情况下合并两个已排序的子序列
首先要引入一个辅助函数 rotate,用于旋转数组的一部分
1 | // 反转数组 |
1 | template <class Item> |
inplace_merge 在最坏情况下,要进行rotate(),这导致的最坏时间复杂度为
进一步的,在 inplace_merge 中使用分治思想,可以把复杂度降低到
1 | template <class Item> |
使用分治思想的 inplace_merge 的归并排序时间复杂度为
不需要额外的数组空间,因此堆内存开销为
因此,使用分治思想的 inplace_merge 的归并排序空间复杂度为
堆排序
堆是一种完全二叉树,可以用数组表示,它要求任意节点的值不小于(或不大于)其子节点的值,称为大根堆(或小根堆)
通过 shiftDown 操作可以将一个数组调整为堆,时间复杂度
1 | template <class Item> |
建堆操作:从最后一个非叶节点开始,依次对每个节点执行 shiftDown 操作,直到根节点
进行了 n 次 shiftDown 操作,每次操作的时间复杂度为
由于第shiftDown 的比较次数为:
堆排序算法:借助堆的结构排序,首先将数组调整为堆,然后将堆顶元素与最后一个元素交换,再对剩余元素重新调整为堆,重复此过程直到所有元素排序完成
1 | template <class Item> |
堆排序时间复杂度最佳
空间复杂度
不是稳定排序
在堆排序过程中,会把堆顶元素与最后一个元素交换位置,这可能会改变相同元素的相对顺序,因此堆排序不是稳定排序算法
插入新节点后需要调整堆,将其上浮到合适位置
1 | template <class Item> |
这里贴出 sgi-stl 的源码
源码: 点击查看代码
1 | // partial_sort, partial_sort_copy, and auxiliary functions. |
参数说明:
partial_sort:对区间
快速排序及优化
基本思想:分治
选择一个基准元素,将数组划分为两部分,一部分小于等于基准元素,另一部分大于等于基准元素,然后递归地对这两部分进行排序
1 | template <class Item> |
时间复杂度最佳
空间复杂度最坏
不是稳定排序
快速排序的优化
- 栈+短序列优先
- 三数取中法选择基准元素
- 与归并一样,对小规模数组使用插入排序
- 三路划分以减小重复元素的影响
优化一:栈+短序列优先
1 | template <class Item> |
优化二:三数取中法选择基准元素
1 | template <class Item> |
优化三:对小规模数组使用插入排序
1 | enum { k = 16 }; |
优化四:三路划分以减小重复元素的影响
1 | template <class Item> |
综合了优化二三四的快速排序
1 | enum { k = 16 }; |
Introspective Sort
内省排序:结合了快速排序、堆排序的优点
通过引用堆排序,避免了快速排序在最坏情况下的
示例代码
1 | enum { k = 16 }; |
这里贴出 sort() sgi-stl 的源码
源码: 点击查看代码
1 | // sort() and its auxiliary functions. |
计数排序
计数排序适用于数据范围较小的整数排序,假设待排序数组为
1 | template <class Item> |
时间复杂度为
空间复杂度为
是稳定排序
这位基数排序的正确性提供了保证
基数排序
基将整数按位数进行排序,从最低有效位到最高有效位,使用稳定的排序算法(如计数排序)对每一位进行排序
MSD 基数排序从最高有效位开始排序
1 | template <class Item> |
LSD 基数排序从最低有效位开始排序
1 | template <class Item> |
LSD 基数排序时间复杂度为
空间复杂度为
是稳定排序
MSD 与 LSD 的比较
LSD 基数排序从最低有效位开始排序,适用于位数较少且分布均匀的数据
MSD 基数排序从最高有效位开始排序,适用于位数较多且分布不均匀的数据
LSD 必须要比较完所有位,而 MSD 可以在某些情况下提前终止排序
但当需要比较完所有位时,MSD 相比 LSD 会有更高的递归开销
MSD 要对每一个子序列重新计算基数,而 LSD 只需每轮计算一次,这使得 LSD 在处理大规模数据时通常更高效
桶排序
假设数据均匀分布
桶排序将区间
1 | template <class Item> |
假设映射函数均匀分配,此时桶排序的时间复杂度为
这是一个稳定的排序算法
桶排序容易在桶层面做并行化
并行化代码实现
1 |
|
排序的比较
| 排序方法 | 平均时间 | 最坏时间 | 最佳时间 | 辅助空间 | 稳定性 |
|---|---|---|---|---|---|
| 插入排序 | 稳定 | ||||
| 冒泡排序 | 稳定 | ||||
| 选择排序 | 不稳定 | ||||
| 归并排序 | 稳定 | ||||
| 快速排序 | 不稳定 | ||||
| 堆排序 | 不稳定 | ||||
| 计数排序 | 稳定 | ||||
| 基数排序 LSD | 稳定 | ||||
| 桶排序 | 稳定 |
其中
Top-K 问题
Top-K 问题是指在一个数据集中找到前 K 个最大的元素或最小的元素
大致分为两类:
静态 Top-K:数据集在查询过程中保持不变
动态 Top-K:数据集在查询过程中可能会发生变化,需要动态维护 Top-K 元素
静态 Top-K
完全排序
将输入内容进行完全排序,然后取前 K 个元素
使用小根堆
不对输入内容进行排序
维护一个大小为
时间复杂度:
空间复杂度:
1 | template <class Item> |
随机选择算法
先找到第 K 大的元素,然后遍历数据集,收集所有大于等于该元素的元素
这借鉴了快速排序的思想,将数组进行划分,区别是快排会递归地对两部分进行排序,而随机选择算法只对包含第 K 大元素的部分进行处理
时间复杂度:
空间复杂度:
1 | int mtrand(int l, int r){ |
选择算法
这是基于随机选择算法的改进
- 将输入的
个元素 划分为 组,每组包含 5 个元素(最后一组可能少于 5 个) - 对每组进行插入排序,确定中位数
- 对所有中位数递归地调用选择算法,找到中位数的中位数,作为划分的基准元素
- 以该基准元素对数组进行划分,然后根据划分结果递归地选择第 K 大元素所在的部分
最坏情况下时间复杂度为
空间复杂度为
1 | template <class Item> |
动态 Top-K
使用对顶堆
对顶堆是一种特殊的堆结构,其中包含两个堆:一个大根堆和一个小根堆
小根堆用于存储当前的 Top-K 元素,而大根堆用于存储剩余的元素
1 | template <class Item> |
时间复杂度:添加元素
空间复杂度:
还有使用动态平衡树(如 AVL 树或红黑树)、线段树来维护 Top-K 元素的方案,等到树章节再补充
逆序对
逆序对是指在一个数组中,存在一对元素
同理存在正序对
它们的求法可以使用归并排序的思想,在归并的过程中统计逆序对的数量
1 | template <class Item> |
时间复杂度:
空间复杂度:
其实均与归并排序相同
TimSort
TimSort 是一种混合排序算法,结合了归并排序和插入排序的优点,最初实现于 Python 的排序函数中,后来被广泛应用于 Java 和其他编程语言的标准库中
- Title: 排序算法
- Author: exdoubled
- Created at : 2025-11-02 09:00:00
- Updated at : 2025-11-05 14:53:57
- Link: https://github.com/exdoubled/exdoubled.github.io.git/sjjg/sjjg05/
- License: This work is licensed under CC BY-NC-SA 4.0.