分治思想-选pivot问题

前言

选择的pivot会影响比较次数。为了进一步理解如何选pivot,我们从找出数组中第k小的数问题入手来理解。

例子:寻找第k小的元素

问题叙述

给定一个乱序数组A,试求该数组中第k小的元素?

最直接的解决办法就是数组排序返回index为k-1的元素,排序选用快排或归并(若不考虑内存使用),时间复杂度是O(nlogn)。

思考一下,实际上这个问题没有必要将数组完全排序再找第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]:
			put A[i] in S_r // 大放pivot右
		else:
			put A[i] in S_l // 小放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:
		return select(S_r,k-|S_l|-1)  // 第k小元素落到了S_r集合中,k=k-|S_l|-1

与快排不同,此问题每次只做pivot一边的递归。上述算法的好坏取决于pivot的选择,由快排可知每次pivot的选择越靠近中心越好。

下面比较3种不同的pivot选择方法

随机选择

像普通快排中选择pivot一样随机选择一个元素作为pivot很暴力直接。每一次的选择导致的运行时间都不一致,但总体来说,可以证明是O(n)。

假设pivot的选择均匀划分了数组,例如1/2平分,因为每一次只需对一边递归,所以时间复杂度推导如下:

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]视为较好情况),因此我们考虑那些子实例大小在(3/4)^(j+1)n+1,j=0,1,2…此时称算法处于第j期。也就是直到选在了[1/4,3/4]内才算1期,概率论上讲50%的概率,平均2次就可以。

伪代码如下

QuickSelect(A, k)
Choose an element Ai from A uniformly at random;
S+ = {};
S− = {};
for all element Aj in A do
    if Aj>Ai then
        S+ = S+ ∪ {Aj};
    else
        S− = S− ∪ {Aj};
    
    if |S−| = k − 1 then
        return Ai ;
    else if |S−|>k−1 then
        return QuickSelect(S− , k);
    else
        return QuickSelect(S+ , k−|S−|−1);

随机采样,选中位数

选中好区间的可能性越大越好,因此我们通过采样然后寻找更好中位数。

pivot选取算法步骤

  1. 首先从数组A中随机地取r个元素,设为S
  2. 对S排序,令u为S中(1−δ)r/2小的元素,v为S中(1+δ)r/2小的元素

可以将u,v视为分位点,δ是一个调整参数

  1. 将数组A划分成三部分,如下

L = A i : A i < u ; M = A i : u ≤ A i ≤ v ; H = A i : A i > v ; L = {A_i:A_i < u};\\ M = {A_i:u \le A_i \le v};\\ H = {A_i:A_i > v}; L=Ai:Ai<u;M=Ai:uAiv;H=Ai:Ai>v;

  1. 检查M是否满足以下条件:

∣ L ∣ ≤ n 2 且 ∣ H ∣ ≤ n 2 , 这 保 证 了 中 位 数 在 M 中 ∣ M ∣ ≤ c δ n , M 不 能 太 大 |L|\le \frac{n}{2} 且 |H|\le \frac{n}{2},这保证了中位数在M中\\ |M|\le c\delta n,M不能太大 L2nH2nMMcδnM

如果不满足上述条件,则回到第一步

举例图示如下:
在这里插入图片描述

选到的pivot就是中位数,在划分的时候就能使得数据量呈指数级下降。接下来就是之前一样的select函数。

选择分组中位数的中位数

我们可以使用分组取中位数策略来尽量找到靠近中位数的数作为pivot,因为这样可以让划分更加均匀,时间复杂度更低。细节如下:

  1. 假设将数组所有元素按5个一组划分,一共n/5组
  2. 找出每一组的中位数,一共耗费6n/5时间

5个元素只需要6次比较就能找到中位数,过程可以参考这篇文章

3、递归寻找这些中位数的中位数(记为M),耗费T(n/5)

4、基于M将A划分成S_l 和 S_rS ,耗费O(n)时间

5、采用一开始的递归查找步骤

if |S_l| = k-1:
	return M  // M刚好是第k小元素
else if |S_l| > k-1:
	return select(S_l,k)  // 时间复杂度最多T(7n/10)
else:
	return select(S_r,k-|S_l|-1)  // 时间复杂度最多T(7n/10)

在这里插入图片描述

