TOP-N 算法 论文相关

top-k算法的二分实现(修正版)(C++实现)

摘要:本文简要介绍了top-k(求一个序列中前K个最大或最小的元素)算法的二分实现方法,并给出了C++源

代码

关键字:top-k,二分,快排序

         网上介绍top-k算法的博文貌似不多,有一个搜索引擎中排名靠前的top-k算法介绍中给出了源码,

我试了试,发现有点小BUG,就自己整理了一下,先说说实现原理,后面给出源码。

         top-k的实现方法比较常见的有堆和二分法,这里只介绍二分实现方法,具体思路如下:

以求长度为N的整数数组中前K个最大数为例,类似于快排序,本方法递归地把数组分为两部分

(设划分点索引为M),将大的数放到左边部分,将小的数放到右边部分。如果M大于K

对数组0~(M-1)部分执行top-k算法;如果M小于等于K,计上一次划分点索引为LAST, 对数组0~LAST

进行快排序。运行结束后,数组的0~(K-1)即为最大的K个整数!算法的时间复杂度为O(N*log2K),适合

数据可以全部装入内存的情形!

        源码如下(VC++ 6.0编译测试通过):
/*
 * 二分实现top-k算法
 * 取长度为N的整数数组中前K个最大的数
 */

#include <iostream>
#include <ctime>
using namespace std;

#define MAX_NUM 1000

#define N 200

#define K 20

/* global data */

int array[N];

/* funcitons declaration*/
void init_array(int n);
void print_result(int n);
void quicksort(int low, int high);
void topk(int low, int high, int k);

/* main */

int main(int argc, char* *argv)
{
        cout<<"init array,size:"<<N<<"......"<<endl;
        init_array(N);
        print_result(N);
        cout<<"generate top-"<<K<<"......"<<endl;
        topk(0, N - 1, K);
        cout<<"result....."<<endl;
        print_result(K);
        system("pause");
        return 0;
}

/* function definition */

void init_array(int n)
{
        for(int i = 0; i < n; ++i)
        {
                array[i] = rand() % MAX_NUM;
        }
}

void print_result(int n)
{
        for(int i = 0; i < n; ++i)
        {
                cout<<array[i]<<" ";
        }
        cout<<endl;
}
        
void mswap(int i, int j)
{
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
}
void quicksort(int low, int high)
{
        if(low >= high) return;
        mswap(low, rand() % (high - low + 1) + low);
        int m = low;
        for(int i = low + 1; i <= high; ++i)
        {
                if(array[i] > array[low])
                {
                        mswap(++m, i);
                }
        }
        mswap(m, low);
        quicksort(low, m - 1);
        quicksort(m + 1, high);
}

void topk(int low, int high, int k)
{
        static int last = high;
        if(low >= high) return;
        mswap(low, rand() % (high - low + 1) + low);
        int m = low;
        for(int i = low + 1; i <= high; ++i)
        {
                if(array[i] > array[low])
                {
                        mswap(++m, i);
                }
        }
        mswap(m, low);
        if(k < m)
        {
                last = m;
                topk(low, m - 1, k);
        }
        else
        {
                quicksort(low, last);
        }
}

参考文章:http://www.penglixun.com/study/it/program/dichotomy_topk_cpp.html



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




二分法实现TopK算法的方法


在Jacky一篇关于Oracle排序算法的文章中,讨论了下Oracle的Short sort算法。文章中对此算法有详细描述,这里不赘述,大致就是通过Heap来实现。
虽然Heap在处理优先队列类型的问题上很有优势,但是我一致觉得它不太适合做排序,调堆的代价其实是比较高的,每加入一个元素删除一个元素都要调堆。
对于TopK的问题,我还是觉得二分法实现比较好。首先按快排的算法把数据分成两堆,左大右小,再判断左边的大堆是不是数量小于了K,小于了了就使用上次的右边界进入排序流程,不到则继续二分。
程序如下:



#include <iostream>
#include <cstdlib>
#include <time.h>
#include <math.h>
#define MAXN 10000
#define LIMIT 500
 
