BFPRT算法是解决从n个数中选择第k大或第k小的数这个经典问题的著名算法,但很多人并不了解其细节。本文将首先介绍求解这个第k小数字问题的几个思路,然后重点介绍在最坏情况下复杂度仍然为O(n)的BFPRT算法。

一 基本思路

关于选择第k小的数字有许多方法,其效率和复杂度各不一样,可以根据实际情况进行选择。

  1. 将n个数排序(比如快速排序或归并排序),选取排序后的第k个数,时间复杂度为O(nlogn)。使用STL函数sort可以大大减少编码量。
  2. 将方法1中的排序方法改为线性时间排序算法(如基数排序或计数排序),时间复杂度为O(n)。但线性时间排序算法使用限制较多,不常使用。
  3. 维护一个k个元素的最大堆,存储当前遇到的最小的k个数,时间复杂度为O(nlogk)。这种方法同样适用于海量数据的处理。
  4. 部分的选择排序,即把最小的放在第1位,第二小的放在第2位,直到第k位为止,时间复杂度为O(kn)。实现非常简单。
  5. 部分的快速排序(快速选择算法),每次划分之后判断第k个数在左右哪个部分,然后递归对应的部分,平均时间复杂度为O(n)。但最坏情况下复杂度为O(n^2)。
  6. 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),出现在每次划分之后左右总有一边为空的情况下。为了避免这个问题,需要谨慎地选取划分的主元,一般的方法有:

  1. 固定选择首元素或尾元素作为主元。
  2. 随机选择一个元素作为主元。
  3. 三数取中,选择三个数的中位数作为主元。一般是首尾数,再加中间的一个数或者随机的一个数。

============================================================

通常,我们需要在一大堆数中求前K大的数,或者求前K小的。比如在搜索引擎中求当天用户点击次数排名前10000的热词;在文本特征选择中求IF-IDF值按从大到小排名前K个的等等问题,都涉及到一个核心问题,即TOP-K问题

通常来说,TOP-K问题可以先对所有数进行快速排序,然后取前K大的即可。但是这样做有两个问题。

(1)快速排序的平均复杂度为,但最坏时间复杂度为,不能始终保证较好的复杂度。

(2)我们只需要前K大的,而对其余不需要的数也进行了排序,浪费了大量排序时间。

除这种方法之外,堆排序也是一个比较好的选择,可以维护一个大小为K的堆,时间复杂度为

我们的目的是求前K大的或者前K小的元素,实际上有一个比较好的算法,叫做BFPTR算法,又称为中位数的中位数算法,它的最坏时间复杂度为,它是由Blum、Floyd、Pratt、Tarjan、Rivest 提出。

该算法的思想是修改快速选择算法的主元选取方法,提高算法在最坏情况下的时间复杂度。我们先来看看快速排序是如何进行的。

一趟快速排序的过程如下

(1)先从序列中选取一个数作为基准数。

(2)将比这个数大的数全部放到它的右边,把小于或者等于它的数全部放到它的左边。

一趟快速排序也叫做Partion,即将序列划分为两部分,一部分比基准数小,另一部分比基准数大,然后再进行分治过程,因为每一次Partion不一定都能保证划分得很均匀,所以最坏情况下的时间复杂度不能保证总是为

对于Partion过程,通常有两种方法:

(1)两个指针从首尾向中间扫描(双向扫描)

这种方法可以用挖坑填数来形容,比如

初始化:i = 0; j = 9; pivot = a[0];

现在a[0]保存到了变量pivot中了,相当于在数组a[0]处挖了个坑,那么可以将其它的数填到这里来。从j开始向前找一个小于或者等于pivot的数,即将a[8]填入a[0],但a[8]又形成了一个新坑,再从i开始向后找一个大于pivot的数,即a[3]填入a[8],那么a[3]又形成了一个新坑......

就这样,直到i==j才停止,最终得到结果如下

上述过程就是一趟快速排序

代码:

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
#include <time.h>using namespace std;
const int N = 10005;int Partion(int a[], int l, int r)
{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;
}void QuickSort(int a[], int l, int r)
{if (l < r){int k = Partion(a, l, r);QuickSort(a, l, k - 1);QuickSort(a, k + 1, r);}
}int a[N];int main()
{int n;while (cin >> n){for (int i = 0; i < n; i++)cin >> a[i];QuickSort(a, 0, n - 1);for (int i = 0; i < n; i++)cout << a[i] << " ";cout << endl;}return 0;
}

