第二部分 排序和顺序统计量
一些概念
排序问题
- 输入:一个
n
个数的序列
<a1,a2,...,an> ; - 输出:输入序列的一个排列(重排) <a′1,a′2,...,a′n> <script id="MathJax-Element-3" type="math/tex"> </script>,使得 a′1≤a′2≤...≤a′n 。
数据结构
在实际中待排序的数很少是单独的数值,它们通常是记录的一部分。每个记录包含一个关键字,这也是排序问题中要重排的值;记录的其他部分我们称为卫星数据,它和关键字是一同存取的。
学习排序的目的
排序是算法研究中最基础的问题。
- 有些应用需要对信息进行排序,如准备报表时,银行按照编号对支票排序;
- 很多算法将排序作为关键子程序,如Photoshop里面的图层,有顶层,底层等按照上下关系的排序图层;
- 排序中有很多有用的技术,排序可以作为掌握这些技术的一个应用实例;
- 排序问题的下界可作为其他问题下界证明的参照;
- 在实现排序算法时出现的工程问题,能让我们意识到很多问题,需要从算法层面,而非“代码调优”层面去解决。
排序算法
原址
如果输入数组中仅有常数个元素需要在排序过程中存储在元素之外,则称该排序算法为原址的。
常用排序算法对比
算法 | 最坏情况运行时间 | 平均情况/期望运行时间 | 空间原址性 |
---|---|---|---|
插入排序 | Θ(n2) | Θ(n2) | 是 |
归并排序 | Θ(nlgn) | Θ(nlgn) | 否 |
堆排序 | O(nlgn) | — | 是 |
快速排序 | Θ(n2) | Θ(nlgn) (期望) | 是 |
计数排序 | Θ(n+k) | Θ(n+k) | 否 |
基数排序 | Θ(d(n+k) | Θ(d(n+k) | 否 |
桶排序 | Θ(n2) | Θ(n) (平均情况) | 否 |
为什么快速排序是目前来看最好的选择?
- 快速排序具有空间原址性且平均情况运行 时间较小;
- 我们注意到堆排序最坏情况是 O(nlgn) 且具有空间原址性,按道理讲应该堆排序更好一点,这里我们需要注意的是快速排序虽然最坏情况为 Θ(n2) ,但在实际应用中却几乎不会出现,更常见的是平均情况;还有的就是堆排序相对于快速排序而言, Θ(nlgn) 中的隐藏常数因子要大得多。
顺序统计量
一个
n
个数的集合的第
第六章 堆排序
堆排序
(二叉)堆**是一个数组,它可以被看成一个近似的完全二叉树。树上的每一个结点对应数组的一个元素。除了最底层外,该树是完全充满的,而且是从左往右填充。
堆中父结点,左孩子,右孩子下标关系如下所示
堆的两种形式:最大堆和最小堆。最大堆是指除了根以外的所有结点
如何描述堆排序?
- 堆排序是指利用堆这种数据结构所设计的一种排序算法,它是一种具有空间原址性的选择排序,时间复杂度为 O(nlgn) ;
- 堆这种数据结构,我们可以把它近似看成二叉树,它有两种类型:最大堆和最小堆;最大堆要求,除根结点以外的所有结点
i
都要满足:
A[Parent(i)]≥A[i] ,相反,最小堆则要求除根结点以外的所有结点 i 都要满足:A[Parent(i)]≤A[i] ; - 堆排序的非降序排列采用的是最大堆。堆排序利用堆能够自我维护的特性,在不断取出堆根结点的同时,对堆剩余元素重新建立最大堆,直至堆中只剩下根结点。
堆排序的核心步骤
- MAX-HEAPIFY:维持最大堆性质的关键,时间复杂度为 O(lgn) ;
- BUILD-MAX-HEAP:从无序的数据中构建最大堆,时间复杂度为 Θ(n) ;
- HEAPSORT:堆排序,原址排序。
堆排序伪代码
代码简单解析:
MAX−HEAPIFY
是维持最大堆的关键函数,当最大堆中结点
i
改变时,通过
时间复杂度分析: MAX−HEAPIFY 时间复杂度为 Θ(lg(n)) ; MAX−HEAPIFY 时间复杂度为 O(n) ; HEAP−SORT 时间复杂度为 O(nlg(n)) 。
优先队列
描述:优先队列是一种用来维护由一组元素构成的集合 S 的数据结构,其中每一个元素都有一个相关的值,称为关键字。一个最大优先队列支持以下操作:
INSERT(S,x) :把 x 插入集合S 中,操作等价于 S=S∪{x} ;- MAXIMUM(S) :返回 S 中具有最大关键字的元素;
EXTRACT−MAX(S) :去除并返回 S 中的最大元素;INCRESE−KEY(S,x,k) :将元素 x 的关键字增加到k ,当然这里假设 x 的关键字不大于k 。优先队列的应用:在共享计算机系统中进行作业调度。最大优先队列记录将要执行的各个作业以及它们之间的相对优先级,当一个作业完成或被中断时,调度器用 EXTRACT−MAX 从所有等待的作业中,选出具有最高优先级的作业来执行。在任何时候,调度器都可以调用 INSERT 把一个新作业加入到队列中。
优先队列相关伪代码
所有最大优先队列的操作时间复杂度均为 O(lg(n)) 。
第七章 快速排序
一句话描述快速排序
快 速排序是目前来说最常用也是效果也最好的排序方法。它具有空间原址性,期望时间复杂度为 Θ(nlg(n)) ,且 Θ(nlg(n)) 中隐藏的常数因子非常小。它的原理是从待排序的数组中选出一个元素作为主元,根据主元将待排序数组分为两个子数组(一个由比主元小的元素组成,另一个由比主元大的元素组成),之后根据分治策略,分别对两个子数组进行相同操作,直至子数组中只剩下一个元素(也就是基本情况)。快速排序时间复杂度依赖于主元的选取,在实际应用时,一般会采取随机算法选取主元,以确保处理数据避免出现最坏情况。
思考:是否可采用第九章第3节的中位数方法,来确定主元?
快速排序分析
- 快速排序是目前实际应用中最好的选择,它具有空间原址性;
- 它的最坏情况时间复杂度为 Θ(n2) ,但是它的期望时间复杂度为 Θ(nlg(n)) ,且 Θ(nlg(n)) 中的隐藏因子非常小。
分治思想考虑快速排序
- 分解:数组 A[p...r] 被划分为两个子数组 A[p...q−1] 和 A[q+1...r] ,使得 A[p...q−1] 中的每一个元素都小于等于 A[q] ;而 A[q+1...r] 中的每一个元素都大于等于$A[q];
- 解决:通过递归调用快速排序,对子数组 A[p...q−1] 和 A[q+1...r] 进行排序;
- 合并:因为子数组都是原址排序的,所以不需要合并操作,数组 A[p...r] 已经有序。
快速排序伪代码
循环不变性分析 PARTITION 内的迭代操作
这里的循环不变式为:
- 当 p≤k≤i 时, A[k]≤A[r] ;
当 i+1≤k≤j−1 时, A[k]≥A[r] ;
- 初始化:迭代开始时, i=p−1,j=p ,可知 A[p...i],A[i+1...j−1] 均为空,可知循环不变式成立;
- 保持:第
j
次迭代之前循环不变式成立,即有
A[k]≤A[r],k=p...r;A[k]≥A[r],k=r+1...j−2 ;当第 j 次迭代后,假设A[j]≤A[r] ,则交换 A[i+1] 和 A[j] ,且 i=i+1,j=j+1 ,因为迭代之前有 A[i+1]≤A[r],A[j]≤A[r] ,所以迭代之后,交换 A[i+1],A[j] 且自增 i,j 后,则仍有循环不变式成立;反之 A[j]>A[r] ,除了 j 自增1之外无其他操作,所以仍有循环不变式成立; - 终止:当迭代结束时,
j=r 则有 A[p...i]≤A[r],A[i+1...r−1]≥A[r] ,由 PARTITION 的目的可知,它是为了按照主元 A[r] 将 A[p...r−1] 分成满足循环不变式的两部分,并且算法最后两行将 A[r] 和 A[i+1] 交换,可知数组被关键字 q=i+1 分成了两个子数组 A[p...q] 和 A[q+1...r] 算法正确。
快速排序的性能
最好情况划分
- 每次都是划分成两个规模相等的子问题;
- T(n)=2T(n/2)+Θ(n) ,所以时间复杂度为 Θ(nlgn) 。
最坏情况划分
- 每次划分后的子问题都有一个为空;
- T(n)=T(n−1)+Θ(n) ,将每一层的代价叠加可知,时间复杂度为 Θ(n2) 。
平均情况(期望)划分
- 随机化版本,即在数组 A 中根据均匀分布选择主元
- 考察之后可知,其平均情况时间复杂度为
Θ(nlgn) 。
第八章 线性时间排序
本章前言
对之前的排序进行总结:均属于比较排序。
- 比较排序:在排序的最终结果中,各元素的次序依赖于它们之间的比较;有插入排序,归并排序,堆排序,快速排序。
本章介绍内容:三种线性时间复杂度的排序算法——计数排序,基数排序,桶排序
比较排序的最坏情况下界: Ω(nlgn)
计数排序
限制:输入为一个小区间的整数,它假设 n 个输入元素中的每一个元素都在
0 到 k 区间内的整数,当k=O(n) ,一般计数排序要求 k 不能太大;基本思想:对于每一个输入元素
x ,确定小于等于 x 的元素个数。利用这一信息,将x 放在合适位置。计数排序伪代码
基数排序
基数排序很简单,它是一种用在卡片排序机上的排序算法。它的限制是排序的数据必须是整数且基础排序算法必须是稳定的,通过从低到高分别对不同位进行排序,从而最后实现对整个数组的排序,一般把计数排序(稳定的)作为基数排序对每一位的数据进行排序的基础算法。
基数排序伪代码
基数排序和快速排序的对比
- 虽然基数排序时间复杂度为 Θ(n) ,比快速排序 Θ(nlgn) 看上去更好,但其中隐藏的常数因子却大得多;
- 基数排序借助的稳定排序(通常为计数排序)是非空间原址的,而快速排序是空间原址的。
桶排序
桶排序的限制
- 严条件:输入数据服从均匀分布 [a,b)
- 宽条件:所有桶的大小的平方和与总元素数呈线性关系
满足任何一个条件都可以。
桶排序伪代码
桶排序平均情况时间复杂度分析
首先我们知道插入排序的时间代价为 O(n2) ,我们假设第 i 个桶里的元素个数为
ni ,所以我们知道第 11−12 行代码的时间代价为 ∑n−1i=0O(n2i) ,所以桶排序的总时间代价为 T(n)=Θ(n)+∑n−1i=0O(n2i) 。求其期望时间代价为 E[T(n)]=E[Θ(n)+T(n)]=Θ(n)+E[∑n−1i=0O(n2i)]=Θ(n)+T(n)=Θ(n)+∑n−1i=0O(E[n2i])接下来,我们说明 E[T(n)]=Θ(n) :
我们定义指示变量:对于所有 i=0...n−1 和 j=1...n , Xij=I{A[j] in Bucket[i]} ;因为输入数组 A 均匀分布,所以每个元素等概率落入每个桶中,故每个桶具有相同的期望值
E[n2i] 且 ni=∑nj=1Xij
E[n2i]=E[(∑j=1nXij)2]=E[∑j=1n∑k=1nXijXik]=∑j=1nE[X2ij]+∑1≤j≤n∑1≤k≤n;k≠jE[XijXik]E[X2ij]=12⋅1n+02⋅(1−1n)当k≠j时,Xij和Xik相互独立,所以E[XijXik]=1n⋅1n=1n2∴ E[n2i]=n⋅1n+(n2−n)⋅1n2=2−1n
所以 E[T(n)]=Θ(n)+∑n−1i=0O(E[n2i])=Θ(n)+n⋅O(2−1n)=Θ(n)分析完毕。
第九章 中位数和顺序统计量
一句话:这一章主要讨论的是选择问题,其中涉及到最大元素,最小元素,中位数以及一般地,第 i 大的元素的选择。诚然,根据之前的介绍,我们可以先根据排序算法对数组进行排序,然后根据下标进行访问,但这样所需的时间代价为
O(nlgn) ,本章的目的是提出期望为线性时间的选择算法。概念
顺序统计量:第 i 个顺序统计量就是该集合中第
i 小的元素。选择问题
- 输入:一个包含
n
个(互异的)元素的集合和一个整数
i , 1≤i≤n 。 - 输出:元素
x∈A
,且
A
中恰好有
i−1 个其他元素小于它。
求最小值时间代价: Θ(n) ,比较次数为 n ;如果同时求最大和最小值,其时间代价为
Θ(n) ,但是我们可以将比较次数由 2n 降到 3⌊n/2⌋ 。期望为线性时间的选择算法
RANDOMIZED−SORT 算法是以快速排序算法为模型的,但它与快速排序不同的是, RANDOMIZED−SORT 只需要处理一边,使得其期望时间代价降为 Θ(n) ,这里就不证明了,可自行参考算法导论第九章教材。
结论:假设所有元素是互异的,在期望线性时间内,我们可以找到任一顺序统计量,特别是中位数。
最坏情况为线性时间的选择算法
由于快速排序在最坏情况时,其时间复杂度为 Θ(n2) ;为了避免出现最坏情况,一种办法是采用随机算法选取主元,当然我们可以采用另一种更好的选择主元的方式,使得算法即使在最坏情况下,时间代价也是 Θ(n) 。
其选择主元的方式称为 SELECT 算法:
将输入数组的 n 个元素划分为
⌈n/5⌉ 组,每组5个元素,且至多只有一组由剩下的 nmod5 个元素组成;寻找这 ⌈n/5⌉ 组中每一组中位数:首先在组内进行插入排序,然后选择每组的中位数;
对2中找出的 ⌈n/5⌉ 个中位数,递归调用 SELECT 以找出中位数 x ;
利用修改过的
PARTITION 版本,按中位数的中位数 x 对输入数组进行划分。如果最后中位数x 落在数组 A 的第k 个位置,表明 x 是A 中第 k 小的元素;如果
i=k ,则返回 x ,如果i<k ,则在低区(元素值均比 x 小)递归调用SELECT 来查找第 i 小元素;否则在高区递归调用SELECT 来查找第 i−k 小的元素。
写出它的伪代码
总结
介绍了数组排序的各种算法:比较排序,线性时间复杂度排序;在最后一行介绍了选择算法,并且指出选择算法的时间复杂度也可以是线性的。