BFPTR算法是由Blum、Floyed、Pratt、Tarjan、Rivest这五位牛人一起提出来的,其特点在于可以以最坏复杂度为O(n)O(n)O(n)地求解top−ktop-ktop−k问题。所谓top−ktop-ktop−k问题就是从一个序列中求解其第k大的问题。

top−ktop-ktop−k问题有许多解决方法,其他解决方法总结可以看我之前的一篇博客:传送门

这个算法的介绍更详细的可以参照算法导论9.3节 Selection in worst-case linear time,这里记录一下我对这个算法的理解、实现以及复杂度的证明。

下面详细的介绍一下BFPTR算法:

算法思想

宏观来看,BFPTR算法是快速排序思想的一种应用:

  1. 每次我们找到枢纽元素,然后按照枢纽元素将数组划分为两部分,就可以得到左边x个元素都是比枢纽元素小的,右边y个元素都是比枢纽元素大的

  2. 然后根据我们想要查找的k决定到哪边进行递归查找

    假设我们想要查找第k小,如果x==y,则枢纽元素就是需要查找的元素,如果x>k,则到左边继续查找,如果x<k,则去右边的区间查找第k-x个元素

典型的分治思想。问题的关键在于如何查找枢纽以及如何划分,如果这两步做的不够好那么将会导致子问题的规模不能够很快的缩小,从而使复杂度退化为O(n2)O(n^2)O(n2)

关于如何划分,主要就是快速排序的划分思想,我在另一篇博客中已经详细介绍了各种划分方法的优劣,如果有兴趣可以移步:传送门。

而BFPTR算法的精髓就在于如何选择枢纽以保证复杂度不会退化。

选择枢纽

  1. 将数组五个五个划分为┌n/5┐\ulcorner n/5 \urcorner┌n/5┐个小块,求出每个块的中位数
  2. 递归求出这些中位数的中位数,将这个数当做枢纽,如何做到这点呢?我们BFPTR不就是求第k大吗?我们递归地调用BFPTR就可以了。

我在网上仔细研究了其他人实现BFPTR的代码,个人觉得他们的代码并没有按照算法原本的思想操作(如果我理解的有问题欢迎大家批评指正),他们的做法都是将第一步划分的中位数再递归地划分为五个块,求取中位数,直到最后不足五个元素直接返回中位数作为枢纽。代码如下:

void GetPovit(T* a,int l,int r)
{int x;    //将区间分割为[x,x+5)int cnt=0;  //有多少个中位数for(x=l; x+5<r; x+=5){InsertSort(a,x,x+5);swap(a[l+cnt],a[x+2]);  //将当前区间的中位数放在最前面++cnt;}if(x<r){InsertSort(a,x,r);swap(a[l+cnt],a[(x+r)>>1]);++cnt;}if(1 == cnt) return;GetPovit(a,l,l+cnt);
}

虽然这样仍然可以解决问题,但是我觉得这样并不是BFPTR算法的思想,我们可以参见算法导论中对这一步骤的描述:

其中第三步中的递归地选择这些中位数中的中位数,这显然不是再将其划分为五个五个的小块求取中位数。因为我们再将其划分为五个五个的小块是不能保证最后选出来的是中位数的,如果这样可行的话就会有一种求中位数的方法是这样分块递归了,但是现实是我们现在做的正是这个算法。所以我认为他们的做法是不对的,可行的原因也只是这是类似于一种随机选取枢纽的操作。同样佐证这一点的是后面对于复杂度的分析,如果使用上面网上的做法那么是无法进行复杂度分析的。

我自己的做法是对于选出来的中位数调用BFPTR算法得到中位数

实现代码

首先是主体部分:

int BFPTR(T* a,int l,int r,int k)
{if(r-l == 1) return l;   //返回找到的数字//五个一组的中位数的中位数的位置int idx = GetPovit(a,l,r);T povit = a[idx];swap(a[l],a[idx]);int i=l,j=r;while(i<j){do ++i; while(i+1 < r && a[i]<povit);do --j; while(a[j]>povit);if(i<j) swap(a[i],a[j]);}swap(a[l],a[j]);int num=j-l+1;  //povit在当前序列中排第几if(k == num) return j;else if(num > k) return BFPTR(a,l,j,k);else return BFPTR(a,j+1,r,k-num);
}

