查找第K小的数 BFPRT算法
出处 http://blog.csdn.net/adong76/article/details/10071297
BFPRT算法是解决从n个数中选择第k大或第k小的数这个经典问题的著名算法,但很多人并不了解其细节。本文将首先介绍求解这个第k小数字问题的几个思路,然后重点介绍在最坏情况下复杂度仍然为O(n)的BFPRT算法。
一 基本思路
关于选择第k小的数有许多方法
- 将n个数排序(比如快速排序或归并排序),选取排序后的第k个数,时间复杂度为O(nlogn)。
- 维护一个k个元素的最大堆,存储当前遇到的最小的k个数,时间复杂度为O(nlogk)。这种方法同样适用于海量数据的处理。
- 部分的快速排序(快速选择算法),每次划分之后判断第k个数在左右哪个部分,然后递归对应的部分,平均时间复杂度为O(n)。但最坏情况下复杂度为O(n^2)。
- BFPRT算法,修改快速选择算法的主元选取规则,使用中位数的中位数的作为主元,最坏情况下时间复杂度为O(n)。
二 快速选择算法
快速选择算法就是修改之后的快速排序算法,前面快速排序的实现与应用这篇文章中讲了它的原理和实现。
其主要思想就是在快速排序中得到划分结果之后,判断要求的第k个数是在划分结果的左边还是右边,然后只处理对应的那一部分,从而达到降低复杂度的效果。
在快速排序中,平均情况下数组被划分成相等的两部分,则时间复杂度为T(n)=2*T(n/2)+O(n),可以解得T(n)=nlogn。
在快速选择中,平均情况下数组也是非常相等的两部分,但是只处理其中一部分,于是T(n)=T(n/2)+O(n),可以解得T(n)=O(n)。
但是两者在最坏情况下的时间复杂度均为O(n^2),出现在每次划分之后左右总有一边为空的情况下。为了避免这个问题,需要谨慎地选取划分的主元,一般的方法有:
- 固定选择首元素或尾元素作为主元。
- 随机选择一个元素作为主元。
- 三数取中,选择三个数的中位数作为主元。一般是首尾数,再加中间的一个数或者随机的一个数。
为了方便,这里把前面的代码也放在这里。
int partition(int a[], int l, int r) //对数组a下标从l到r的元素进行划分
{
//随机选取一个数作为划分的基数
int rd = l + rand() % (r-l+1);
swap(a[rd], a[r]); int j = l - 1; //左边数字最右的下标
for (int i = l; i < r; i++)
if (a[i] <= a[r])
swap(a[++j], a[i]);
swap(a[++j], a[r]);
return j;
}
int NthElement(int a[], int l, int r, int id) //求数组a下标l到r中的第id个数
{
if (l == r) return a[l]; //只有一个数
int m = partition(a, l, r), cur = m - l + 1;
if (id == cur) return a[m]; //刚好是第id个数
else if(id < cur) return NthElement(a, l, m-1, id);//第id个数在左边
else return(a, m+1, r, id-cur); //第id个数在右边
}
三 BFPRT算法
BFPRT算法,又称为中位数的中位数算法,由5位大牛(Blum 、 Floyd 、 Pratt 、 Rivest 、 Tarjan)提出,并以他们的名字命名。参考维基上的介绍Median of medians。
算法的思想是修改快速选择算法的主元选取方法,提高算法在最坏情况下的时间复杂度。其主要步骤为:
- 首先把数组按5个数为一组进行分组,最后不足5个的忽略。对每组数进行排序(如插入排序)求取其中位数。
- 把上一步的所有中位数移到数组的前面,对这些中位数递归调用BFPRT算法求得他们的中位数。
- 将上一步得到的中位数作为划分的主元进行整个数组的划分。
- 判断第k个数在划分结果的左边、右边还是恰好是划分结果本身,前两者递归处理,后者直接返回答案。
首先看算法的主程序,代码如下。小于5个数的情况直接处理返回答案。否则每5个进行求取中位数并放到数组前面,递归调用自身求取中位数的中位数,然后用中位数作为主元进行划分。
注意这里只利用了中位数的下标,而不关心中位数的数值,目的是方便在划分函数中使用下标直接进行交换。BFPRT算法执行完毕之后可以保证我们想要的数字是排在了它真实的位置上,所以可以直接使用中位数的下标。
int BFPRT(int a[], int l, int r, int id) //求数组a下标l到r中的第id个数
{
if (r - l + 1 <= 5) //小于等于5个数,直接排序得到结果
{
insertionSort(a, l, r); return a[l + id - 1];
} int t = l - 1; //当前替换到前面的中位数的下标
for (int st = l, ed; (ed = st + 4) <= r; st += 5) //每5个进行处理
{
insertionSort(a, st, ed); //5个数的排序
t++; swap(a[t], a[st+2]); //将中位数替换到数组前面,便于递归求取中位数的中位数
} int pivotId = (l + t) >> 1; //l到t的中位数的下标,作为主元的下标
BFPRT(a, l, t, pivotId-l+1);//不关心中位数的值,保证中位数在正确的位置
int m = partition(a, l, r, pivotId), cur = m - l + 1;
if (id == cur) return a[m]; //刚好是第id个数
else if(id < cur) return BFPRT(a, l, m-1, id);//第id个数在左边
else return BFPRT(a, m+1, r, id-cur); //第id个数在右边
}
这里的划分函数与之前稍微不同,因为指定了划分主元的下标,所以参数增加了一个,并且第一步需要交换主元的位置。代码如下:
int partition(int a[], int l, int r, int pivotId) //对数组a下标从l到r的元素进行划分
{
//以pivotId所在元素为划分主元
swap(a[pivotId],a[r]); int j = l - 1; //左边数字最右的下标
for (int i = l; i < r; i++)
if (a[i] <= a[r])
swap(a[++j], a[i]);
swap(a[++j], a[r]);
return j;
}
这里简单分析一下BFPRT算法的复杂度。
划分时以5个元素为一组求取中位数,共得到n/5个中位数,再递归求取中位数,复杂度为T(n/5)。
得到的中位数x作为主元进行划分,在n/5个中位数中,主元x大于其中1/2*n/5=n/10的中位数,而每个中位数在其本来的5个数的小组中又大于或等于其中的3个数,所以主元x至少大于所有数中的n/10*3=3/10*n个。同理,主元x至少小于所有数中的3/10*n个。即划分之后,任意一边的长度至少为3/10,在最坏情况下,每次选择都选到了7/10的那一部分,则递归的复杂度为T(7/10*n)。
在每5个数求中位数和划分的函数中,进行若干个次线性的扫描,其时间复杂度为c*n,其中c为常数。其总的时间复杂度满足 T(n) <= T(n/5) + T(7/10*n) + c * n。
我们假设T(n)=x*n,其中x不一定是常数(比如x可以为n的倍数,则对应的T(n)=O(n^2))。则有 x*n <= x*n/5 + x*7/10*n + c*n,得到 x<=10*c。于是可以知道x与n无关,T(n)<=10*c*n,为线性时间复杂度算法。而这又是最坏情况下的分析,故BFPRT可以在最坏情况下以线性时间求得n个数中的第k个数。
算法复杂度也可以用树的方式来较准确的估计(略)
查找第K小的数 BFPRT算法的更多相关文章
- 快排查找第K小的数
#include "iostream.h" using namespace std; int findMedian(int *A,int left,int right){ int ...
- 基于快速排序思想partition查找第K大的数或者第K小的数。
快速排序 下面是之前实现过的快速排序的代码. function quickSort(a,left,right){ if(left==right)return; let key=partition(a, ...
- 算法---数组总结篇2——找丢失的数,找最大最小,前k大,第k小的数
一.如何找出数组中丢失的数 题目描述:给定一个由n-1个整数组成的未排序的数组序列,其原始都是1到n中的不同的整数,请写出一个寻找数组序列中缺失整数的线性时间算法 方法1:累加求和 时间复杂度是O(N ...
- 算法总结之 在两个排序数组中找到第K小的数
给定两个有序数组arr1 和 arr2 ,再给定一个int K,返回所有的数中第K小的数 要求长度如果分别为 N M,时间复杂度O(log(min{M,N}),额外空间复杂度O(1) 解决此题的方法跟 ...
- 每天一道算法题(32)——输出数组中第k小的数
1.题目 快速输出第K小的数 2.思路 使用快速排序的思想,递归求解.若键值位置i与k相等,返回.若大于k,则在[start,i-1]中寻找第k大的数.若小于k.则在[i+1,end]中寻找第k+st ...
- 每天一道算法题目(18)——取等长有序数组的上中位数和不等长有序数组的第k小的数
1.取上中位数 题目: 给定两个有序数组arr1和arr2,两个数组长度都为N,求两个数组中所有数的上中位数.要求:时间复杂度O(logN). 例如: arr1 = {1, ...
- 计算序列中第k小的数
作者:jostree 转载请注明出处 http://www.cnblogs.com/jostree/p/4046399.html 使用分治算法,首先选择随机选择轴值pivot,并使的序列中比pivot ...
- 第 k 小的数
一.寻找两个有序数组的中位数 1.1 问题描述 给定两个大小为 m 和 n 的不同时为空的有序数组 nums1 和 nums2.找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m ...
- POJ 2985 The k-th Largest Group(树状数组 并查集/查找第k大的数)
传送门 The k-th Largest Group Time Limit: 2000MS Memory Limit: 131072K Total Submissions: 8690 Acce ...
随机推荐
- 75.iOS内存管理
堆区和栈区 1.栈区:由编译器自动分配释放,函数的参数值,局部变量等值 2.堆区:一般由开发人员分配释放,若不释放,则可能会引起内存泄漏 NSString *string = @"abcd& ...
- Python12/25--前端之BOM/DOM
一.DOM 1. 什么是DOM 文档对象模型 Document Object Model 文档对象模型 是表示和操作 HTML和XML文档内容的基础API 文档对象模型,是W3C组织推荐的处理可扩展标 ...
- Nhibernate入门篇连接Sqlserver的增删查改
第一步:创建数据库 create table Emp( EmpId int primary key identity, EmpName ), EmpDate date ) 第二步:去官网下载:http ...
- Jira/Confluence的备份、恢复和迁移
之前的文章已经分别详细介绍了Jira.Confluence的安装及二者账号对接的操作方法,下面简单说下二者的备份.恢复和迁移: 一.Jira.Confluence的备份.恢复1)Confluence的 ...
- CVE-2015-1641 Office类型混淆漏洞及shellcode分析
作者:枕边月亮 原文来自:CVE-2015-1641 Office类型混淆漏洞及shellcode分析 0x1实验环境:Win7_32位,Office2007 0x2工具:Windbg,OD,火绒剑, ...
- 仿B站项目(4)webpack打包第三方库jQuery
概述 在项目中不可避免的会用到jquery等第三方库,来看看有什么问题,怎么解决. 遇到的问题 一般情况下,直接require第三方库,比如jquery,然后webpack会自动把第三方库打包进bun ...
- web API简介(二):客户端储存之document.cookie API
概述 前篇:web API简介(一):API,Ajax和Fetch 客户端储存从某一方面来说和动态网站差不多.动态网站是用服务端来储存数据,而客户端储存是用客户端来储存数据.document.cook ...
- 从零搭建java后台管理系统(一)框架初步搭建
框架搭建 一.初步设想,使用springboot,框架打算用到依赖 spring web,devTools,mysql,Aspect,Redis,Lombok,Freemark,Shiro,Rabbi ...
- C# 对密码等数据进行对称性加密解密
类: /// <summary> /// DESEncrypt加密解密算法. /// </summary> public class DESEncrypt { private ...
- gitlab服务部署及使用
一.什么是gitlib Gitlab 是一个基于Git实现的在线代码仓库托管软件,你可以用Gitlab自己搭建一个类似于Github一样的系统平台,一般搭建gitlab私服就是用在公司的内部 Gitl ...