(2)两个指针一前一后逐步向前扫描(单向扫描)

代码:

#include <iostream>
#include <string.h>
#include <stdio.h>  using namespace std;
const int N = 10005;  int Partion(int a[], int l, int r)
{  int i = l - 1;  int pivot = a[r];  for(int j = l; j < r; j++)  {  if(a[j] <= pivot)  {  i++;  swap(a[i], a[j]);  }  }  swap(a[i + 1], a[r]);  return i + 1;
}  void QuickSort(int a[], int l, int r)
{  if(l < r)  {  int k = Partion(a, l, r);  QuickSort(a, l, k - 1);  QuickSort(a, k + 1, r);  }
}  int a[N];  int main()
{  int n;  while(cin >> n)  {  for(int i = 0; i < n; i++)  cin >> a[i];  QuickSort(a, 0, n - 1);  for(int i = 0; i < n; i++)  cout << a[i] << " ";  cout << endl;  }  return 0;
}  

实际上基于双向扫描的快速排序要比基于单向扫描的快速排序算法快很多。接下来,我们学习BFPTR算法的原理。

BFPTR算法中,仅仅是改变了快速排序Partion中的pivot值的选取,在快速排序中,我们始终选择第一个元素或者最后一个元素作为pivot,而在BFPTR算法中,每次选择五分中位数的中位数作为pivot,这样做的目的就是使得划分比较合理,从而避免了最坏情况的发生。算法步骤如下:

(1)将输入数组的个元素划分为组,每组5个元素,且至多只有一个组由剩下的个元素组成。

(2)寻找个组中每一个组的中位数,首先对每组的元素进行插入排序,然后从排序过的序列中选出中位数。

(3)对于(2)中找出的个中位数,递归进行步骤(1)和(2),直到只剩下一个数即为这个元素的中位数,找到中位数后并找到对应的下标

(4)进行Partion划分过程,Partion划分中的pivot元素下标为

(5)进行高低区判断即可。

本算法的最坏时间复杂度为,值得注意的是通过BFPTR算法将数组按第K小(大)的元素划分为两部分,而这高低两部分不一定是有序的,通常我们也不需要求出顺序,而只需要求出前K大的或者前K小的。

另外注意一点,求第K大就是求第n-K+1小,这两者等价。TOP K问题在工程中有重要应用,所以很有必要掌握。

代码:

