排序算法笔记
排序算法笔记
排序算法是最基础的算法, 在算法学科中有着很高的地位
排序算法的目的是将一系列可比较元素按照某种规则进行排序. 其中需要重点关注的是不同算法的时间复杂度和空间复杂度, 以及使用场景
公共API
程序中将有以下公共 API
方法签名 | 注释 |
---|---|
boolean less(Comparable v, Comparable w) | 当且仅当 v 小于 w 时, 返回 true |
void exchange(Comparable<?>[] a, int i, int j) | 交换 a 数组中索引为 i 和 j 位置的元素 |
Comparable 接口的全类名为 java.lang.Comparable
算法实现
下文中的 N 无特殊说明时代表输入数组的长度
选择排序
将数组划分为有序区和无序区, 重复按序遍历无序区元素, 每次将最小的元素放在有序区末尾, 直到无序区没有元素
分析
选择排序算法需要经过 N * (N - 1) / 2 (~ N^2 / 2) 次元素比较, 与 N 次元素交换, 时间复杂度为 O(N^2), 空间复杂度为 O(1)
选择排序算法主要有以下特点:
- 运行时间和输入无关
- 与其他算法相比, 元素交换最少
- 不稳定的排序算法
实现
1 | void selection(Comparable<?>[] a) { |
示例
输入 :1 | [10, 5, 11, 4, 6, 7, 2, 3, 1, 0, 8, 9] |
1 | [0, 5, 11, 4, 6, 7, 2, 3, 1, 10, 8, 9] |
1 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] |
插入排序
将数组划分为有序区和无序区, 重复按序选择无序区元素, 将每次选择的元素放在有序区的合适位置, 直到无序区没有元素
分析
在最好情况下, 插入排序算法需要经过 N - 1 次元素比较, 与 0 次元素交换;
在最坏情况下, 插入排序算法需要经过 ~ N^2 / 2 次元素比较, 与 ~ N^2 / 2 次元素交换;
平均情况下, 插入排序算法需要经过 ~ N^2 / 4 次元素比较, 与 ~ N^2 / 4 次元素交换.
插入排序算法时间复杂度为 O(N^2), 空间复杂度为 O(1).
插入排序算法主要有以下特点:
- 对已有序的数组效率非常高
- 稳定的排序算法
实现
1 | void insertion(Comparable<?>[] a) { |
插入排序哨兵
可以先将一个最小的元素放在有序区最前面, 这样可以避免边界判断
示例
输入 :1 | [10, 5, 11, 4, 6, 7, 2, 3, 1, 0, 8, 9] |
1 | [10, 5, 11, 4, 6, 7, 2, 3, 1, 0, 8, 9] |
1 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] |
希尔排序
希尔排序是基于插入排序的一种排序算法
希尔排序将数组按一定间隔 h 将原数组划分为若干子序列, 每个子序列中索引相同的元素归同一个子数组, 如数组 a, b, c, d, e, f, g, h, i, j, k, l
, 当 h = 4 时, 子序列有 a, b, c, d
, e, f, g, h
, i, j, k, l
, 子数组为a, e, i
, b, f, j
, c, g, k
和 d, h, l
. 排序时, 先把每个子数组利用插入排序化为有序数组, 之后减小 h 的值重复操作, 直到 h 的值为 1
分析
希尔排序在插入排序的基础上还权衡了子数组的规模和有序性, 其算法的性能与 h 的选取有很大关系. 在面对大数组时, 希尔排序比选择排序和插入排序优势较大, 但对规模非常大的输入排序不是最优选择. 希尔排序的时间复杂度为 O(N^1.5), 空间复杂度为 O(1)
插入排序算法主要有以下特点:
- 不稳定的排序算法
实现
1 | void shell(Comparable<?>[] a) { |
示例
输入 :1 | [10, 5, 11, 4, 6, 7, 2, 3, 1, 0, 8, 9] |
1 | h = 4 [10, 5, 11, 4, 6, 7, 2, 3, 1, 0, 8, 9] |
1 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] |
归并排序
归并排序的核心是 “组合”, 将数组划分为子数组, 使子数组有序后再把子数组进行合并. 子数组也用同样的方法进行排序, 当子数组中只有一个元素时它本身就是有序的
分析
归并排序发时间复杂度是 O(N * lb N), 空间复杂度是 O(N), 是一种稳定的排序方法
TimSort 排序是归并排序的优化版本, 对归并排序排在已经反向排好序的输入时做了特别优化, 最好情况的时间复杂度为 O(N)
在面对较小输入, 归并排序的大量方法调用可能会使程序效率不如插入排序
对于较大输入可以考虑将归并排序和插入排序结合使用, 当子数组较小时, 使用插入排序将其排序, 再进行归并排序
公共 API
归并排序有不同的实现, 其中将两个有序子数组组合成一个有序数组的方法是共有的, 将这部分抽象为公共 API
方法签名 | 注释 |
---|---|
void merge(Comparable<?>[] a, int low, int mid, int high) | 将 a 的子序列 a[low … mid] 和 a[mid + 1 … high] 归并 |
void mergeSort(Comparable<?>[] a, int type) | 归并排序的公共接口, type 为指定使用具体实现 |
查看 merge 方法的实现
1 | void merge(Comparable<?>[] a, int low, int mid, int high) { |
查看经过优化的 merge 方法的实现
1 | void merge(Comparable<?>[] a, int low, int mid, int high) { |
查看 mergeSort 方法的实现
1 | void mergeSort(Comparable<?>[] a, int type) { |
实现
自顶向下归并排序
自顶向下归并排序使用了分治思想
分析
自顶向下归并排序至少需要 0.5 * N * lb N 次比较, 至多 N * lb N 次比较和 6 * N * lb N 次数组访问
实现
1 | void upToDownMerge(Comparable<?>[] a, int low, int high) { // type = 0 |
示例
输入 :1 | [10, 5, 11, 4, 6, 7, 2, 3, 1, 0, 8, 9] |
1 | [5, 10, 11, 4, 6, 7, 2, 3, 1, 0, 8, 9] |
1 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] |