分治算法 求第k小元素 O(n) & O(nlog2^n)

 

BFPRT算法:时间复杂度O(n)求第k小的数字(分治算法+快排)

 

各位小伙伴,由于本篇文章代码太过杂乱。我于 2018年12月25日 对文中介绍的算法进行了重写。点击上面的蓝色字体,可以阅读重写后的文章,修复了一些存在的错误。

 


 

最容易想到的算法是采用一种排序算法先将数组按不降的次序排好,然后从排好序的数组中捡出第k个元素。这样的算法在最坏情况下时间复杂度是O(nlog2^n)。

 

实际上,我们可以设计出在最坏情况下的时间复杂度为O(n)的算法。

 

利用分治算法并结合快排思想,很容易达到O(n)的时间复杂度。其核心思想在于快排中基准的选取。(根据严蔚敏版教材,一般直接选取第一个元素作为快排基准。但求第k小元素,则依赖于一种中值选取法,以加速剪枝)。

 

阅读以下内容时,需要先学习快排算法,可以看看这篇文章《[Data Structure]九大内部排序算法》。

 

下面举个例子,如何达到O(n)选取第k小的元素。

 

问题:如何在O(n)内,确定A[17]中第k小的元素? A[17] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };

这里给出一个分治算法,选取基准的求解过程。

step1 设置一个值r,将A[15]分为长度为r的几个组。(假设r = 5)

A[17] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };

                     G1               G2                   G3                  G4

分组结果,G1[5] = {0, 1, 2, 3, 4} ;G2[5] = {5, 6, 7, 8, 9};G3[5] = {10, 11, 12, 13, 14};G4[2] = {15, 16}。这里注意第四组,只有2个元素。

 

step2 分别求取G1~4组别中的中值,G1 = 2, G2 = 7, G3 = 12, G4 = 15。

并由这四个数再组成一个数组G[4] = {2, 7, 12, 15}。

 

step3 求得G[4] = {2, 7, 12, 15}数组中的中值,mid = 7。

那么,mid = 7便是最终选取出来的快排划分基准。

 

以下程序中,partition函数(第13行)以上部分即为上述基准选取过程。这里需要说明的是,求得中值后,程序中并没有开辟额外空间,而是在原有A[]基础上进行操作的,将中值放在最前面,故需要swap以保证数据信息不丢失。

 

//A[low..high]
int select_rank_k(int A[], int low, int high, int k)
{
	int r_group = ceil((high - low + 1)*1.0 / r);//ceil取上限,总共分为r_group个组
	//计算每个分组中值,存于A[]最前面
	for (int i = 1; i <= r_group; ++i) {
		sort(&A[low + (i - 1)*r], &A[(low + i*r - 1) > high ? high : (low + i*r - 1)]);
		swap(A[low + i - 1], A[low + (i-1)*r + r / 2]);
	}
	//获得每个组的中值的中值(并置于A[low]位置,方便调用快排划分函数)
	sort(&A[low], &A[low + r_group]);
	swap(A[low], A[r_group / 2]);
	int cur = partition(A, low, high);
	if (cur == k-1){
		return A[cur];
	}
	else if (cur < k){
		return select_rank_k(A, cur + 1, high, k);
	}
	else{
		return select_rank_k(A, low, cur - 1, k);
	}
}

2018年6月4日修正

swap(A[low], A[r_group / 2]); 应该更改为swap(A[low], A[low+r_group / 2]);

 

程序中其它执行步的时间复杂度都至多是n的倍数。如果用T(n)表示算法在数组长度为n的时间复杂度,则当n≥24时,有递归关系

其中c是常数。从上述递推关系式出发,用数学归纳法可以证明,

所以,在最坏情况下,select_rank_k算法的时间复杂度是O(n)。

 

最后,对整个问题抽象以下,并给出完整DEMO。问题:已知n元数组A[1..n],试确定其中第k小的元素。

补充:36行需修正为swap(A[low], A[low+r_group / 2]);

#include <stdio.h>
#include <algorithm>
#include <math.h>
using namespace std;

//划分——每次划分唯一确定一个元素位置
int partition(int A[], int low, int high)
{
	int pivot = A[low];    //一般采用严蔚敏教材版本,以第1个位置为基准
	while (low < high){
		while (low < high && A[high] >= pivot){
			--high;
		}
		A[low] = A[high];  //将比基准小的元素移动到左端
		while (low < high && A[low] <= pivot){
			++low;
		}
		A[high] = A[low];  //将比基准小的元素移动到右端
	}
	A[low] = pivot;
	return low;
}

int r = 5;
//A[low..high]
int select_rank_k(int A[], int low, int high, int k)
{
	int r_group = ceil((high - low + 1)*1.0 / r);//ceil取上限,总共分为r_group个组
	//计算每个分组中值,存于A[]最前面
	for (int i = 1; i <= r_group; ++i) {
		sort(&A[low + (i - 1)*r], &A[(low + i*r - 1) > high ? high : (low + i*r - 1)]);
		swap(A[low + i - 1], A[low + (i-1)*r + r / 2]);
	}
	//获得每个组的中值的中值(并置于A[low]位置,方便调用快排划分函数)
	sort(&A[low], &A[low + r_group]);
	swap(A[low], A[r_group / 2]);
	int cur = partition(A, low, high);
	if (cur == k-1){
		return A[cur];
	}
	else if (cur < k){
		return select_rank_k(A, cur + 1, high, k);
	}
	else{
		return select_rank_k(A, low, cur - 1, k);
	}
}

int main(void)
{
	int A[15] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 };
	printf("%d\n", select_rank_k(A, 0, 3, 2));
	return 0;
}

 

                                                                              @qingdujun

                                                                      2017-11-22 北京 怀柔

 

Reference:陈玉福.计算机算法设计与分析,59-61

 

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页