int a[MAXN];
int last;
int count_sw, count_cmp;
 
//初始化测试数据
void init () {
	srand(time(0));
	//参与查询的数据
	for(int i=0; i<MAXN; ++i) {
		a[i] =  rand()%MAXN;
	}
	return ;
}
void swap(int &x, int &y) {
	int t ;
	t = x;
	x = y;
	y = t;
	return ;
}
 
void qsort(int arr[], int start, int end) {
	if(start < end) {
		int mid = arr[rand()%(end-start) + start];
		int i = start - 1;
		int j = end + 1;
		while (true) {
			while (arr[++i] > mid) count_cmp++;
			while (arr[--j] < mid) count_cmp++;
			if(i>=j) break;
			swap(arr[i], arr[j]);
			count_sw++;
		}
		qsort (arr, start, i-1);
		qsort (arr, j+1, end);
	}
	return ;
}
 
void topK(int arr[], int start, int end, int k) {
	if(start < end) {
		int mid = arr[rand()%(end-start) + start];
		int i = start - 1;
		int j = end + 1;
		while (true) {
			while (arr[++i] > mid) count_cmp++;
			while (arr[--j] < mid) count_cmp++;
			if(i>=j) break;
			swap(arr[i], arr[j]);
			count_sw++;
		}
		if(i-start > k) {
			last = i-1;
			topK (arr, start, i-1, k);
		} else{
			qsort(arr, start, last);
		}
	}
	return ;
}
 
int main() {
	init();
	count_sw = 0;
	count_cmp = 0;
	topK(c, 0, MAXN-1,10);
	cout << "Result:" << MAXN << endl;
	for(int i=0; i<10; ++i) {
		cout << c[i] << endl;
	}
	cout << "Swaps:" << count_sw << endl;
	cout << "Campare:" << count_cmp << endl;
	return 0;
}


测试结果比Heap要好一些。

标签: c++, TopK, 算法


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


Top K算法详细解析---百度面试


问题描述:

这是在网上找到的一道百度的面试题:

搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复度比较 高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。请你统计最热门的10个查询 串,要求使用的内存不能超过1G。

问题解析:

【分析】:要统计最热门查询,首先就是要统计每个Query出现的次数,然后根据统计结果,找出Top 10。所以我们可以基于这个思路分两步来设计该算法。下面分别给出这两步的算法:


第一步:Query统计

算法一:直接排序法

首先我们能想到的算法就是排序了,首先对这个日志里面的所有Query都进行排序,然后再遍历排好序的Query,统计每个Query出现的次数了。但 是题目中有明确要求,那就是内存不能超过1G,一千万条记录,每条记录是225Byte,很显然要占据2.55G内存,这个条件就不满足要求了。

让我们回忆一下数据结构课程上的内容,当数据量比较大而且内存无法装下的时候,我们可以采用外排序的方法来进行排序,这里笔者采用归并排序,是因为归并排序有一个比较好的时间复杂度O(NlgN)。

排完序之后我们再对已经有序的Query文件进行遍历,统计每个Query出现的次数,再次写入文件中。

综合分析一下,排序的时间复杂度是O(NlgN),而遍历的时间复杂度是O(N),因此该算法的总体时间复杂度就是O(NlgN)。

算法二:Hash Table法

在上个方法中,我们采用了排序的办法来统计每个Query出现的次数,时间复杂度是NlgN,那么能不能有更好的方法来存储,而时间复杂度更低呢?
题目中说明了,虽然有一千万个Query,但是由于重复度比较高,因此事实上只有300万的Query,每个Query255Byte,因此我们可以考虑 把他们都放进内存中去,而现在只是需要一个合适的数据结构,在这里,Hash Table绝对是我们优先的选择,因为Hash Table的查询速度非常的快,几乎是O(1)的时间复杂度。
那么,我们的算法就有了:维护一个Key为Query字串,Value为该Query出现次数的HashTable,每次读取一个Query,如果该字串 不在Table中,那么加入该字串,并且将Value值设为1;如果该字串在Table中,那么将该字串的计数加一即可。最终我们在O(N)的时间复杂度 内完成了对该海量数据的处理。
本方法相比算法一:在时间复杂度上提高了一个数量级,但不仅仅是时间复杂度上的优化,该方法只需要IO数据文件一次,而算法一的IO次数较多的,因此该算法比算法一在工程上有更好的可操作性。