#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 a[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 a[l];  return FindMid(a, l, l + n);
}  //寻找中位数的所在位置
int FindId(int a[], int l, int r, int num)
{  for(int i = l; i <= r; i++)  if(a[i] == num)  return i;  return -1;
}  //进行划分过程
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 BFPTR(int a[], int l, int r, int k)
{  int num = FindMid(a, l, r);    //寻找中位数的中位数  int p =  FindId(a, l, r, num); //找到中位数的中位数对应的id  int i = Partion(a, l, r, p);  int m = i - l + 1;  if(m == k) return a[i];  if(m > k)  return BFPTR(a, l, i - 1, k);  return BFPTR(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, BFPTR(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
*/  

关于本算法最坏时间复杂度为的证明可以参考《算法导论》9.3节,即112页,有详细分析。

原文链接:https://blog.csdn.net/wyq_tc25/article/details/51801885

bfptr算法(即中位数的中位数算法)相关推荐

  1. 带权中位数-算法导论第三版第九章思考题9-2

    带权中位数-算法导论第三版第九章思考题9-2 b 时间复杂度O(nlgn) float find_median_with_weights_b(float *array,int length) {qui ...

  2. c语言编程n位自幂数,自幂数9位数查找之算法优化(C语言)(水仙数是4位数自幂数)...

    ``# 自幂数的9位数查找之算法优化(C语言) 这是一篇C语言有关自幂数查找的优化过程,目前笔者最好结果是8位数用时7.007秒,9位数用时79.079秒.(水仙数是4位数自幂数) 期待有更棒的结果. ...

  3. mysql 编程算法_十大编程算法助程序员走上高手之路

    算法一:快速排序算法 快 速排序是由东尼·霍尔所发展的一种排序算法.在平均状况下,排序 n 个项目要Ο(n log n)次比较.在最坏状况下则需要Ο(n2)次比较,但这种 状况并不常见.事实上,快速排 ...

  4. 排序算法 - 面试中的排序算法总结

    排序算法总结 查找和排序算法是算法的入门知识,其经典思想可以用于很多算法当中.因为其实现代码较短,应用较常见.所以在面试中经常会问到排序算法及其相关的问题.但万变不离其宗,只要熟悉了思想,灵活运用也不 ...

  5. cmos全局曝光算法_2019腾讯广告算法大赛方案分享(冠军)

    来自公众号:机器学习初学者 本文提供2019年腾讯广告算法大赛冠军的代码分享. 俞士纶(Philip S. Yu)教授的评价"冠军队伍已经在有意无意使用"广度学习"的方法 ...

  6. 疯子的算法总结(四)贪心算法

    一.贪心算法 解决最优化问题的算法一般包含一系列的步骤,每一步都有若干的选择.对于很多最优化问题,只需要采用简单的贪心算法就可以解决,而不需要采用动态规划方法.贪心算法使所做的局部选择看起来都是当前最 ...

  7. 相似图像搜索的哈希算法思想及实现(差值哈希算法和均值哈希算法)

    图像相似度比较哈希算法: 什么是哈希(Hash)? • 散列函数(或散列算法,又称哈希函数,英语:Hash Function)是一种从任何一种数据中创建小 的数字"指纹"的方法.散 ...

  8. 令人拍案叫绝的算法学习网站新手算法入门到精通,算法面试冲刺资料这里都有

    (9月已更)学算法认准这6个网站就够了! 写在前面:作为ACM铜牌选手,从FB到腾讯,从事算法&java岗位工作也是5年有余.在工作中接触到了很多同学,在算法学习和算法面试这件事上我还是很有发 ...

  9. 算法的浅论:算法前序

    博客中的图片可以顺序出现问题,以全力弥补,未防止疏漏,特将文档链接放于此处,可以优先浏览文档 [金山文档] 算法考试解析 https://kdocs.cn/l/cijvgvO6a4Tj 一.快速排序 ...

  10. php md5算法,php如何实现md5算法?

    php实现md5算法:1.当数组元素超过整形长度时的自动转换:2.实现无符号右移操作:3.将字符串转换成8位存储为一个元素的数据结构. PHP实现MD5算法: 1.MD5算法是对输入的数据进行补位,使 ...

最新文章

  1. 使用扩展方法和静态门面类实现伪领域对象
  2. 探索JAVA并发 - 线程池详解
  3. RocketMQ 高级功能介绍
  4. Google Chrome Source Code 源码下载
  5. 【编程语言】Java基础进阶——面向对象部分
  6. 到底什么是RestFul架构?
  7. 计组之总线:2、总线仲裁(链式查询、计数器查询、独立请求、分布式查询)
  8. 数千万智能手机集体脱机?罪魁祸首是……
  9. 《Android游戏开发详解》——导读
  10. 中文系统使用日文键盘-转
  11. 金山html编辑器,fresh html
  12. 搭建 Kodbox 私有云教程
  13. java出现报错java.lang.IndexOutOfBoundsException
  14. Vue-cli 脚手架构建的项目使用echarts进行数据可视化
  15. xshell是什么软件
  16. 计算机毕业设计(附源码)python英语四六级在线学习系统
  17. pac for linux,Ubuntu下安装PAC Manager 4.5.3.9
  18. 用python画小黄人
  19. 添加数据时候获取自增的ID
  20. 【Leetcode】487. Max Consecutive Ones II

热门文章

  1. 计算字符串占用字节数
  2. 一年级下册计算机教学计划,一年级下册教学计划
  3. css实现简单几何图形
  4. 第五课 大数据技术之Fink1.13的实战学习-状态编程和容错机制
  5. 接收前端传回的JSON字符串,并存入数据库
  6. Python中shape简易用法
  7. 到底要不要去外包公司?这篇带你全面了解外包那些坑!
  8. 青铜时代 —— 图像处理
  9. 如何让Excel输入数据后自动保护,不能被修改
  10. 结构化程序设计方法和面向对象程序设计方法的区别