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