清华邓俊辉数据结构学习笔记(6) - 排序

第十二章

(a1)快速排序:算法A

分而治之:将序列分为两个子序列S = SL + SR (O(n)),规模缩小(max{ |SL| + |SR| } < n),彼此独立(max( SL ) ≤ min( SR )),在子序列分别递归地排序之后,原序列自然有序(sorted( S ) = sorted( SL ) + sorted( SR )),只剩单个元素时,本身就是解(平凡解)。mergesort的计算量和难点在于合,而quicksort在于分。
轴点:左/右侧的元素,均不比它更大/小。以轴点为界,原序列的划分自然实现 [lo, hi) = [lo, mi) + [mi] + (mi, hi)。

template <typename T> void Vector<T>::quickSort(Rank lo, Rank hi){
     
	if (hi - lo < 2) return; //单元素区间自然有序,否则
	Rank mi = partition(lo,hi-1); //先构造轴点,再
	quickSort(lo,mi);//前缀排序
	quickSort(mi+1,hi);//后缀排序
}

坏消息:在原始序列中,轴点未必存在
必要条件:轴点必已就位,反之不然。
derangement:2 3 4 … n 1
特别地:在有序序列中,所有元素皆为轴点,反之亦然。
快速排序:将所有元素逐个转换为轴点的过程。
好消息:通过适当交换,可使任一元素转换为轴点。
问题:如何交换?成本多高?
不变性+单调性:L ≤ pivot ≤ G;U=[lo, hi]中,[lo]和[hi]交替空闲。

while((lo<hi)&&(pivot<=_elem[hi])) hi--;
_elem[lo]=_elem[hi];
while((lo<hi)&&(_elem[lo]<=pivot)) lo++;
_elem[hi]=_elem[lo];

性能分析:(1)不稳定:lo/hi的移动方向相反,左/右侧的大/小重复元素可能前/后颠倒;(2)就地:只需**O(1)**附加空间;(时间:2T(n/2) + O(n) = T(n) = O(nlogn),到达下界);(3)最好情况:每次划分都接近平均,轴点总是接近中央;(4)最坏情况:每次划分都极不均衡,比如轴点总是最小/大元素,T( n ) = T( n - 1) + T( 0 ) + O( n ) = O( n² ),与气泡排序相当。即便采用随机选取、(Unix)三者取中之类的策略,也只能降低最坏情况的概率,而无法杜绝。
平均性能:O(nlogn)

(a4)快速排序:变种

不变性:将S分为四部分 S = [ lo ] + L( lo, mi ] + G( mi, k ) + U[ k, hi ],L < pivot ≤ G。
单调性:[k]不小于轴点?直接G拓展:G滚动后移,L拓展。

pivot <= S[k]? k++ : swap(S[++m],S[k++])

实现

template <typename T> Rank Vector<T>::partition(Rank lo,Rank hi){
     
	swap( _elem[lo], _elem[ lo + rand() % ( hi - lo + 1 )])
	T pivot = _elem[lo]; int mi = lo;
	for( int 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; //返回轴点的秩
}

(b1)选取:众数

k-selection:在任一一组可比较大小的元素中,如何由大到小,找到次序为k者?亦即在这组元素的非降序序列S中找出S[k]。
median中位数:长度为n的有序序列S中,元素S[ |n/2| ]称作中位数,数值上可能重复。
majority众数:无序向量中,若有一半以上元素同为m,则称之为众数。如在{3,5,2,3,3}中,众数为3,然而在{3,5,2,3,3,0}中,却无众数。
平凡算法:排序 + 扫描。进一步:若限制时间不超过O(n),附加空间不超过O(1)?众数若存在,亦必为中位数,只要找出中位数即不难验证它是否众数。

template <typename T> bool majority(Vector<T> A, T & maj)
	{
     return majEleCheck( A, maj = median( A ));}

遍历+计数+取极值
然而在高效的中位数算法未知之前,如何确定众数的候选呢?引入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 = majEleCandidate(A));}

减而治之:若在向量A的前缀P(|P|为偶数)中,元素出现的次数恰占半数,则A有众数仅当对应的后缀A - P有众数m,且m就是A的众数。
最终总要花费O(n)的时间做验证,故只需考虑A的确含有众数m的两种情况:(1)若 x = m,则在排除前缀P之后,m与其他元素在数量上的差距保持不变;(2)若 x ≠ m,则在排除前缀P之后,m与其他元素在数量上的差距不致缩小。

template <typename T> T majEleCandidate( Vector<T> A ){
     
	T maj; //众数候选者
//线性扫描:借助计数器c,记录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; //至此,原向量的众数若存在,则只能是maj,反之不然
		
}

k选取
尝试:堆(A) getMax() - > Heapifiation(O(n)) + delMin() (O( k * logn )

尝试:堆(B) 任选(比如前)k个元素,组织为最大顶堆O( k )),对于剩余的 n - k个元素,各调用一次 insert() 和 delMax()(O( 2*(n - k)*logk))