第二步:找出Top 10

算法一:排序

我想对于排序算法大家都已经不陌生了,这里不在赘述,我们要注意的是排序算法的时间复杂度是NlgN,在本题目中,三百万条记录,用1G内存是可以存下的。

算法二:部分排序

题目要求是求出Top 10,因此我们没有必要对所有的Query都进行排序,我们只需要维护一个10个大小的数组,初始化放入10Query,按照每个Query的统计次数由 大到小排序,然后遍历这300万条记录,每读一条记录就和数组最后一个Query对比,如果小于这个Query,那么继续遍历,否则,将数组中最后一条数 据淘汰,加入当前的Query。最后当所有的数据都遍历完毕之后,那么这个数组中的10个Query便是我们要找的Top10了。
不难分析出,这样的算法的时间复杂度是N*K, 其中K是指top多少。

算法三:堆

在算法二中,我们已经将时间复杂度由NlogN优化到NK,不得不说这是一个比较大的改进了,可是有没有更好的办法呢?
分析一下,在算法二中,每次比较完成之后,需要的操作复杂度都是K,因为要把元素插入到一个线性表之中,而且采用的是顺序比较。这里我们注意一下,该数组 是有序的,一次我们每次查找的时候可以采用二分的方法查找,这样操作的复杂度就降到了logK,可是,随之而来的问题就是数据移动,因为移动数据次数增多 了。不过,这个算法还是比算法二有了改进。
基于以上的分析,我们想想,有没有一种既能快速查找,又能快速移动元素的数据结构呢?回答是肯定的,那就是堆。
借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此到这里,我们的算法可以改进为这样,维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别和根元素进行对比。。。
那么这样,这个算法发时间复杂度就降到了NlogK,和算法而相比,又有了比较大的改进。

结语:

至此,我们的算法就完全结束了,经过步骤一和步骤二的最优结合,我们最终的时间复杂度是O(N) + O(N')logK。如果各位有什么好的算法,欢迎跟帖讨论。


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


TOP k算法的简单实现


顾名思义,TOP k就是从海量的数据中选取最大的k个元素或记录。基本思想就是维护一个具有k个元素的小顶堆。每当有新的元素加入时,判断它是否大于堆顶元素,如果大于,用该元素代替堆顶元素,并重新维护小顶堆,直到所有元素被处理完毕。时间复杂度为O(N*logk),基本达到线性复杂度。部分代码如下:

//打印数组元素
void print(int data[], int length)
{
    for(int i = 1; i <= length; ++i)
        cout << data[i] << " ";
    cout << endl;
}

//维护小顶堆
void modifySmallHeap(int data[], int location, int length)
{
    int lchild = 2 * location;
    int rchild = 2 * location + 1;
    int smallest;
    if(lchild <= length && data[lchild] < data[location])smallest = lchild;
    else smallest = location;
    if(rchild <= length && data[rchild] < data[smallest])smallest = rchild;

    if(smallest != location)
    {
        swap(data[location], data[smallest]);
        modifySmallHeap(data, smallest, length);
    }
}

//建立小顶堆
void buildSmallHeap(int data[], int length)
{
    for (int i = length / 2; i > 0; --i)
    {
        modifySmallHeap(data, i, length);
    }
}

//top k算法的简单实现
void HeapSortK(int data[], int length, int topk)
{
    buildSmallHeap(data, topk);
    for (int i = topk + 1; i <= length; ++i)
    {
        if(data[i] <= data[1])continue;
        else
        {
            swap(data[1], data[i]);
            modifySmallHeap(data, 1, topk);
        }
    }
}
       为了便于测试,可以利用随机数函数构造测试用例。鉴于一切从简,我就不多说了。


PS:顺便贴出一个小顶堆和大顶堆的图片

TOP-N 算法 论文相关

你可能感兴趣的:(top)