打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
N个数里面找出最大的k个数

寻找最优的 TopN 算法


1 概要
在大量的数据记录中,依据某可排序的记录属性(一般为数字类型),找出最大的前 N 个记录,称为
TopN 问题。这是一个常常遇到的问题,也是一个比较简单的算法问题,却很少能有人能写出最优化的
topn 算法。本文对常见的 TopN 算法,进行分析比较,最后给出最优的 TopN 算法:基于小根堆的筛选
法.
2 问题定义
为了方便。我们把问题具体化:要求使用 C 语言来实现函数,在一个大小为 m 的,无序的整数数组中选
取最大的 n 个整数。即,问题定义为一个函数的实现:
int * topn(int *pdata, int m,int *ptop, int n);
其中,pdata 指向保存大量整数的内存,整数的个数为 m,要求函数把前 n 个最大的整数保存到 ptop 指
向的内存中,显然满足 m≥n≥0 。 同时假定 n
m
2
, 因为当 n 如果超过半数,可以问题转换为排
除 n?
m
2
个最小的整数----这种转换减少了问题规模。并且,已经实现选择 n 个最值(最大或最小)
的算法的情况下,排除 n 个最值的算法可以仿照实现。这里不再费时间叙述。
我们虽然假设的数据类型为整数,其他任意的数据类型,只要其数据是可以排序的,都可以根据这里的
算法,写出对应的算法实现。同样,我们虽然假定输入数据的方式为数组,实际输入的方式可以是任意
其他方式(如文件,数据库等)。
3 算法介绍
3.1 算法 1 完整排序法
对 pdata 中的数据,进行内部排序,然后选取前 n 个整数放到 ptop 中。如果使用较高效率的内部排序
算法(如:快速排序,堆排序),复杂度为 m?log2
m .
使用这种算法,结果数据是从大到小排序的。
3.2 算法 2 基于有序数组的筛选法
保持 ptop 中的结果数组有序,即满足 ptop[0]≥ptop[1]≥...≥ptop[n?1] ,遍历 pdata 中的数
据,如果发现当前整数大于 ptop[n-1]就把数据插入到合适位置,以保持 ptop 中的仍然有序。算法复
杂度为. m?n
使用这种算法,结果数据是从大到小排序的。显然,只从理论上的复杂度考虑,就可以看出来:当 n 较小的时候,算法 2 有较高的效率。但当 n 较大
的时候,算法 1 会有较高的效率。
仔细分析算法 2, 保持 ptop 中数据有序的目的,就是每次能找到最小的值,在有序数组中插入一个新元
素,最坏需要 n 次操作才能保持数组有序。其实要快速找到最小的值,使用小根堆更合适,因为中堆中
每次插入一个元素,最坏只需要 log2
n 次操作,根据这个思路产生下面的算法 3。
3.3 算法 3 基于小根堆的筛选法。
使用大小为 n 的小根堆保存结果集,最小项为堆的根节点 ptop[0],遍历 pdata 中,如果当前数据大于
ptop[0],并把数据替换根节点,并对根节点 SiftDown 操作,保持结果集仍然为小根堆。算法复杂度,
只有 m?log2
n .- 更多关于小根堆的介绍,请看后面。
显然有,
m?log2 nm?log2 m , m?log2 nm?n
所以,算法 3 的复杂度最低。
这种算法,生成的结果数据是没有排序的。因为结果已经保存最小根堆种,如果需要排序,只需要完成
常规堆排序流程的后半部分,排序需要的复杂度只有 n。
4 实际测试结果
测试环境:
OS: Debain Linux kernel 2.6.26
C Complier: gcc 4.3.4
MEM: 768m
CPU: Intel(R) Pentium(R) 4 CPU 2.40GHz


5 算法分析
1. 在任何情况下,算法 3 都是最优的。但算法 3 的实现较复杂。
2. 如果不想实现复杂的算法 3,如果 n 相对于 m 很小,可以选择算法 2
3. 如果不想实现复杂的算法 3, 如果 n 比较大,例如:接近了 m/2, 可以选择算法 1
4. 如果不要求结果排序,算法 3 更有优势。
5. 算法 1 和算法 2,结果自然就排号顺序了。是否要求排序与其时间花费无关。
根据理论复杂度和实际的测试结果,更有下面的结论。
当 m 值固定,n 的变化区间为[1,m/2],三种算法消耗时间随 n 变化的曲线,如下面的图示意。相应的,当 n 固定,m 的变化区间为[2*n, ∞ ),三种算法消耗算法随 m 变化的曲线,如下面的图示意。