可以发现,依据这种方法找到M很接近实际中位数。且知道蓝色的区域一定小于M,红色的区域一定大于M,这两个区域的数量各占3/10n,那么我们如果用M作为pivot划分,至少可以分出3/10n的数据,所以下一步递归最多还需要处理7n/10的数据。

时间复杂度如下:

T ( n ) ≤ T ( n 5 ) + T ( 7 10 n ) + c n , c = 6 5 T(n)\le T(\frac{n}{5}) + T(\frac{7}{10}n) + cn,c=\frac{6}{5} T(n)T(5n)+T(107n)+cn,c=56

T ( n ) ≤ T ( n 5 ) + T ( 7 10 n ) + c n ≤ T ( n 25 ) + T ( 7 50 n ) + T ( 7 50 n ) + T ( 49 100 n ) + c 9 10 n + c n ≤ . . . ≤ c n + c 9 10 n + c ( 9 10 ) 2 n + . . . = c n ( 1 − ( 9 10 ) i 1 − 9 10 ) = O ( n ) T(n) \le T(\frac{n}{5})+T(\frac{7}{10}n)+cn \\ \le T(\frac{n}{25})+T(\frac{7}{50}n)+T(\frac{7}{50}n)+T(\frac{49}{100}n)+c\frac{9}{10}n+cn\\ \le...\\ \le cn+c\frac{9}{10}n+c(\frac{9}{10})^2n+...\\ = cn(\frac{1-(\frac{9}{10})^i}{1-\frac{9}{10}})\\ =O(n) T(n)T(5n)+T(107n)+cnT(25n)+T(507n)+T(507n)+T(10049n)+c109n+cn...cn+c109n+c(109)2n+...=cn(11091(109)i)=O(n)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 为什么分成5个一组?

假如3个一组,则时间复杂度:

T ( n ) ≤ T ( n 3 ) + T ( 2 3 n ) + O ( n ) = O ( n l o g n ) T(n) \le T(\frac{n}{3})+T(\frac{2}{3}n)+O(n) = O(nlogn) T(n)T(3n)+T(32n)+O(n)=O(nlogn)

假如我们要找的数很接近中间,例如在55个数中找第28小,下一次递归我们就是找(0-31)中第28小,但这个时候按照中位数的原则,我们选中的pivot往往是15左右的数,发现与28相距甚远。这就使得我们需要递归几次来逼近28。也就是说此轮的要找的数如果很接近中间,则下轮的数会比较偏靠边。基于此,有人提出了优化算法如下:

  • α−β算法

首先回顾一下上述算法的简要步骤:

  1. 将A分成若干块,组成集合G,G中每个块长度要求大于3(因为前面证明了3会导致时间复杂度提升)
  2. 取G中每个块的中位数,构成集合M
  3. 选择M的中位数作为pivot

引理:若α=k/n=1/2,设下一次迭代需要找第 k′ 小的数,α′=k′/n,则必然有α′≤1/4或α′≥3/4 ,简言之,就是如果要找的数k靠近中位数,那么下一次迭代中要找的数k′必然靠近数组的边缘。

将改进算法的算法简要步骤整理如下:

  1. 将A分成若干块,组成集合G,G中每个块长度要求大于3(因为前面证明了3会导致时间复杂度提升)
  2. 取G中每个块的中位数,构成集合M
  3. 依据要找的第k小数在n中的位置α(α=k/n),去求我们要返回的pivot在M中的位置β(β由α决定)

α ≤ 1 4 + λ → β = 3 2 α , 目 标 在 左 半 部 分 α ≥ 3 4 − λ → β = 3 2 α − 1 2 , 目 标 在 右 半 部 分 1 4 + λ < α < 3 4 − λ → β = α , 目 标 在 中 间 \alpha \le \frac{1}{4} + \lambda \to \beta = \frac{3}{2}\alpha,目标在左半部分\\ \alpha \ge \frac{3}{4} - \lambda \to \beta = \frac{3}{2}\alpha - \frac{1}{2},目标在右半部分\\ \frac{1}{4} + \lambda \lt \alpha \lt \frac{3}{4} - \lambda \to \beta = \alpha,目标在中间\\ α41+λβ=23α,α43λβ=23α2141+λ<α<43λβ=α

总结

  • 将算法中的常数改为可调参数,然后寻找调整参数的策略,来优化算法
  • 手动模拟算法的运行过程,发现规律

猜你喜欢

转载自blog.csdn.net/qq_32505207/article/details/106244899