BFPRT 算法 (TOP-K 问题)——本质就是在利用分组中位数的中位数来找到较快排更合适的pivot元素
先说快排最坏情况下的时间复杂度为n^2。
正常情况:

最坏的情况下,待排序的记录序列正序或逆序,每次划分只能得到一个比上一次划分少一个记录的子序列,(另一个子序列为空)。此时,必须经过n-1次递归调用才能把所有记录定位,而且第i趟划分需要经过n-i次比较才能找个才能找到第i个记录的位置,因此时间复杂度为

在BFPTR算法中,仅仅是改变了快速排序Partion中的pivot值的选取,在快速排序中,我们始终选择第一个元素或者最后一个元素作为pivot,而在BFPTR算法中,每次选择五分中位数的中位数作为pivot,这样做的目的就是使得划分比较合理,从而避免了最坏情况的发生。算法步骤如下:
1. 将 个元素划为
组,每组5个,至多只有一组由
个元素组成。
2. 寻找这 个组中每一个组的中位数,这个过程可以用插入排序。
3. 对步骤2中的 个中位数,重复步骤1和步骤2,递归下去,直到剩下一个数字。
4. 最终剩下的数字即为pivot,把大于它的数全放左边,小于等于它的数全放右边。
5. 判断pivot的位置与k的大小,有选择的对左边或右边递归。
当你看到本质上后,至于”首先把数组按5个数为一组进行分组,最后不足5个的忽略。 ” 、“偶数个元素的中位数取中间2个中较小的一个。”这些小细节都是无关紧要的了!!!
下面为代码实现,其所求为前 k 小的数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | #include <iostream> #include <algorithm> using namespace std; int InsertSort( int array[], int left, int right); int GetPivotIndex( int array[], int left, int right); int Partition( int array[], int left, int right, int pivot_index); int BFPRT( int array[], int left, int right, int k); int main() { int k = 8 ; / / 1 < = k < = array.size int array[ 20 ] = { 11 , 9 , 10 , 1 , 13 , 8 , 15 , 0 , 16 , 2 , 17 , 5 , 14 , 3 , 6 , 18 , 12 , 7 , 19 , 4 }; cout << "原数组:" ; for ( int i = 0 ; i < 20 ; i + + ) cout << array[i] << " " ; cout << endl; / / 因为是以 k 为划分,所以还可以求出第 k 小值 cout << "第 " << k << " 小值为:" << array[BFPRT(array, 0 , 19 , k)] << endl; cout << "变换后的数组:" ; for ( int i = 0 ; i < 20 ; i + + ) cout << array[i] << " " ; cout << endl; return 0 ; } / * * * 对数组 array[left, right] 进行插入排序,并返回 [left, right] * 的中位数。 * / int InsertSort( int array[], int left, int right) { int temp; int j; for ( int i = left + 1 ; i < = right; i + + ) { temp = array[i]; j = i - 1 ; while (j > = left && array[j] > temp) array[j + 1 ] = array[j - - ]; array[j + 1 ] = temp; } return ((right - left) >> 1 ) + left; } / * * * 数组 array[left, right] 每五个元素作为一组,并计算每组的中位数, * 最后返回这些中位数的中位数下标(即主元下标)。 * * @attention 末尾返回语句最后一个参数多加一个 1 的作用其实就是向上取整的意思, * 这样可以始终保持 k 大于 0 。 * / int GetPivotIndex( int array[], int left, int right) { if (right - left < 5 ) return InsertSort(array, left, right); int sub_right = left - 1 ; / / 每五个作为一组,求出中位数,并把这些中位数全部依次移动到数组左边 for ( int i = left; i + 4 < = right; i + = 5 ) { int index = InsertSort(array, i, i + 4 ); swap(array[ + + sub_right], array[index]); } / / 利用 BFPRT 得到这些中位数的中位数下标(即主元下标) return BFPRT(array, left, sub_right, ((sub_right - left + 1 ) >> 1 ) + 1 ); } / * * * 利用主元下标 pivot_index 进行对数组 array[left, right] 划分,并返回 * 划分后的分界线下标。 * / int Partition( int array[], int left, int right, int pivot_index) { swap(array[pivot_index], array[right]); / / 把主元放置于末尾 int partition_index = left; / / 跟踪划分的分界线 for ( int i = left; i < right; i + + ) { if (array[i] < array[right]) { swap(array[partition_index + + ], array[i]); / / 比主元小的都放在左侧 } } swap(array[partition_index], array[right]); / / 最后把主元换回来 return partition_index; } / * * * 返回数组 array[left, right] 的第 k 小数的下标 * / int BFPRT( int array[], int left, int right, int k) { int pivot_index = GetPivotIndex(array, left, right); / / 得到中位数的中位数下标(即主元下标) int partition_index = Partition(array, left, right, pivot_index); / / 进行划分,返回划分边界 int num = partition_index - left + 1 ; if (num = = k) return partition_index; else if (num > k) return BFPRT(array, left, partition_index - 1 , k); else return BFPRT(array, partition_index + 1 , right, k - num); } |
运行如下:
原数组:11 9 10 1 13 8 15 0 16 2 17 5 14 3 6 18 12 7 19 4
第 8 小值为:7
变换后的数组:4 0 1 3 2 5 6 7 8 9 10 12 13 14 17 15 16 11 18 19
性能分析:
划分时以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个数。
时间复杂度为O(n)的原因
我们选取的x可以在每次递归的时候最少淘汰(n/10-2)x3的数据量
详细的时间复杂度分析见:https://blog.csdn.net/LaoJiu_/article/details/54986553
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
2018-01-10 dns tunnel 使用 nishang 下载TXT里的cmd(TXT里)实现CC command+ ceye实现数据外发
2018-01-10 Powershell 渗透测试工具-Nishang
2018-01-10 powershell渗透工具——PowerShell攻防进阶篇:nishang工具用法详解
2017-01-10 elasticsearch负载均衡节点——客户端节点 node.master: false node.data: false 其他配置和master 数据节点一样
2017-01-10 Elasticsearch压缩索引——lucene倒排索引本质是列存储+使用嵌套文档可以大幅度提高压缩率
2017-01-10 elasticsearch 2.2+ index.codec: best_compression启用压缩