BFPRT算法详细解析
概念
BFPRT算法即是选取中位数的中位数
的方式,找出数组n个元素中第k大的数
。我们可以根据快速排序得到该值,但是快速排序的平均复杂度为O(nlog(n)),最坏时间复杂度为O(n^2)。而堆排序也是一个较好的方法,维护一个大小为k的堆,时间复杂度为O(nlog(k))。而BFPTR算法。它的最坏时间复杂度为O(n)
。
BFPRT算法原理
之前讲过一篇文章关于快速排序文章,我们便继续从那里开始引入。快速排序的大致过程如下:
- 先从序列中选取一个数最为基准数
- 将比这个数大的数全部放到它的右边,把小于或者等于它的数全部放到它的左边
一趟快速排序也叫做Partion,即将序列划分为两部分,一部分比基数小,另一部分比基数大,然后再进行分治的过程,每一次Partion不一定都能保证划分得很均匀,所以最坏情况下得时间复杂度不能保证总是为O(nlog(n))。而在BFPTR算法中,仅仅是改变了快速排序Partion中的pivot值的选取
,在快速排序中,我们始终选择第一个元素或者最后一个元素作为Pivot,而在BFPTR算法中,每次选择五分中位数的中位数作为pivot`,这样做的目的就是使得划分比较合理,从而避免了最欢情况的发生。算法步骤如下:
- 将n个元素划为⌊n/5⌋组,每组5个,至多只有一组由n mod 5 个元素组成。
- 寻找⌈n/5⌉个组中每一个组的中位数,这个过程可以用插入排序。
- 对步骤2中的⌈n/5⌉个中位数,重复步骤1和步骤2,递归下去,直到剩下一个数字。
- 最终剩下的数字即为pivot,把大于它的数全放左边,小于等于它的数全放右边。
- 判断pivot的位置与k的大小,有选择的对左边或右边递归。
下面以一个例子具体分析,设A = {{8,33,17,51,57}},{49,35,11,25,37},{14,3,2,13,52},{12,6,29,32,54},{5,16,22,23,7}},数组A包含25个元素。首先,将数组A分成 5组,每组5个元素然后,对每组元素由小到大排序再按每组的中位数由小到大排 序得到如图2.2所示的数据方阵由各组中位数组成 一个中位数数组,如图2.2 中中间长条矩形中的元素取中位数数组的中位数,得到中位数的中位数mm=29. 最后以29为基准,将数组A划分成三部分: A1 ={ala< mm}, A2 ={ala= mm} 和A3={ala> mm} .如图所示
图中左下角矩形中的元素均小于等千中位数的中位数mm(29), 而右上角矩形中的元素均大千等千中位数的中位数mm(29).
如果对含有 n 个元素的数组调用BFPRT算法的计算时间复杂度为T(n),由于每组p个元素,对于p = 5, n可取44(44的取值为保证该式(7n/10)+1.2<=⌊3n/4⌋成立,当然最小值可以取到39,去掉向下取整符号,看它相应的余数最大是多少,相应的剪掉就好了,比方这里最大余数是3/4)。可以得到q = ⌈n/p⌉个中位数,且只可能有一组不是5个元素,找中位数的中位数时丢弃这一组,但不会影响最终结果,所以在第10步的计算时间复杂度为T(⌊n/5⌋)(因为丢弃了一组)。当n>=44时,不等式(7n/10)+1.2<=⌊3n/4⌋成立。从而,在该算法中第12-20步的计算时间复杂度为T(⌊3n/4⌋)。此外,算法第3-6步的时间复杂度为O(1);算法第7步时间复杂度为O(1);算法第8步和第9步的计算时间复杂度均为O(n)。从而可得到下列关于T(n)的递归方程,即,
算法实现如下:
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <time.h>
#include <algorithm>
using namespace std;
const int N = 10005;
int a[N];
//插入排序
void InsertSort(int a[], int l, int r)
{
for(int i = l + 1; i <= r; i++)
{
if(a[i - 1] > a[i])
{
int t = a[i];
int j = i;
while(j > l && a[j - 1] > t)
{
a[j] = a[j - 1];
j--;
}
a[j] = t;
}
}
}
//寻找中位数的中位数
int FindMid(int a[], int l, int r)
{
if(l == r) return l;
int i = 0;
int n = 0;
for(i = l; i < r - 5; i += 5)
{
InsertSort(a, i, i + 4);
n = i - l;
swap(a[l + n / 5], a[i + 2]);
}
//处理剩余元素
int num = r - i + 1;
if(num > 0)
{
InsertSort(a, i, i + num - 1);
n = i - l;
swap(a[l + n / 5], a[i + num / 2]);
}
n /= 5;
if(n == l) return l;
return FindMid(a, l, l + n);
}
//进行划分过程
int Partion(int a[], int l, int r, int p)
{
swap(a[p], a[l]);
int i = l;
int j = r;
int pivot = a[l];
while(i < j)
{
while(a[j] >= pivot && i < j)
j--;
a[i] = a[j];
while(a[i] <= pivot && i < j)
i++;
a[j] = a[i];
}
a[i] = pivot;
return i;
}
int BFPRT(int a[], int l, int r, int k)
{
int p = FindMid(a, l, r); //寻找中位数的中位数
int i = Partion(a, l, r, p);
int m = i - l + 1;
if(m == k) return a[i];
if(m > k) return BFPRT(a, l, i - 1, k);
return BFPRT(a, i + 1, r, k - m);
}
int main()
{
int n, k;
scanf("%d", &n);
for(int i = 0; i < n; i++)
scanf("%d", &a[i]);
scanf("%d", &k);
printf("The %d th number is : %d\n", k, BFPRT(a, 0, n - 1, k));
for(int i = 0; i < n; i++)
printf("%d ", a[i]);
puts("");
return 0;
}
/**
10
72 6 57 88 60 42 83 73 48 85
5
*/
另一种复杂度解释(基于该C++代码,相对好理解)
BFPRT算法的最坏时间复杂度为O(n)。设T(n)为时间复杂度,那么很容易有如下公式
T(n) <= T(n/5) + T(7n/10) + c*n
- T(n/5)来自FindMid(), n个元素,5个一组,共有 ⌈n/5⌉个中位数。
- T(7n/10)来自BFPRT(),在 ⌈n/5⌉个中位数中,主元pivot大于其中 (1/2)*(n/5)=n/10个中位数,而每个中位数在本来5个数的小组中又大于或等于其中的3个数,所以主元pivot至少大于所有数中的(n/10)*3=3n/10 个。即划分之后任意一边的长度至少为 3/10,在最坏情况下,每次选择都选到了7/10 的那一部分。
- c*n来自其它地方,例如插入排序等其它的额外操作。
证明:设T(n)=tn ,其中t可能是一个常数,也可能是关于n 的函数。带入上式
tn <= T(n/5) + T(7n/10) + cn 可推出 t<= 10c
其中 c是常数,所以 t也是常数,即T(n)<=10cn ,所以T(n) = O(n).
在BFPRT算法中,分组的选择
首先,偶数排除,因为对于奇数来说,中位数更容易计算。
如果选用3,有 T(n) <= T(n/3) + T(2n/3) + c*n,其操作元素个数还是n 。
如果选取7,9或者更大,在插入排序时耗时增加,常数 c 会很大,有些得不偿失。
参考文献:
https://zhuanlan.zhihu.com/p/31498036
https://www.cnblogs.com/HIIM/p/12926685.html
面向数据挖掘的算法设计与分析