一、快速排序
1.轴点
1.1 分而治之
- 轴点(pivot):左侧/右侧的元素,均不比它更大/更小
- 以轴点为界,自然划分:max( [0, mi) ) ≤ min( (mi, hi) )
- 前缀、后缀各自(递归)排序之后,原序列自然有序
- sorted(S) = sorted(SL) + sorted(SR)
- 归并排序(mergesort)难点在于合,而快速排序(quicksort)在于分
1.2 快速排序
template <typename T> void Vector<T>::quickSort( Rank lo, Rank hi ) {
if ( hi - lo < 2 ) return;
Rank mi = partition( lo, hi );
quickSort( lo, mi );
quickSort( mi + 1, hi );
}
1.3 轴点
- 必要条件: 轴点必定已然就位
- 特别地: 在有序序列中,所有元素皆为轴点,反之亦然
- 快速排序: 就是将所有元素逐个转换为轴点的过程
- 絮乱(derangement): 任何元素都不在原位。比如,顺序序列循环移位
- 不需很多交换,即可使任一元素转为轴点
2.快速划分:LUG版
2.1 减而治之,相向而行
-
任取一个候选者(如[0])
-
L + U + G
-
交替地向内移动lo和hi
-
逐个检查当前元素: 若更小/大,则转移归入L/G
-
当lo = hi时,只需 将候选者嵌入于L、G之间,即成轴点
-
各元素最多移动一次(候选者两次) ——累计O(n)时间、O(1)辅助空间
2.2 快速划分:LUG版
template <typename T> Rank Vector<T>::partition( Rank lo, Rank hi ) { //[lo, hi)
swap( _elem[ lo ], _elem[ lo + rand() % ( hi - lo ) ] ); //随机交换
hi--; T pivot = _elem[ lo ]; //经以上交换,等效于随机选取候选轴点
while ( lo < hi ) { //从两端交替地向中间扫描,彼此靠拢
while ( lo < hi && pivot <= _elem[ hi ] ) hi--; //向左拓展G
_elem[ lo ] = _elem[ hi ]; //凡小于轴点者,皆归入L
while ( lo < hi && _elem[ lo ] <= pivot ) lo++; //向右拓展L
_elem[ hi ] = _elem[ lo ]; //凡大于轴点者,皆归入G
} //assert: lo == hi
_elem[ lo ] = pivot; return lo; //候选轴点归位;返回其秩
}
2.3 不变性 + 单调性:L ≤ pivot ≤ G; U = [lo, hi]中,[lo]和[hi]交替空闲
2.4 实例
- 线性时间:尽管lo、hi交替移动,累计移动距离不过O(n)
- in-place只需O(1)附加空间
- unstable
- lo/hi的移动方向相反
- 左/右侧的大/小重复元素可能前/后颠倒
3.迭代、贪心与随机
3.1 空间复杂度 ~ 递归深度
- 最好:划分总均衡
- 最差:划分皆偏侧
- 平均:均衡不致太少
3.2 非递归 + 贪心
#define Put( K, s, t ) { if ( 1 < (t) - (s) ) { K.push(s); K.push(t); } }
#define Get( K, s, t ) { t = K.pop(); s = K.pop(); }
template <typename T> void Vector<T>::quickSort( Rank lo, Rank hi ) {
Stack<Rank> Task; Put( Task, lo, hi ); //等效于对递归树的先序遍历
while ( ! Task.empty() ) {
Get( Task, lo, hi ); Rank mi = partition( lo, hi );
if ( mi-lo < hi-mi ) { Put( Task, mi+1, hi ); Put( Task, lo, mi ); }
else { Put( Task, lo, mi ); Put( Task, mi+1, hi ); }
} //大|小任务优先入|出栈,可保证(辅助栈)空间不过O(logn) 非递归
}
3.3 时间性能 + 随机
- 最好情况:每次划分都(接近)平均,轴点总是(接近)中央 —— 到达下界
- 最坏情况:每次划分都极不均衡(比如,轴点总是最小/大元素) —— 与起泡排序相当
- 采用随机选取(Randomization)、三者取中(Sampling)之类的策略,只能降低最坏情况的概率,而无法杜绝
4.递归深度
4.1 居中 + 偏侧
- 最坏情况(递归深度),概率极低
- 平均情况(递归深度),概率极高
- 实际上:除非过于侧偏的pivot,都会有效地缩短递归深度
- 准居中:pivot落在宽度为λ·n的居中区间 (λ也是这种情况出现的概率)
- 每一递归路径上,至多出现个准居中的pivots
4.2 期望深度
- 每递归一层,都有λ(1-λ)的概率准居中(准偏侧)
- 深入层后,即可期望出现次准居中,且有极高的概率出现
- 相反情况的概率,且随着λ增加而下降,比如λ>1/3之后,即至少有的概率,使得递归深度不超过
5.比较次数
5.1 递推分析
- 记期望的比较次数为T(n):T(1)=0,T(2)=1
5.2 后向分析(Backward Analysis)
-
设经排序后得到的输出序列为:
-
这一输出与具体使用何种算法无关,故可使用Backward Analysis
-
比较操作的期望次数应为,亦即,每一对在排序过程中接受比较之概率的总和
-
quickSort的过程及结果,可理解为:按某种次序,将各元素逐个确认为pivot
-
若,则早于或晚于和被确认,均与Pr(i,j)无关
-
实际上,接受比较,当且仅当在 中,或率先被确认
5.3 对比
| #compare | #move (对实际性能影响更大) | in-placed | |
|---|---|---|---|
| Quicksort | 平均 且高概率接近 | 平均不超过 且实际更少 | ✓ |
| Mergesort | 严格 | 严格 实际往往加倍 | ✗ |
6.快速划分:DUP版
6.1 重复元素
-
大量甚至全部元素重复时
- 轴点位置总是接近于lo
- 子序列的划分极度失衡
- 二分递归退化为线性递归
- 递归深度接近于O(n)
- 运行时间接近于O(n2)
-
移动lo和hi的过程中,同时比较相邻元素,若属于相邻的重复元素,则不再深入递归
-
但一般情况下,如此计算量反而增加,得不偿失
6.2 算法
template <typename T> Rank Vector<T>::partition( Rank lo, Rank hi ) { //[lo, hi)
swap( _elem[ lo ], _elem[ lo + rand() % ( hi – lo ) ] ); //随机交换
hi--; T pivot = _elem[ lo ]; //经以上交换,等效于随机选取候选轴点
while ( lo < hi ) { //从两端交替地向中间扫描,彼此靠拢
while ( lo < hi )
if ( pivot < _elem[ hi ] ) hi--; //向左拓展G,直至遇到不大于轴点者
else { _elem[ lo++ ] = _elem[ hi ]; break; } //将其归入L
while ( lo < hi )
if ( _elem[ lo ] < pivot ) lo++; //向右拓展L,直至遇到不小于轴点者
else { _elem[ hi-- ] = _elem[ lo ]; break; } //将其归入G
} //assert: lo == hi
_elem[ lo ] = pivot; return lo; //候选轴点归位;返回其秩
}
6.3 性能
-
可以正确地处理一般情况,同时复杂度并未实质增高
-
处理重复元素时
- lo和hi会交替移动
- 二者移动的距离大致相当,轴点最终被安置于(lo+hi)/2处
-
由LUG版的勤于拓展、懒于交换,转为懒于拓展、勤于交换
-
交换操作有所增加,更不稳定
7.快速划分:LGU版
7.1 不变性
7.2 单调性
pivot <= S[ k ] ? k++ : swap( S[ ++mi ], S[ k++ ] )[k]不小于轴点 ? 直接 G 拓展 : G 滚动后移, L 拓展
7.3 算法
template <typename T> Rank Vector<T>::partition( Rank lo, Rank hi ) { //[lo, hi)
swap( _elem[ lo ], _elem[ lo + rand() % ( hi – lo ) ] ); //随机交换
T pivot = _elem[ lo ]; int mi = lo;
for ( Rank k = lo + 1; k < hi; k++ ) //自左向右考查每个[k]
if ( _elem[ k ] < pivot ) //若[k]小于轴点,则将其
swap( _elem[ ++mi ], _elem[ k ]); //与[mi]交换,L向右扩展
swap( _elem[ lo ], _elem[ mi ] ); //候选轴点归位(从而名副其实)
return mi; //返回轴点的秩
}
7.4 实例
二、选取
1.众数
1.1 选取 + 中位数
- 中位数(median):长度为n的有序序列S中,元素称作中位数
- 中位数是k-选取的一个特例,也是其中难度最大者
1.2 众数(Majority)
- 无序向量中,若有一半以上元素同为m,则称之为众数
- 必要性:众数若存在,则亦必中位数
- 事实上,只要能够找出中位数,即不难验证它是否众数
template <typename T> bool majority( Vector<T> A, T & maj ) {
return majEleCheck( A, maj = median( A ) );
}
1.3 必要条件
- mode():众数若存在,则亦必频繁数
template <typename T> bool majority( Vector<T> A, T & maj ) {
return majEleCheck( A, maj = mode( A ) );
}
- 同样地,mode()算法难以兼顾时间、空间的高效
- 可行思路:借助更弱但计算成本更优的必要条件,选出唯一的候选者
template <typename T> bool majority( Vector<T> A, T & maj ) {
return majEleCheck( A, maj = majEleCandiate( A ) );
}
1.4 减而治之
- 若在向量A的前缀P(|P|为偶数)中,元素 x 出现的次数恰占半数,则A有众数,仅当对应的后缀 A − P 有众数m,且m就是 A 的众数
- 既然最终总要花费O(n)时间做验证,故而只需考虑A的确含有众数的两种情况:
- 若x = m,则在排除前缀 P 之后,m与其它元素在数量上的差距保持不变
- 若x ≠ m,则在排除前缀 P 之后,m与其它元素在数量上的差距不致缩小
1.5 算法
template <typename T> T majCandidate( Vector<T> A ) {
T maj;
for ( int c = 0, i = 0; i < A.size(); i++ )
if ( 0 == c ) {
maj = A[i]; c = 1;
} else maj == A[i] ? c++ : c--;
return maj;
}
2.中位数
2.1 归并向量的中位数
- 任给有序向量和,长度和
- 蛮力: 经归并得到有序向量S取即是
- 如此,共需时间,但毕竟未能充分利用和的有序性
- 以下,先解决的情况,依然采用减而治之策略
2.2 等长子向量
- 考查:和
- 若,则它们同为、和的中位数
- 若,则n无论偶奇,必有:,这意味着,如此减除(一半规模)之后,中位数不变
- 时同理
template <typename T> //尾递归,可改写为迭代形式
T median( Vector<T> & S1, int lo1, Vector<T> & S2, int lo2, int n ) {
if ( n < 3 ) return trivialMedian( S1, lo1, n, S2, lo2, n ); //递归基
int mi1 = lo1 + n/2, mi2 = lo2 + (n - 1)/2; //长度减半
if ( S1[ mi1 ] < S2[ mi2 ] ) //取S1右半、S2左半
return median( S1, mi1, S2, lo2, n + lo1 - mi1 );
else if ( S1[ mi1 ] > S2[ mi2 ] ) //取S1左半、S2右半
return median( S1, lo1, S2, mi2, n + lo2 - mi2 );
else
return S1[ mi1 ];
}
2.3 任意子向量
template <typename T>
T median ( Vector<T> & S1, int lo1, int n1, Vector<T> & S2, int lo2, int n2 ) {
if ( n1 > n2 )
return median( S2, lo2, n2, S1, lo1, n1 ); //确保n1 <= n2
if ( n2 < 6 )
return trivialMedian( S1, lo1, n1, S2, lo2, n2 ); //递归基
if ( 2 * n1 < n2 )
return median( S1, lo1, n1, S2, lo2 + (n2-n1-1)/2, n1+2-(n2-n1)%2 );
int mi1 = lo1 + n1/2, mi2a = lo2 + (n1 - 1)/2, mi2b = lo2 + n2 - 1 - n1/2;
if ( S1[ mi1 ] > S2[ mi2b ] ) //取S1左半、S2右半
return median( S1, lo1, n1 / 2 + 1, S2, mi2a, n2 - (n1 - 1) / 2 );
else if ( S1[ mi1 ] < S2[ mi2a ] ) //取S1右半、S2左半
return median( S1, mi1, (n1 + 1) / 2, S2, lo2, n2 - n1 / 2 );
else //S1保留,S2左右同时缩短
return median( S1, lo1, n1, S2, mi2a, n2 - (n1 - 1) / 2 * 2 );
} //O( log(min(n1,n2)) )——可见,实际上等长版本才是难度最大的
3.快速选取(QuickSelect)
3.1 尝试:蛮力
- 对A排序(),从首元素开始,向后行进k步()
3.2 尝试:堆(A)
- 将所有元素组织为小顶堆(),连续调用k+1次delMin() ()
3.3 尝试:堆(B)
L = heapify( A[0, k] ) //任选 k+1 个元素,组织为大顶堆:O(k)
for each i in (k, n) //O(n - k)
L.insert( A[i] ) //O(logk)
L.delMax() //O(logk)
return L.getMax()
3.4 尝试:堆(C)
- 将输入任意划分为规模为k、n-k的子集,分别组织为大、小顶堆 //O(k + (n-k)) = O(n)
while ( m < M ) //O(min(k, n – k))
swap( m, M )
L.percolateDown() //O(logk)
G.percolateDown() //O(log(n - k))
return m = G.getMin()
3.5 下界与最优
- 所谓第k小,是相对于序列整体而言,所以在访问每个元素至少一次之前,绝无可能确定
3.6 算法
template <typename T> void quickSelect( Vector<T> & A, Rank k ) {
for ( Rank lo = 0, hi = A.size() - 1; lo < hi; ) {
Rank i = lo, j = hi; T pivot = A[lo]; //大胆猜测
while ( i < j ) { //小心求证:O(hi - lo + 1) = O(n)
while ( i < j && pivot <= A[j] ) j--; A[i] = A[j];
while ( i < j && A[i] <= pivot ) i++; A[j] = A[i];
} //assert: quit with i == j
A[i] = pivot;
if ( k <= i ) hi = i - 1;
if ( i <= k ) lo = i + 1;
} //A[k] is now a pivot
}
3.7 期望性能
- 记期望的比较次数为T(n)
- T(1)=0,T(2)=1,...
- 可以证明:
- 事实上,不难验证:
4.线性排序(LinearSelect)
4.1 linearSelect( A, n, k )
-
设Q为小常数
-
if ( n = |A| < Q ) return trivialSelect( A, n, k )递归基:序列长度|A| ≤ Q -> -
else divide A evenly into n/Q subsequences (each of size Q)子序列划分 -> -
Sort each subsequence and determine n/Q medians子序列各自排序,并找到中位数 -> -
Call linearSelect() to find M, median of the medians从n/Q个中位数中,递归地找到全局中位数 -> -
Let L/E/G = { x</=/>M | x ∈ A }划分子集L/E/G,并分别计数 —— 一趟扫描足矣 -> -
if (k < |L|) return linearSelect(A, |L|, k)if (k < |L|+|E|) return Mreturn linearSelect(A+|L|+|E|, |G|, k-|L|-|E|)-> T(3n/4)
4.2 复杂度
-
-
当时,
三、希尔排序
1.框架+实例
1.1 希尔排序(Shellsort)
-
将整个序列视作一个矩阵,逐列各自排序
-
递减增量(diminishing increment)
- 由粗到细:重排矩阵,使其更窄,再次逐列排序(h-sorting/h-sorted)
- 逐步求精:如此往复,直至矩阵变成一列(1-sorting/1-sorted)
-
步长序列(step sequence):由各矩阵宽度逆向排列而成的序列
-
正确性:最后一次迭代,等同于全排序
- 1-sorted = ordered
1.2 实例
1.3 循秩访问
- 借助一维向量足以实现矩阵重排
- 在每步迭代中,若当前的矩阵宽度为,则或
1.4 实现
template <typename T> void Vector<T>::shellSort( Rank lo, Rank hi ) {
// Using PS Sequence { 1, 3, 7, 15, 31, 63, 127, ..., 1073741823, ... }
for ( Rank d = 0x3FFFFFFF; 0 < d; d >>= 1 )
for ( Rank j = lo + d; j < hi; j++ ) { //for each j in [lo+d, hi)
T x = _elem[j]; Rank i = j - d;
while ( lo <= i && _elem[i] > x )
{ _elem[i + d] = _elem[i]; i -= d; }
_elem[i + d] = x; //insert [j] into its subsequence
}
} //0 <= lo < hi <= size <= 2^30