算法设计与分析:分治思想(2)- 选择问题(对单个数组归约)

本文介绍了如何使用分治思想解决寻找数组中第k小元素的问题,避免了完全排序的高时间复杂度。通过分析问题,提出了三种选择pivot的方法:随机选择元素、随机样本中位数和分组中位数的中位数。讨论了每种方法的优缺点和时间复杂度,特别强调了pivot选择对算法效率的影响。最后,提出了改进的α-β算法,以更好地逼近目标元素。

本文参考UCAS卜东波老师算法设计与分析课程撰写

前言

本文内容承接上次分治思想-入门的内容,分治思想的应用十分广泛,除了经典的排序中的使用以外,还有许多类型的题目可以利用分治思想简单解决。如快速从数组中找出中位数,或更一般地。找出第k小的数我们用一个简单的问题来引入。通过这个问题,我们能够更进一步理解如何对pivot进行选择分析。

寻找第k小的元素

问题描述与分析

  • 给定一个乱序数组A,试求该数组中第k小的元素?
    最直白的解决办法,就是对数组进行排序返回下标为k-1的元素,排序的时候选用快排或归并(若不考虑内存使用),虽然暴力,但是排序的时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)

    这种办法在数据量较小的情况下仍然可以接受。但我们仔细思考一下将数组完全排序再找第k个元素是不是有点杀鸡用牛刀,大炮轰蚊子的感觉,因为你做了很多额外的事情,浪费了许多本可以节省的时间,在acm比赛中往往后面的例子就会卡这些时间导致无法ac,在大数据应用中你反映给用户的结果就是多了几秒的响应时间,削弱了用户体验,很可能导致暴躁老哥直接不用你的产品了。因此我们考虑仍然用分治的思想,但不采用排序的方式来解决

解决思路

要找第k小的元素,我们参考快排的策略,将所有小于pivot的放到左边,大于pivot的放到右边。试想,如果有一个数,它左边的数都小于它,它右边的数都大于它,且左边的数的个数刚好是k-1,那么这个数就是第k小的元素!可以发现,这种情况下,只是说了左边的数的个数是k-1,没有要求左边的数一定是有序的,这就减少了查找的运行时间。那么我们参考快排的伪代码(在深入理解快速排序一文中),可以得到伪代码如下:

select(A,k):
	S_l = [],S_r = []  // s_l存储小于pivot的值,s_r存储大于pivot的值
	choose pivot A[p]  // 随机抽取
	for i = 0 to |A|-1:
		if A[i] > A[p]:
			S_r += A[i]  // 若比pivot大,则将A[i]放到pivot右边
		else:
			S_l += A[i] // 否则将A[i]放到pivot左边
	// 上面的步骤与快排类似
	if |S_l| = k-1:
		return A[p]  // A[p]刚好是第k小元素
	else if |S_l| > k-1:
		return select(S_l,k)  // 第k小元素落到了S_l集合中,递归查找
	else:
		// 第k小元素落到了S_r集合中,注意这里k改变了,因为右边的起始元素是从privot+1开始,原来第k小的元素在这里是第k-|S_l|-1小
		return select(S_r,k-|S_l|-1)  

从伪代码中也可以看出,每次做递归调用的时候只会对一边进行递归调用,而在快排中两边都要递归调用,从这就可以明显感觉到时间差异。当然,上述算法的好坏取决于pivot的选择,依据我们在深入理解快速排序对pivot的讨论,可以知道每次pivot的选择越靠近中心越好。下面考察三种不同的pivot选择方法做对比。

随机选择一个元素作为pivot

随机选择一个元素作为pivot很直观,很简便,就像普通快排中选择pivot一样。每一次的选择导致的运行时间都不一致,但总体来说,可以证明它是线性的(即期望运行时间O(n))。老师课程中给出了证明方法,但比较难以理解,这里我详细再说一次:

  • 严格证明
    首先考虑如果pivot的选择使得数组得到了较为均匀的划分(例如刚好平分),那么由于每一次只需对一边递归,可以得到时间复杂度推导如下
    T ( n ) = T ( n 2 ) + c n = T ( n 4 ) + c n 2 + c n = . . . = c ( n + n 2 + n 4 + . . . + 1 ) = c ( 2 n − 1 ) = O ( n ) T(n) = T(\frac{n}{2}) + cn\\ = T(\frac{n}{4}) + c\frac{n}{2}+ cn\\ = ... \\ =c(n+\frac{n}{2} + \frac{n}{4} + ...+ 1) \\ =c(2n-1) = O(n) T(n)=T(2n)+cn=T(4n)+c2n+cn=...=c(n+2n+4n+...+1)=c(2n1)=O(n)
    同样地,在较好情况下都能得到该时间复杂度(一般将 [ 1 4 , 3 4 ] [\frac{1}{4},\frac{3}{4}] [41,43]视为较好情况),因此我们考虑那些子实例大小在 ( 3 4 ) j + 1 n + 1 , ( 3 4 ) j n , j = 0 , 1 , 2... (\frac{3}{4})^{j+1}n+1,(\frac{3}{4})^{j}n,j=0,1,2... (43)j+1n+1,(43)jn,j=0,1,2...,此时称算法处于第
    j期。

    注意一点,并不是每一次划分都会经历一个时期,有的时候当pivot取得不当的时候,可能要好几次划分才跨越一个时期。为了方便理解我绘制了下面的图作为参考。
    在这里插入图片描述
    其中,第0期跨越了两次划分,是因为第二次的划分中只划分出1个元素,即这次的pivot选的很差,我们将这两次划分看作一个时期。

    设X为算法运行过程中的比较次数, X j X_j Xj为处于第j期时的比较次数,有
    X = X 0 + X 1 + X 2 + . . . X=X_0+X_1+X_2+... X=X0+X1+X2+...
    从时期的概念来看,当一个时期跨度越小时,算法的时间耗费越少(说明此时的pivot选的好),什么时候跨度小?(pivot选中了好区间的元素,此时一次划分就进行下一期),什么时候跨度大?(pivot选中了边边的元素,最坏情况每次都选到了最边上的,相当于每一次划分就减少一个元素)。注意这里选中好区间的概率是 1 2 \frac{1}{2} 21,因此第j期进行划分的期望是2,算法的期望比较次数不超过 2 ( 3 4 ) j n 2(\frac{3}{4})^jn 2(43)jn(子实例最多有 ( 3 4 ) j n (\frac{3}{4})^jn (43)jn个元素)
    因此有 E ( X ) = E [ X 0 + X 1 + . . . ] ≤ ∑ j 2 c n ( 3 4 ) j ≤ 8 c n , 利 用 等 比 求 和 公 式 E(X) = E[X_0+X_1+...]\le\sum_j2cn(\frac{3}{4})^j\le8cn,利用等比求和公式 E(X)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值