尝试:堆(C) H:任取k个元素,组织为大顶堆O(k));G:其余 n - k个元素,组织为小顶堆O(k));反复地:比较堆顶元素h和g(O(1)),如有必要,交换之(O( 2 × ( logk + log(n-k)))),直到 h ≤ g(O( min( k, n - k ))

针对k-selection设想最好的情况,如果足够幸运,候选轴点可能恰好就是查找目标。

quickSelect()

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 ){
     
			while( i < j && pivot <= A[j]) j--; A[i] = A[j];
			while( i < j && A[i] <= pivot) i++; A[j] = A[i];
		}//assert: i==j
		A[i] = pivot;
		if ( k <= i ) hi = i - 1;
		if ( i <= k ) lo = i + 1; 
	}//A[k]是轴点
}

linearSelect()
令| Q | 为一个小常数
(0) if ( n = | A | < Q ) return trivialSelect( A, k ) - > O(QlogQ) - > O(1) //递归基,序列长度|A|≤Q
(1) else divide A evenly into n/Q subsesquences (each of size Q) - > O(n) //子序列划分
(2) Sort each subsequence and determine n/Q medians - > O(n) = O(1) × n/Q //子序列各自排序,并找到中位数
(3) Call linearSelect to find M, median of the medians - > T( n/Q ) //从n/Q个中位数中,递归地找到全局中位数
(4) Let L / E / G = { x < / = / > M | x ∈ A } - > O(n) //划分子集L/E/G,并分别计数,一趟扫描
(5) if ( k ≤ |L|) return linearSelect( L, K )
if ( k ≤ |L| + |E| ) return M
return linearSelect( G, k - |L| - |E|) - > T( 3n/4 )
复杂度:某种意义上,如上所确定的M必然不偏不倚,各有 n/4 个元素不小于/不大于M;n/Q个中位数中,至少半数不小于M,而它们又各自不大于至少Q/2个元素 n/Q / 2 * Q/2 = n/4。
T(n) = O(n) + T(n/Q) + T(3n/4),为使之解作线性函数,只需 n/Q + 3n/4 < n,或等价地 1/Q + 3/4 < 1。比如取Q = 5,则存在常数c,使T(n) = cn + T(n/5) + T(3n/4),T(n) = O(20cn) = O(n)。

(c1)希尔排序:Shell序列

定义:将整个序列视作一个矩阵,逐列各自排序w-sorting。
递减增量:(1)由粗到细:重排矩阵,使其更窄,再次逐列排序w-ordered;(2)逐步求精:如此往复,直至矩阵变成一列1-sorting。
步长序列:由各矩阵宽度构成的逆序列,H = { w1 = 1, w2, w3, …, wk, … }。
正确性:最后一次迭代等同于全排序1-ordered = sorted。
寻秩访问:如何实现矩阵重排?是否需要借助二维向量?实际上借助一维向量足矣,在每步迭代中,若当前矩阵宽度为w,则第i列中的记录依次就是 a[ i+kw ], 0 <= k < n/w。
插入排序:各列内部的排序如何实现?必须采用输入敏感的算法,以保证有序性可持续改善,且总体成本足够低廉,如insertionsort,其实际运行时间更多地取决于输入序列所含逆序对的总数。Shellsort总体效率取决于具体使用何种步长序列H,主要考察和评测:(1)比较操作、移动操作的次数;(2)收敛的速度,或反过来迭代的轮数。
希尔序列:Hshell = { 1, 2, 4, 8, …, 2^k, … },采用Hshell在最坏情况下需要运行 Ω(n²)时间,例如考察子序列 A = unsort[ 0, 2^(N-1)) 和 B = unsort[ 2^(N-1), 2 ^ N)交错而成的,在2-sorting刚结束时A和B必然各自有序,但其中的逆序对仍然很多,1-sorting仍需 Ω(n²)时间。

(c2)希尔序列:逆序对

eg : 邮资问题,一封信50F,一张民信片35F,但是只有4F和13F的邮票。将50和35视作线性组合 4m + 13n, where m, n ∈ N = { 0, 1, 2, … }。
让g, h∈N,对于任何m,n∈N,f = mg + nh为g和h的线性组合。将g,h∈N视作相对初等的,N(g,h)={不是g和h组合的数目},令x( g, h ) = max( N(g, h) )。理论上 x( g, h ) = ( g - 1 )*( h - 1) - 1 = gh - g - h,例如x(3, 7) = 11,x(4, 13) = 35。
h-ordered:令h∈N,对于0<=iS[i] ≤ S[i+h]。1-ordered序列是有序的。
h-sorting:(1)将S排列成h行的二维矩阵;(2)对每行分别排序。
K理论:一个g-ordered序列在h-sorted后依然是g-ordered。
线性组合:一个序列若既为g-ordered又为h-ordered,则为(g, h) - ordered。并对于任意m, n∈N,这个序列既为(g+h)-ordered,又为(mg+nh)-ordered。
反向:若S[0,n)为(g,h)-ordered序列,从[i]起的(g-1)(h-1)个元素可能比S[i]小。

你可能感兴趣的:(清华邓俊辉数据结构学习笔记(6) - 排序)