6 算法实现
6.1 算法 1 实现
使用标准 c 语言库支持的 qsort,直接就可以实现。
//用于比较操作的函数
static int cmpint(const void *p1, const void *p2)
{
return *((int *)p2) - *((int *)p1);
}
//算法实现
int * topn1(int *pdata, int m,int *ptop, int n)
{
qsort(pdata, m, sizeof(int),cmpint);
memcpy(ptop, pdata, n*sizeof(int));
return ptop;
}
注意:如果自己实现 qsort 算法,会有一定效率的提高。但代码级的优化,这里不会对效率产生本质的
影响。6.2 算法 2 实现
比较简单,源代码如下
int *topn2(int *pdata,int m,int *ptop,int n)
{
int i;
for(i=0;i<m;i++)
{
//判断当前结果集的大小(数组的长度)
int t = (i+1<n) ? (i+1):n;
if(i>=n && pdata[i] <= ptop[n-1])
continue;
int j=t-1;
for(;j>=1 && pdata[i]>ptop[j-1];j--)
ptop[j] = ptop[j-1];
ptop[j] = pdata[i];
}
return ptop;
}
注意:如果使用监视哨等优化手段,可以减少循环的判断条件。但这种代码级别的优化,这里不会对效
率产生本质的影响。
6.3 算法 3 实现
6.3.1 小根堆概念定义
小根堆精确定义:数组 x[1..n](注意,这里假定数组的第一个元素的索引为 1,是为了下面描述方便),
如果满足:
?i?[2,n], x[i/2]≤x [i]
就说明,这个数组就是小根堆
为了下面描述方便,我们定义对数组 x[1..n]的子数组 x[u..v],( 1≤u≤v≤n ),如果满足:
?i?[2l, u],x[i/2]≤x [i]
就说明:x 整个数组的[u..v]部分已经是小根堆了。
6.3.2 我们需要的堆操作
//如果[2..n]已经是堆了,-除了第一个元素,其他部分已经是小根堆。
//通过下面的函数把第一个元素,往后交换,把整个数组变成堆。
void siftdown_head(int *pheap,int n)
{
pheap --; //这样作是让第一个元素的索引变成 1, pheap[0]永远不会被访问到int t = pheap[1];
int i =1;
int c = i*2;
while(c<=n)
{
if(c<n && pheap[c]>pheap[c+1])
c++;
if(pheap[c] < t)
{
pheap[i] = pheap[c];
i = c;
c = 2*i;
}
else
break;
}
pheap[i] = t;
}
//如果除了最后一个元素外,其他部分[1..n-1]已经是堆了,
//通过下面的函数,把最后一个元素往前交换,把整个数组调整成堆
void siftup_rear(int *pheap,int n)

pheap --;
int t = pheap[n];
int i;
for(i=n; i>1;)
{
int p = i/2;
if( pheap[p] > t)
pheap[i] = pheap[p];
else
break;
i=p;
}
pheap[i] =t;
}
更多关于堆的概念,可以在网上找到。最常见的应用是使用堆来实现高效率的优先级队列(如 c++的标
准模板库)。在本文应用场景中,也可以把保存 topn 结果的数组看成是优先级队列,因为我们总是选择
数值最小的值,进行出队操作(数值越小,优选级越大)。最后,队列中剩余的,自然就是数值较大的
topn 结果。
6.3.3 算法实现
int * topn3(int *pdata, int m,int *ptop, int n){
ptop[0] = pdata[0];
int i;
for(i=1; i<n;i++)
{
ptop[i] = pdata[i];
siftup_rear(ptop,i+1);
}
for(i=n;i<m;i++)
{
register t = pdata[i];
if(ptop[0] >= t)
continue;
ptop[0] = t;
siftdown_head(ptop,n);
;
}
//如果不需要把结果排序,下面的这段循环可以乎略掉
for(i=n-1; i>=1;i--)
{
int t = ptop[i];
ptop[i] = ptop[0];
ptop[0] = t;
siftdown_head(ptop,i);
}
return ptop;
}
其实,在本文问题定义下,也可以直接使用 c++标准库的模板种的优先级队列,来实现 topn 算法。

 

思路2: http://blog.csdn.net/wwang196988/article/details/6618746
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
​LeetCode刷题实战485:最大连续 1 的个数
算法41(调整数组顺序使奇数位于偶数前面)
一步一步写算法(之合并排序)
最少零钱问题 最少硬币问题
C语言冒泡排序算法及代码
排序算法入门之冒泡排序
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服