这里的划分方法有一点点不同于我快速排序文章中的方法,主要是这里我将枢纽放在了中间,因为这样我们有可能直接返回枢纽而不必一定要将区间划分为1才返回。我认为可以一定程度上降低复杂度。
这里我返回的是需要查找的数在数组中的位置,觉得位置包含了更多的信息,我们知道位置以后可以直接得到查询的数字,可是返回查询的数字无法直接得到位置。

下面是最重要的获取枢纽的操作:

void InsertSort(T* a,int l,int r)
{//插入排序int mid=(l+r)>>1;   //获得中位数就足够了for(int i=l+1;i<=mid;++i){T x=a[i]; int j=i-1;while(j>=l && a[j]>x){a[j+1]=a[j]; --j;}a[j+1]=x;}
}int BFPTR(T* a,int l,int r,int k);T GetPovit(T* a,int l,int r)
{int x;    //将区间分割为[x,x+5)int cnt=0;  //有多少个中位数for(x=l; x+5<r; x+=5){InsertSort(a,x,x+5);swap(a[l+cnt],a[x+2]);  //将当前区间的中位数放在最前面++cnt;}if(x<r){InsertSort(a,x,r);swap(a[l+cnt],a[(x+r)>>1]);++cnt;}if(1 == cnt) return l;return BFPTR(a,l,l+cnt,cnt/2);
}

其中上面是一个简单的插入排序,下面是将数组划分为五个五个的小段,求取中位数以后放在数组的前面,然后再调用BFPTR函数得到中位数的中位数。之所以放在数组前面是因为这样可以原地操作而不用占用其他的空间。

完整的测试代码会附在最后,也会附上网上大家的代码供大家测试。

复杂度分析

对于一个规模为n的输入,其最坏的时间复杂度为T(n)=T(n5)+Θ(n)+T(7n10)+Θ(n)T(n)=T(\frac{n}{5})+\Theta(n)+T(\frac{7n}{10})+\Theta(n)T(n)=T(5n​)+Θ(n)+T(107n​)+Θ(n)

其中T(n5)+Θ(n)T(\frac{n}{5})+\Theta(n)T(5n​)+Θ(n)为求解其中位数的中位数的复杂度。我们首先将其划分为┌n/5┐\ulcorner n/5 \urcorner┌n/5┐个小块,然后求解每个小块的中位数是常数操作,因此总共是Θ(n)\Theta(n)Θ(n),前面的T(n5)T(\frac{n}{5})T(5n​)是BFPTR递归求解中位数的复杂度

最后的Θ(n)\Theta(n)Θ(n)是划分的复杂度

比较难理解的是为什么是T(7n10)T(\frac{7n}{10})T(107n​)。我们可以先看一张图(算法导论上盗下来的)


这里的箭头表示的含义是箭屁股的元素小于箭脑袋的元素。那么白元素的含义就很明显啦,即就是每个五个元素小块里面的中位数,其中的x表示中位数中的中位数。

由箭头关系我们不难看出,右下方的灰色区域的元素全部小于x,同理左上方区域的元素全部大于x,那么我们以x为枢纽元素将数组划分以后数组的两边至少都有这些元素。这些元素最少有多少呢?

白色区域大体和灰色区域的个数相等,因此我们计算灰色区域的个数。总共有┌n/5┐\ulcorner n/5 \urcorner┌n/5┐列,因为我们求取的是最小有多少,因此我们取下界└n/5┘\llcorner n/5 \lrcorner└n/5┘,灰色区域占到其中的一半,每一列有三个元素,因此总共有3∗┌└n/5┘/2┐3*\ulcorner \llcorner n/5 \lrcorner/2 \urcorner3∗┌└n/5┘/2┐,即最少3n10\frac{3n}{10}103n​的元素在一边。

我们考虑最坏的时间复杂度,因此另一边最多7n10\frac{7n}{10}107n​个元素,而我们总落入这一边。因此我们每次划分以后都需要再进行T(7n10)T(\frac{7n}{10})T(107n​)的递归。

