线性时间内找到第k小的元素:快排应用与BFPRT算法

问题分析

面对这个问题,最简单的想法是对数据进行排序,然后根据下标即可找到第k小的元素,目前已知的排序算法的最低时间复杂度为 O ( n log ⁡ 2 log ⁡ 2 n ) O(n\sqrt{\log_2 {\log_2 n}}) O(nlog2log2n ),但并不为人熟知。目前应用最广的排序算法的最低时间复杂度为 O ( n log ⁡ 2 n ) O(n\log_2 n) O(nlog2n)

但是,作为完美主义者的程序员,需要思考,找到第k小的元素一定需要排序吗?但除了寻找最大或最小的元素之外,我们似乎只能选择排序。那么能否只进行部分排序,便可找到第k小的元素呢?答案是显然的,只需要采用每一趟都能确定一个固定元素的排序算法即可。

思考 O ( n log ⁡ 2 n ) O(n\log_2 n) O(nlog2n)的排序算法中有哪些算法每一趟可以确定一个固定元素,答案是快速排序和堆排序(不考虑锦标赛排序,堆排序是锦标赛排序的升级版)。这里以快速排序为例,方便引入后面介绍的BFPRT算法,不过还有一个原因是堆排序获取第k小的元素的时间复杂度是 O ( n ) O(n) O(n)的概率小于快速排序。

快排应用

我们知道,快排每一趟都会有随机有一个元素处于最终位置上,故只需在快排中设置一个新的递归出口再加以微改便能返回第k小的元素。
代码如下:
这里采用的快排为随机化快速排序,没有了解过的的朋友可以看一下我的另外一篇博客:升级版快速排序——随机化快速排序

template<typename T>
void partition(T array[],int left,int right,int& mid)
{
	srand(time(0));
	int i = left, j = right, move = rand() % (right - left + 1) + left;
	T temp = array[move];
	array[move] = array[left];
	while (i != j)
	{
		while (array[j] > temp&&j > i) j--;
		if (i < j) array[i++] = array[j];
		while (array[i] < temp&&i < j) i++;
		if (i < j) array[j--] = array[i];

	}
	array[i] = temp;
	mid = i;
}

template<typename T>
T random_quick_sort(T array[], int left, int right,int k)
{
	
	if (k > right - left + 1) exit(7);
	int i;
	partition(array, left, right, i);
	if (k == i - left + 1) return array[i];
	else if (k > i - left + 1) return random_quick_sort(array, i + 1, right, k - i + left - 1);
	else return random_quick_sort(array, left, i - 1, k);
}

这个算法的时间复杂度的确定需要用到指示器随机变量。 E [ T ( n ) ] = E [ ∑ k = 0 n − 1 X k ( T ( m a x E[T(n)]=E[\sum_{k=0}^{n-1}X_k(T(max E[T(n)]=E[k=0n1Xk(T(max{ k , n − k − 1 k,n-k-1 k,nk1} ) ) + Θ ( n ) ] ))+\Theta(n)] ))+Θ(n)],最终得到 E [ T ( n ) ] = Θ ( n ) E[T(n)]=\Theta(n) E[T(n)]=Θ(n),即该算法的时间复杂度的期望值为 O ( n ) O(n) O(n)
但是存在最坏情况,即每一次划分只能划分出一个部分,即 T ( n ) = T ( n − 1 ) + Θ ( n ) T(n)=T(n-1)+\Theta(n) T(n)=T(n1)+Θ(n),由等差级数可以得到最坏情况下的时间复杂度为 O ( n 2 ) O(n^2) O(n2)

BFPRT算法

BFPRT算法由Blum、Floyd、Pratt、Rivest、Tarjan提出,故以其名首字母拼接命名。
在应用快排寻找第k小的元素时,最坏情况为 O ( n ² ) O(n²) O(n²)的原因是没有明显划分,为了“消除这个最坏情况”,只需要找到一个合适的值,使得划分具有意义,并且这个值递归迭代后仍保留这个性质。不妨称这个数为主元,BFPRT算法就找到了这样的一个主元。
下面开始欣赏这个算法的巧妙之处吧!

  • 首先,把数据按5个数为一组进行分组,最后不足5个的为一组
    在这里插入图片描述
  • 对每组数进行排序求得其中位数,图中标红部分为每组的中位数,用箭头表示两个数之间的大小关系,箭头的数比箭尾的数大,按照这样操作,便形成了如图所示的有向图模型。在这里插入图片描述
  • 对前面所求得的中位数进行排序(在图中对应列跟着一起动,但实际算法中不需要这样,此处这样操作是便于理解),求得中位数的中位数,各个中位数的大小关系也在图中体现了出来。在这里插入图片描述
  • 看到这里你可能会觉得这有什么卵用,别急,我一开始学的时候也是这么想的,来吧,展示!
    观察下面的图片,可以发现有一个矩形区域(紫色框)的所有数字都比中位数的中位数要大,有一个矩形区域(红色框)的所有数字都比中位数的中位数小。看到这里,聪明的读者应该知道这意味着什么了吧。
    在这里插入图片描述
    这意味着这个中位数的中位数便是我们寻找的一个主元,可以得到一个优质的划分。

