排序
总述
- 按照升序重新排列给定列表中的数据项
- 为了让问题有意义,列表中的数据项应该能够排序(数据之间 有一种全序关系)
- 键:在对记录排序时,需要选取的、作为排序的依据的一段信息
目的
- 排序可能是所求解的问题输出要求
- 排序能够更方便的求解和列表相关的问题
- 查找问题
- 在其他领域的重要算法中,排序也被作为辅助步骤
发展
排序领域已经有很多不错的算法,只需要做$nlog_{x}^{n}$次 比较就能完成长度为$n$的任意数组排序,且没有一种基于 键值比较(相较于比较键值部分内容而言)的排序算法能 在本质上操作其,
但是还是需要不断探寻新的算法虽然有些算法比其他的要好, 但是没有任何算法在任何情况下是最优的
- 有些算法比较简单,但速度较慢
- 有些算法适合随机排列的输入,而有些适合基本有序的列表
- 有些算法适合驻留在快速存储器中的列表,而有些适合存储 在磁盘上的大型文件排序
评价
- 稳定性:排序算法保留等值元素在输入中的相对顺序
- 一般来说,将相隔很远的键交换位置的算法虽然不稳定, 但往往很快
- 在位性:排序算法不需要额外的存储空间,除极个别存储单元外
线性表排序
选择排序
- 每轮选择出剩余元素中最小值,放在对于位置上
顺序表算法
1 | SelectionSort(A[0..n-1]): |
- 扫描整个列表找到最小元素,然后和首个元素交换,将其放在 最终位置上
- 从第2个元素开始寻找之后最小元素,和第2个元素交换,将其 放在最终位置上
- 重复n-1次,列表有序
链表算法
1 | SelectionSort(linked): |
特点
- 对任何输入,选择排序键值比较都是$\Theta(n^2)$
- 键交换次数仅为$\Theta(n)$
- 选择排序此特性优于许多其他排序算法
冒泡排序
- 比较表中相邻元素,如果逆序就交换位置
- 重复多次则最大元素被“沉到”列表最后位置
- 第2轮比较将第2大元素“沉到”其最终位置
- 重复比较n-1轮,列表有序
顺序表算法
1 | BubbleSort(A[0..n-1]): |
链表算法
1 | BublleSort(head): |
特点
- 对任何输入,冒泡排序键值比较都是$\Theta(n^2)$
- 其交换次数取决于特定输入
- 最坏情况是遇到降序排列数组,此时键交换次数同比较次数
- 冒泡排序看起来就像是选择排序的一直交换+最大优先版本
改进
- 如果对列表比较一轮后没有交换任何元素,说明列表有序,可以 结束算法
Insertion Sort
插入排序:利用减一技术对数组$A[0..n-1]$进行排序
算法
- 假设对较小数组$A[0..i-2]$排序已经解决
- 从右至左(方便将将元素右移)扫描有序子数组,直到遇到首个 小于等于$A[i-1]$元素,将$A[i-1]$插入其后
1 | InsertionSort(A[0..n-1]) |
特点
插入排序是自顶向下基于递归思想,但是自底向上使用迭代实现 算法效率更高
算法时间效率
- 算法基本操作是键值比较,比较次数依赖于特定输入
- 最坏情况(递减数组)下:每轮比较次数都到达最大,此时 键值比较次数$\in \Theta(n^2)$
- 最优情况(递增数组)下:每轮比较次数仅为1,此时键值 比较次数$\in \Theta(n)$
- 对随机序列:比较次数$\in \Theta(n^2)$
- 许多应用中都会遇到基本有序的文件,插入排序能保证良好 性能
减常量法
Shell Sort
插入排序的扩展,Shell排序基于h有序
H有序
数组中任意间隔为h的元素都是有序的,对应的数组称为h有序数组
- h有序数组:就是h个互相独立的有序数组编织在一起数组
- h有序数组具有分离、局部有序的特点
算法
- 使用插入排序对h子数组独立排序,每次交换相隔h的元素
- h逐渐减小到1,shell排序退化(最后一轮)为插入排序
1 | ShellSort(A[0..n-1]) |
特点
Shell排序全衡量子数组规模、有序性,更加高效
h递增序列
- 子数组部分有序程度取决于h递增序列的选择
- 不同的h递增序列对算法效率有常数倍的变动,但是在实际 应用中效果不明显
Shell排序是唯一无法准确描述其对于乱序数组性能特征的排序 方法
减常量法
归并排序
Mergesort
- 将需要排序的数组A[0..n-1]均分的
- 对两个子数组递归排序,然后把排序后的子数组合并为有序 数组
1 | Mergesort(A[0..n-1]): |
Merge
- 初始两只指针指向待合并数组首个元素
- 比较两个元素大小,将较小元素添加到新数组中,被复制元素 的数组指针右移
- 重复直到某一数组处理完毕,将未处理完数组复制到新数组尾部
1 | Merge(B[0..p-1], C[0..q-1], A[0..p+q-1]): |
特点
算法时间效率
- 最坏情况下比较次数$\in \Theta(nlogn)$,为 $nlog_2n-n+1$,十分接近基于比较的排序算法理论上能够 达到的最少次数
相较于快排、堆排序
- 合并排序比较稳定,缺点在于需要线性额外空间
- 虽然能做到在位,但是算法过于复杂,只具有理论上意义
算法可以自底向上合并数组的元素对,再合并有序对
- 这可以避免使用堆栈递归调用时时空开销
可以把数组划分为待排序的多个部分,再对其递归排序,最后 合并在一起
- 这尤其适合对存在二级存储空间的文件进行排序
- 也称为Multiway Mergesort
分治算法
快速排序
相较于合并排序按照元素在数组中的位置进行划分,快速排序按照 元素值对其进行划分
Quicksort
- 对数组中元素重新排列,使得A[s]左边元素均小于A[s],A[s]
右边元素都大于等于A[s]
- 此时A[s]已经位于它在有序数组中的最终位置
- 接下来对A[s]左右子数组进行排序
1 | Quicksort(A[l..]r) |
特点
需要选择合适的划分算法J
- LomutoPartition
- HoarePartition
算法效率
- 最优情况:分裂点都位于数组中点,算法比较次数为 $nlog_2n$
- 最差情况:分裂点都趋于极端(逆序输入),算法比较 次数$\in \Theta(n^2)$
- 平均:比较次数为$2nlnn \approx 1.39nlog_2n$
- 且算法内层循环效率非常高,在处理随机排列数组时,速度 比合并排序快
快速排序缺点
- 不稳定
- 需要堆栈存储未被排序的子数组参数,尽管可以先对较短 子数组排序使得堆栈大小降低到$O(logn)$,但是还是比 堆排序的$O(1)$差
- 仍然存在最差情况出现的可能性,即使有很多巧妙地中轴 选择办法
- 对随机数组排序性能的好坏,不仅与算法具体实现有关, 还和计算机系统结构、数据类型有关
分治算法
算法改良
更好的中轴选择方法
- randomized quicksort:随机快速排序,使用随机元素作为 中轴
- median-of-three method:三平均划分法,以数组最左边、 最右边、最中间的中位数作为中轴
子数组足够小时(5~15),改用插入排序;或者直接不对小数组 排序,而是在快速排序结束后使用插入排序对整个近似有序的 数组进行排序
改进划分方法
- 三路划分:将数组划分为3段,小于、等于、大于中轴元素
比较计数排序
算法
- 遍历待排序列表中每个元素
- 计算、记录列表中小于该元素的元素个数
- 更新大于其的元素的小于元素的元素个数
1 | ComparisonCountingSort(A[0..n-1]) |
特点
算法效率
- 算法比较次数为$n(n-1)/2$
- 占用了线性数量的额外空间
算法使得键值的可能移动次数最小化,能够直接将键值放在在 有序数组最终位置
输入增强
分布计数排序
- 待排序元素来自于某个已知小集合$[l..u]$
算法
- 扫描列表,计算、存储列表中元素出现频率于数组$F[l..u]$中
- 再次扫描列表,根据值填入相应位置
1 | DistributionCountingSort(A[0..n-1], l, u): |
特点
算法效率
- 如果元素值范围固定,效率为线性(不是基于比较的排序, $nlogn$没有限制)
- 用空间换时间,其实可以看作是hash
- 利用了输入列表独特自然属性
变治法(输入增强)
应用
- 判断线性表元素是否唯一
- 寻找线性表众数
- 快速查找
线性表顺序统计量
寻找列表中第k小的元素
也即:求出给定列表中k个最小元素问题
采用partitioning的思路,需要将给定列表根据某个值先行划分
- Lumuto划分算法
QuickSelect算法
算法
- 对划分完后出数组,s为分割位置
- 若:s == k-1,则中轴p就是第k小元素
- 若:s < k-1,则应该是数组右边划分第k-s小元素
- 若:s > k-1,则应该是数组左边划分第k小元素
- 这样就得到规模更小的问题实例
1 | QuickSelect(A[l..r], k) |
特点
时间效率:和划分算法有关(对Lomuto算法)
- 最好情况下只需要划分一次即找到,需要比较$n-1$次
- 最坏情况下需要比较$n(n-1)/2$次,这比直接基于排序更差
- 数学分析表明,基于划分的算法平均情况下效率是线性的
已经找到复杂算法替代Lomuto算法用于在快速选择中选出 中轴,在最坏情况下仍保持线性时间效率
QuickSelect算法可以不用递归实现,且非递归版本中甚至 不需要调整参数k值,只需要最后
s == k-1
即可减可变规模:Lomuto算法性质
线性表有序划分
LomutoPartition
算法
考虑子数组A[l..r]分为三段,按顺序排在中轴p之后
- 小于p的元素,最后元素索引记为s
- 大于等于p的元素,最后元素索引记为i
- 未同p比较过元素
从左至右扫描A[l..r],比较其中元素和p大小
- 若
A[i] >= p
,扩大大于等于p元素段 - 若
A[i] < p
,需要扩大小于等于p元素段
- 若
1 | LomutoPartition(A[l..r]) |
HoarePartition
算法
选择一个元素p作为中轴(最简单的,首个元素p=A[l])
从数组A[l..r]两端进行扫描,将扫描到的元素同中轴比较
- 从左至右的扫描忽略小于中轴元素,直到遇到大于等于中轴 元素停止(从第二个元素开始i=l+1)
- 从右至左的扫描忽略大于中轴元素,直到遇到小于等于中轴 元素停止(j=r-1)
若扫描停止后
- 两指针不相交i < j,交换A[i]、A[j],i=i+1、j=j-1, 继续扫描
- 若指针相交i > j,把中轴和A[j]交换即得到数组一个划分 ,分裂点为s=j
- 如果指针重合i==j,此元素一定等于p,也得到数组的一个 划分,分裂点为s==i==j
1 | HoarePartition(A[l..r]) |