接下来就是解递归式:T(n)=T(n5)+T(7n10)+Θ(n)T(n)=T(\frac{n}{5})+T(\frac{7n}{10})+\Theta(n)T(n)=T(5n​)+T(107n​)+Θ(n)。
对于这样的递归式我们没有什么好方法,只能选择代入法进行证明。假设T(k)=ckT(k)=ckT(k)=ck,那么我们需要证明
c∗n5+c∗7n10+Θ(n)<=cnc*\frac{n}{5}+c*\frac{7n}{10}+\Theta(n)<=cn c∗5n​+c∗107n​+Θ(n)<=cn

即就是
c∗n10>=Θ(n)c*\frac{n}{10}>=\Theta(n) c∗10n​>=Θ(n)

对于足够大的ccc,上式成立。
当处于基本情况时,对于足够大的ccc,上式也成立。

因此,BFPTR的复杂度为O(n)O(n)O(n),证毕。

为什么是5?

首先,我们肯定是要划分成奇数块的,偶数块求中位数不好求。
其次,最小可以选择的奇数是3,但是如果是3的话可以证明其复杂度就不是线性的了T(n)=T(n3)+Θ(n)+T(7n10)+Θ(n)T(n)=T(\frac{n}{3})+\Theta(n)+T(\frac{7n}{10})+\Theta(n)T(n)=T(3n​)+Θ(n)+T(107n​)+Θ(n)
也可以选择7,但是选择7的话常数会更大,因此我们选择将数组划分为长度为5的小区间

测试与分析

虽然这个算法的复杂度为O(n)O(n)O(n)的,但是我将按自己思路实现的代码和按网上思路实现的代码都对规模为1e81e81e8规模的数据进行了测试,发现BFPTR算法不是很快。原因可能是BFPTR算法的常数比较大,而网上的思路作为一种模拟的随机性快速选择算法常数比较小。但是我认为如果要使用随机性算法的话直接使用随机数常数会更小。关于随机性快速选择算法和模拟的随机性算法以及BFPTR到底哪一个更快,可以看我的另一篇博客:传送门

BFPTR算法代码

