排序
1. 基础排序
循环不变式满足三条性质
- 初始:第一次迭代之前,循环不变式为真
- 保持:如果当次迭代前循环不变式为真,那么下次迭代前循环不变式依旧为真
- 终止:当循环终止时,循环不变式提供了一个有用的性质
循环不变式的意义:终止情况一定是结果情况,尝试找到一个初始情况和循环操作,使得每次迭代,保持某一段为真,并逐渐扩展该段,直到该段是整段
1.1 插入排序
原理:将每个元素都放到“比左边元素大,比右边元素小”的位置上
循环不变式:数组的前i个元素(数组下标从0到i-1)都是排序好的
- 初始:从i=1开始,此时之前只有一个元素即A[0],肯定是排序好的
- 保持:由于从0到i-1都是排序好的,所以肯定存在一个下标j使得
A[j]<=A[i]<=A[j+1],将A[j+1]、A[j+2]、…、A[i-1]向右平移,然后将A[i]的值插入到A[j+1],即可使得从0到i都是排序好的 - 终止:i等于数组长度
1 | def INSERT_SORT(array): |
1.2 选择排序
原理:选择每一段数组的最大元素放到数组末尾
循环不变式:数组i之后的元素都是排序好的,n是数组长度
- 初始:从i=n-1开始,此时i后的元素为空,是平凡排序好的
- 保持:由于数组i之后元素是排序好的,即它们肯定比从0到i的任何元素都大,所以只需要找到这部分的最大值,将其与A[i]互换,即可使的i-1后的元素都是排序好的
- 终止:i=0,此时只剩一个最小的元素正好在第一位,循环终止
1 | def SELECT_SORT(array): |
1.3 冒泡排序
原理:每次都将相邻元素的较大值放到后面
循环不变式:和插入排序是一样的,但是冒泡排序在遍历每一段数组的过程中,都将较大元素尽可能往后放,而不是只是找到最大元素的索引
1 | def BUBBLE_SORT(array): |
2. 分治排序
分治策略本质上就是分和治,分就是将一个大问题分成多个小问题去解,治就是利用多个小问题的解来得出一个大问题的解,分治策略大体上有以下三个步骤:
- 分解(Divide):将问题划分为一些子问题,子问题的形式与原问题完全一样,只是规模更小
- 解决(Conquer):当问题规模足够小时,停止递归,求解出当前子问题
- 合并(combine/merge):将子问题的解组合成原问题的解
2.1 快速排序
原理:将数组根据主元不断分为两部分,其中主元的位置就是最终排序的位置,且主元左边都是比主元小的元素,主元右边都是比主元大的元素,对两边的子数组递归调用排序
循环不变式:总是取结尾为主元索引pivot,设置两个索引low和high,需要满足索引比low小的都是小于主元的,索引比high大的都是大于等于主元的
- 初始:low=0,high=n-2,此时low之前没有元素可认为比主元小,high之后只有主元
- 保持:如果,则将low和high的元素互换并将high-1,如果,则先将low+1
- 终止:当时,将和互换,即可实现放置主元于正确位置,且主元左边都比主元小,主元右边都比主元大
1 | def QUICK_SORT(array): |
2.2 归并排序
原理:递归地从中间分解数组,然后再合并排序好的结果
分治策略:
- 分解:可以从中间将当前数组分为两个子数组递归
- 解决:如果当前数组长度为1,无法继续分,也可以认为已经排好序,停止递归,返回该数组
- 合并:对两个已经排好序的数组,设立两个指针进行遍历,可以很轻松地合并为一个排好序的数组
1 | def merge(left,right): |
3. 堆排序
原理:将数组构造成最大堆,不断去除并获取堆顶元素放到数组末尾
实现:详细请看另一篇关于堆的文章
1 | def SORT_MAX_HEAP(heap): |
4. 线性排序
之前所讨论的排序都是依赖于元素之间的比较,时间复杂度都是(堆排序、归并排序、快速排序)或(插入、选择、冒泡),而线性时间排序的方法是根据数组中元素的特定属性,并且不是原址操作,会牺牲一定空间
4.1 计数排序
原理:对每个元素i,计算小于等于i的元素个数j,则元素i最终的索引就在j-1
实现:数组满足最小元素是0,最大元素maxnum,且元素都是整数
- 创建一个长为maxnum+1的数组count,对于count的索引i,count[i]就是i出现在array的次数
- 对于count的索引i,在数组array中小于等于i的元素个数就是count[0] + coun[1] + … + count[i]
- 创建一个临时数组temp,遍历array的元素,根据count将元素放到temp正确的排序位置,然后更新count
1 | def COUNT_SORT(array,maxnum): |
4.2 基数排序
原理:从低到高按位排序,假设遍历到第i位,相同数值内的顺序是按照第i-1位的排序情况
实现:给定数组中具有最多位元素的位数maxbit,且元素都是整数
- 每一位都采用计数排序,当前位的数值等于
- 由于上一位已经排序好,所以当前位要从后往前遍历数组,才能保证上一位的排序情况
1 | def RADIX_SORT(array,maxbit): |
4.3 桶排序
原理:将数组分为多个连续的区间,区间里面排好序之后再合并
实现:给定数组中最大的元素maxnum
- 首先计算足够多的桶的数量并创建桶
- 遍历数组的元素,用元素大小除以桶的数量可以得到位于哪个桶
1 | def BUCKET_SORT(array,maxnum): |
5. 区别分析
性质
- 时间复杂度:平均(随机情况)、最好(已经排好序)、最坏(完全逆序或其他特殊情况)
- 稳定性:在排序过程中,相等的元素相对位置是否保持不变
- 数据情况:数量情况,原始排序情况,数值情况
| 排序 | 平均时间 | 最坏时间 | 最好时间 | 稳定性 | 数据情况 |
|---|---|---|---|---|---|
| 插入 | O(n²) | O(n²) | O(n) | 稳定 | 数据量小,基本有序 |
| 选择 | O(n²) | O(n²) | O(n²) | 不稳定 | 数据量小,需要最值 |
| 冒泡 | O(n²) | O(n²) | O(n) | 稳定 | 数据量小,基本有序 |
| 快速 | O(n log n) | O(n²) | O(n log n) | 不稳定 | 数据量大,乱序 |
| 归并 | O(n log n) | O(n log n) | O(n log n) | 稳定 | 数据量大,外部排序 |
| 堆 | O(n log n) | O(n log n) | O(n log n) | 不稳定 | 数据量大且需要最值 |
| 计数 | O(n + k) | O(n + k) | O(n + k) | 稳定 | 数据范围有限且是整数时,大量重复元素 |
| 基数 | O(nk) | O(nk) | O(nk) | 稳定 | 数据范围有限且数据较大时 |
| 桶 | O(n + k) | O(n²) | O(n + k) | 稳定 | 数据均匀分布 |