接下来开始定量分析:
在该算法中,所有数据摆成了5行 ⌈ n 5 ⌉ \lceil \frac{n}{5} \rceil 5n,根据主元进行划分,可以得到至少有 ⌊ ⌊ n 5 ⌋ 2 ⌋ ∗ 3 \lfloor \frac{\lfloor \frac{n}{5} \rfloor}{2}\rfloor *3 25n3个数是小于主元的,至少有 ( 1 − ⌊ ⌊ n 5 ⌋ 2 ⌋ ) ∗ 3 (1-\lfloor \frac{\lfloor \frac{n}{5} \rfloor}{2}\rfloor )*3 (125n)3的数是大于主元的。

在一开始的确定中值的时间开销为 Θ ( n ) \Theta(n) Θ(n)(因为每组数只有5个),接着递归求解中值的中值,时间开销为 T ( n 5 ) T(\frac{n}{5}) T(5n),紧接着便是递归划分,由数学知识,当 n n n较大时 ⌊ ⌊ n 5 ⌋ 2 ⌋ ∗ 3 > n 4 \lfloor \frac{\lfloor \frac{n}{5} \rfloor}{2}\rfloor *3>\frac{n}{4} 25n34n,假设其余 3 n 4 \frac{3n}{4} 43n的元素都比主元大,则每一次递归的最大子问题规模为 T ( 3 n 4 ) T(\frac{3n}{4}) T(43n),故有 T ( n ) ≤ T ( n 5 ) + T ( 3 n 4 ) + Θ ( n ) T(n)≤T(\frac{n}{5})+T(\frac{3n}{4})+\Theta(n) T(n)T(5n)+T(43n)+Θ(n),由代换法解得 T ( n ) < = c n T(n)<=cn T(n)<=cn

下面给出BFPRT算法的具体代码实现:

扫描二维码关注公众号,回复: 11903170 查看本文章
  • 1° 首先把数组按5个数为一组进行分组,最后不足5个的为一组。对每组数进行排序求取其中位数,并将所有中位数移到当前数组的前面
  • 2° 对这些中位数重复(递归)1操作,最后返回最终的中位数
  • 3° 将上一步得到的中位数作为划分的主元进行整个数组的划分。(基于快速排序)
  • 4° 判断第k个数在划分结果的左边、右边还是恰好是划分结果本身,前两者递归处理,后者直接返回答案。
template<typename T>
void partition(T array[], int left, int right, int& mid)
{
	int i = left, j = right;
	T temp = array[left];
	while (i != j)
	{
		while (array[j] > temp&&j > i) j--;
		if (i < j) array[i++] = array[j];
		while (array[i] < temp&&i < j) i++;
		if (i < j) array[j--] = array[i];

	}
	array[i] = temp;
	mid = i;
}

template<typename T>
void quick_sort(T array[], int left, int right)
{

	if (left >= right) return;
	int i;
	partition(array, left, right, i);
	quick_sort(array, i + 1, right);
	quick_sort(array, left, i - 1);
}


template<typename T>
void grouping(T arr[], int left, int right)
{
	int  count = left;
	for (int i=left; i <= right; i += 5)
	{
		if (i + 5 <= right)
		{
			quick_sort(arr, i, i + 4);
			swap(arr[i + 2], arr[count++]);
		}
		else 
		{
			quick_sort(arr, i, right);
			swap(arr[(i + right) / 2], arr[count++]);
		}
		
	}
}

template<typename T>
void findmid(T arr[],int left,int right)
{
	grouping(arr, left, right);
	if (right - left <= 4) return;
	else findmid(arr, left, left + (right - left + 1) / 5);
}

template<typename T>
T BFPRT(T arr[],int left,int right,int k)
{
	if (k > right - left + 1) exit(7);
	grouping(arr, left, right);
	findmid(arr, left, left + (right - left + 1) / 5 );
	int mid;
	partition(arr, left, right, mid);
	if (k == mid - left + 1) return arr[mid];
	else if (k > mid - left + 1) return BFPRT(arr, mid + 1, right, k - mid + left - 1);
	else return BFPRT(arr, left, mid - 1, k);
}

在任何情况下,BFPRT算法在任意情况下都可以在线性时间内求出第k小的元素,它的思想主要是以每5个元素为一组,找出中位数的中位数的中位数的…中位数。如果3个元素为一组,将达不到这个效果。5是能达到效果的最小数字,7也可以成立,但性能提升不明显。

猜你喜欢

转载自blog.csdn.net/RealCoder/article/details/107338319
今日推荐