#include <iostream>
#include <fstream>
#include <ctime>using namespace std;typedef double T;T* CreatList(int &n)
{ifstream in("TestFile");in >> n;T* ret = new T[n];for(int i=0;i<n;++i){in>>ret[i];}in.close();return ret;
}void Show(T* a,int n)
{for(int i=0;i<n;++i){cout<<a[i]<<" ";}cout<<endl;
}void InsertSort(T* a,int l,int r)
{//插入排序int mid=(l+r)>>1;   //获得中位数就足够了for(int i=l+1;i<=mid;++i){T x=a[i]; int j=i-1;while(j>=l && a[j]>x){a[j+1]=a[j]; --j;}a[j+1]=x;}
}int BFPTR(T* a,int l,int r,int k);T GetPovit(T* a,int l,int r)
{int x;    //将区间分割为[x,x+5)int cnt=0;  //有多少个中位数for(x=l; x+5<r; x+=5){InsertSort(a,x,x+5);swap(a[l+cnt],a[x+2]);  //将当前区间的中位数放在最前面++cnt;}if(x<r){InsertSort(a,x,r);swap(a[l+cnt],a[(x+r)>>1]);++cnt;}if(1 == cnt) return l;return BFPTR(a,l,l+cnt,cnt/2);
}int BFPTR(T* a,int l,int r,int k)
{if(r-l == 1) return l;   //返回找到的数字//五个一组递归求取中位数int idx = GetPovit(a,l,r);T povit = a[idx];swap(a[l],a[idx]);int i=l,j=r;while(i<j){do ++i; while(i+1 < r && a[i]<povit);do --j; while(a[j]>povit);if(i<j) swap(a[i],a[j]);}swap(a[l],a[j]);int num=j-l+1;  //povit在当前序列中排第几if(k == num) return j;else if(num > k) return BFPTR(a,l,j,k);else return BFPTR(a,j+1,r,k-num);
}void Query(T* a,int n)
{int k;while(true){cout<<"请输入k:";cin>>k;if(cin.fail() == true)break;if(k>n || k<1){//检查输入的有效性cout<<"请输入1~"<<n<<"之间的数字"<<endl;continue;}clock_t S,E;S=clock();int idx=BFPTR(a,0,n,k);E=clock();cout<<"区间第"<<k<<"大的数字为:"<<a[idx]<<endl;cout<<"这次查询花费时间"<<(double)(E-S)/CLOCKS_PER_SEC<<"s"<<endl;}cin.clear();cin.ignore();
}int main()
{int n;T* a=CreatList(n);Query(a,n);delete[] a;return 0;
}

模拟随机化选择的代码:

#include <iostream>
#include <fstream>
#include <ctime>using namespace std;typedef double T;T* CreatList(int &n)
{ifstream in("TestFile");in >> n;T* ret = new T[n];for(int i=0;i<n;++i){in>>ret[i];}in.close();return ret;
}void Show(T* a,int n)
{for(int i=0;i<n;++i){cout<<a[i]<<" ";}cout<<endl;
}void InsertSort(T* a,int l,int r)
{//插入排序for(int i=l+1;i<r;++i){T x=a[i]; int j=i-1;while(j>=l && a[j]>x){a[j+1]=a[j]; --j;}a[j+1]=x;}
}void GetPovit(T* a,int l,int r)
{int x;    //将区间分割为[x,x+5)int cnt=0;  //有多少个中位数for(x=l; x+5<r; x+=5){InsertSort(a,x,x+5);swap(a[l+cnt],a[x+2]);  //将当前区间的中位数放在最前面++cnt;}if(x<r){InsertSort(a,x,r);swap(a[l+cnt],a[(x+r)>>1]);++cnt;}if(1 == cnt) return;GetPovit(a,l,l+cnt);
}int BFPTR(T* a,int l,int r,int k)
{if(r-l == 1) return l;   //返回找到的数字GetPovit(a,l,r);            //五个一组递归求取中位数T povit=a[l];int i=l-1,j=r;while(i<j){do ++i; while(a[i]<povit);do --j; while(a[j]>povit);if(i<j) swap(a[i],a[j]);}if(j-l+1>=k) return BFPTR(a,l,j+1,k);else return BFPTR(a,j+1,r,k-j+l-1);
}void Query(T* a,int n)
{int k;while(true){cout<<"请输入k:";cin>>k;if(cin.fail() == true)break;if(k>n || k<1){//检查输入的有效性cout<<"请输入1~"<<n<<"之间的数字"<<endl;continue;}clock_t S,E;S=clock();int idx=BFPTR(a,0,n,k);E=clock();cout<<"区间第"<<k<<"大的数字为:"<<a[idx]<<endl;cout<<"这次查询花费时间"<<(double)(E-S)/CLOCKS_PER_SEC<<"s"<<endl;}cin.clear();cin.ignore();
}int main()
{int n;T* a=CreatList(n);Query(a,n);delete[] a;return 0;
}

BFPTR算法详解+实现+复杂度证明相关推荐

  1. 【机器学习基础】EM算法详解及其收敛性证明

    EM算法详解 (一)单高斯模型 1.1 一维高斯分布: 1.2 多维高斯分布: (二)最大似然估计 2.1 最大似然估计的数学概念: 2.2 最大似然估计的基本步骤: 2.2.1 构造似然函数: 2. ...

  2. 新颖性搜索(Novelty Search,NS)算法详解与实现

    新颖性搜索(Novelty Search,NS)算法详解与实现 基于目标的进化算法的缺点 新颖性搜索与自然进化 新颖性指标 算法描述 新颖性搜索算法实践 基于目标的进化算法的缺点 大多数机器学习方法( ...

  3. NEAT(NeuroEvolution of Augmenting Topologies)算法详解与实践(基于NEAT-Python)

    NEAT(NeuroEvolution of Augmenting Topologies)算法详解与实践(基于NEAT-Python) NEAT算法详解 NEAT算法概述 NEAT编码方案 结构突变 ...

  4. 排序算法,最全的10大排序算法详解(Sort Algorithm)

    文章目录 排序算法,最全的10大排序算法详解(Sort Algorithm) 排序算法分类 排序算法稳定性 时间复杂度(time complexity) 1#时间复杂度的意义 2#基本操作执行次数 如 ...

  5. 【机器学习】集成学习及算法详解

    集成学习及算法详解 前言 一.随机森林算法原理 二.随机森林的优势与特征重要性指标 1.随机森林的优势 2.特征重要性指标 三.提升算法概述 四.堆叠模型简述 五.硬投票和软投票 1.概念介绍 2.硬 ...

  6. 神经网络算法详解 01:人工神经网络基础

    本文介绍了人工智能的发展历史,基本概念,应用领域:神经元模型,神经元的学习规则以及神经网络工作原理.本系列文章来自阿里云大学人工智能学习路线中的<神经网络概览及神经网络算法详解>课程. 系 ...

  7. 【最长回文子串】Manacher算法详解

    写在前面 manacher算法解决最长回文子串以及变形问题的时间复杂度为O(n). 如果你想囫囵吞枣,只需要使用到该算法,你可以直接把代码拿走:但如果你想深入了解这个算法的工作原理和关键部分解读,还是 ...

  8. svm 算法详解与推导

    前言 由于纸质笔记又臭又长(SVM这篇长达六张纸),可能字太难看完全没欲望于是把一些笔记整理到网上(公式可能太多),方便翻阅. 主要有这么个部分: 1. 函数间距.几何间距.软间距定义与区别. 2. ...

  9. 粒子群算法(PSO)——算法详解(一)

    PSO的算法配置 粒子的速度更新公式:vi(t+1)=vi(t)+c1⋅r1(t)⋅[yi(t)−xi(t)]+c2⋅r2(t)⋅[y^i(t)−xi(t)]\mathbf v_{i}(t+1)=\m ...

最新文章

  1. java short to byte_java笔记 Java中byte数组与int,long,short,char间的转换
  2. Apache-2.2.32安装配置
  3. 如何快速向数据库插1000万数据?4种方法对比,它简单却速度最快
  4. 论文解读:深度监督网络(Deeply-Supervised Nets)
  5. 三元环计数四元环计数
  6. 软件开发的微信公众号分享
  7. SpringBoot + MyBatis(注解版),常用的SQL方法
  8. python 栈和队列_python 栈和队列的基本实现
  9. ios安装python的步骤_如何利用 Python 爬虫实现给微信群发新闻早报?(详细)
  10. 13001.udp广播接收程序(python)
  11. 想要改变自己,请先突破这3个自我限制
  12. POD 创建 Xcode 项目组
  13. 2018 开发者生态报告:Java 最流行,Go 最有潜力,JavaScript最常用
  14. 《C++程序设计POJ》《WEEK3 类和对象进阶》成员对象和封闭类/友元/this指针/常量成员函数...
  15. st7789 旋转_有没有人调过 ST7789V驱动的显示屏啊
  16. 【PS图像处理】PS软件提示不能完成命令,因为暂存盘已满的解决办法
  17. 运筹说 第12期 | 佳片推荐之“心灵捕手”
  18. 解决Connection error: (‘Unable to connect to any servers‘, {‘192.168.193.136‘: OperationTimedOut。。。。
  19. java如何用雪花算法批量生成唯一编码(保证位数不过长)?
  20. hexo(sakura)给博客增添侧边栏(回到顶部,跳转评论,深色模式,播放音乐)Valine-1.4.4新版本尝鲜+个性制定(表情包、qq头像、UI样式)

热门文章

  1. NOIP2017年11月9日赛前模拟
  2. Oracle锁机制的总结【转】
  3. 自定义控件_VIewPager显示多个Item
  4. 对c++primer 16.6.1的第4小节的代码说明
  5. HK-2000 数采仪系统说明之 5.H2000SService程序调用说明
  6. php数据库中统计人数用什么方法,在PHP中处理用户统计信息的最佳方法是什么
  7. 端口如何支持非localhost访问_新特性解读 | MySQL 8.0.19 支持 DNS SRV
  8. netcore 内存限制_.NET Core 和 Serverless 构建飞速发展的架构
  9. html 文本框数量加减,收藏!js实现input加减
  10. linux转换vcf格式,如何使用awk分割vCard通讯录文件(.vcf)