04Top K算法问题
本章阐述寻找最小的k个数的反面,即寻找最大的k个数,尽管寻找最大的k个树和寻找最小的k个数,本质上是一样的。但这个寻找最大的k个数的问题的实用范围更广,因为它牵扯到了一个Top K算法问题,以及有关搜索引擎,海量数据处理等广泛的问题,所以本文特意对这个Top K算法问题,进行阐述以及实现。
一:寻找最大的k个数
把之前第三章的问题,改几个字,即成为寻找最大的k个数的问题了,如下所述:
题目描述:
输入n个整数,输出其中最大的k个。
例如输入1,2,3,4,5,6,7和8这8个数字,则最大的4个数字为8,7,6和5。
分析:
由于寻找最大的k个数的问题与之前的寻找最小的k个数的问题,本质是一样的,所以,这里就简单阐述下一个思路:
维护k个元素的最小堆,即用容量为k的最小堆存储最先遍历到的k个数,建堆费时O(k),并调整堆(费时O(logk))。继续遍历数列,每次遍历一个元素x,与堆顶元素比较,若x>kmin,则更新堆(用时logk),否则不更新堆。这样下来,总费时O(k*logk+(n- k)*logk)=O(n*logk)。
本文之后的例子主要采用这种思路,剩下的思路不在赘述。
二:搜索引擎热门查询统计
题目描述:
搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。
假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门),请统计最热门的10个查询串,要求使用的内存不能超过1G。
分析:
第一步、先对这批海量数据预处理,在O(N)的时间内用Hash表完成统计;
第二步、借助堆这个数据结构,找出Top K,时间复杂度为N*logK。或者:采用trie树,关键字域存该查询串出现的次数,没有出现为0。最后用10个元素的最小推来对出现频率进行排序。
为了降低实现上的难度,假设这些记录全部是一些英文单词, ok,复杂问题简单化了之后,编写代码实现也相对轻松多了,下面为部分代码:
// 结点指针
typedef struct node_no_space *ptr_no_space; //for hashtable
typedef struct node_has_space *ptr_has_space; //for heap
ptr_no_space head[HASHLEN]; //hash表
struct node_no_space
{
char *word;
int count;
ptr_no_space next;
};
struct node_has_space
{
char word[WORDLEN];
int count;
ptr_has_space next;
};
// 最简单hash函数
int hash_function(char const *p)
{
int value = 0;
while (*p !='/0')
{
value = value* 31 + *p++;
if (value > HASHLEN)
value = value % HASHLEN;
}
return value;
}
// 添加单词到hash表
void append_word(char const *str)
{
int index = hash_function(str);
ptr_no_space p = head[index];
while (p != NULL)
{
if (strcmp(str, p->word) == 0)
{
(p->count)++;
return;
}
p = p->next;
}
// 新建一个结点
ptr_no_space q = new node_no_space;
q->count = 1;
q->word = new char [strlen(str)+1];
strcpy(q->word, str);
q->next = head[index];
head[index] = q;
}
// 将哈希表结果写入文件
void write_to_file()
// 从上往下筛选,维持最小堆性质
void shift_down(node_has_space heap[], int i, int len)
// 建立小根堆
void build_min_heap(node_has_space heap[], int len)
// 去除字符串前后符号
void handle_symbol(char *str, int n)
int main(int argc, char **argv)
{
if(argc != 2)
{
printf("argu error\n");
return -1;
}
//初始化哈希表
char str[WORDLEN];
for (int i = 0; i< HASHLEN; i++)
head[i] = NULL;
// 读取文件,建立哈希表
FILE *fp_passage = fopen(argv[1], "r");
assert(fp_passage);
while (fscanf(fp_passage, "%s", str) != EOF)
{
int n = strlen(str) - 1;
if (n > 0)
handle_symbol(str, n);
append_word(str);
}
fclose(fp_passage);
// 将统计结果输入文件
write_to_file();
int n= 10;
ptr_has_space heap = new node_has_space [n+1];
int c;
FILE *fp_word = fopen("result.txt", "r");
assert(fp_word);
for (int j = 1; j <= n; j++)
{
fscanf(fp_word, "%s%d", &str, &c);
heap[j].count = c;
strcpy(heap[j].word, str);
}
// 建立最小堆
build_min_heap(heap, n);
个单词
while (fscanf(fp_word, "%s %d",&str, &c) != EOF)
{
if (c > heap[1].count)
{
heap[1].count = c;
strcpy(heap[1].word, str);
sift_down(heap, 1, n);
}
}
fclose(fp_word);
// 输出出现频率最大的单词
for (int k = 1; k <= n; k++)
cout << heap[k].count <<" " << heap[k].word << endl;
return 0;
}
三:统计出现次数最多的数据
题目描述:
给你上千万或上亿数据(有重复),统计其中出现次数最多的前N个数据。
分析:
上千万或上亿的数据,现在的机器的内存应该能存下(也许可以,也许不可以)。所以考虑采用hash_map/搜索二叉树/红黑树等来进行统计次数。然后就是取出前N个出现次数最多的数据了。当然,也可以堆实现。
此题与上题类似,最好的方法是用hash_map统计出现的次数,然后再借用堆找出出现次数最多的N个数据。不过,上一题统计搜索引擎最热门的查询已经采用过hash表统计单词出现的次数,特此, 本题改用红黑树取代之前的用hash表,来完成最初的统计,然后用堆更新,找出出现次数最多的前N个数据。下面为部分代码:
typedef enum rb_color{ RED, BLACK } RB_COLOR;
typedef struct rb_node
{
int key;
int data;
RB_COLOR color;
struct rb_node * left;
struct rb_node * right;
struct rb_node * parent;
}RB_NODE;
RB_NODE * RB_CreatNode(int key, int data)
/*左旋*/
RB_NODE * RB_RotateLeft(RB_NODE * node, RB_NODE * root)
/* 右旋 */
RB_NODE * RB_RotateRight(RB_NODE * node, RB_NODE * root)
/*红黑树查找结点*/
RB_NODE *RB_SearchAuxiliary(int key, RB_NODE* root, RB_NODE** save)
/* 返回上述rb_search_auxiliary查找结果 */
RB_NODE *RB_Search(int key, RB_NODE* root)
/* 红黑树的插入*/
RB_NODE *RB_Insert(int key, int data, RB_NODE* root)
typedef struct rb_heap
{
int key; //key表示数值本身
int data; //data表示该数值出现次数
}RB_HEAP;
const int heapSize = 10;
RB_HEAP heap[heapSize+1];
/*MAX_HEAPIFY函数对堆进行更新,使以i为根的子树成最小堆 */
void MIN_HEAPIFY(RB_HEAP* A, const int& size,int i)
/*BUILD_MINHEAP函数对数组A中的数据建立最小堆*/
void BUILD_MINHEAP(RB_HEAP * A, const int & size)
//中序遍历RBTree
void InOrderTraverse(RB_NODE * node)
{
if (node == NULL)
{
return;
}
else
{
InOrderTraverse(node->left);
if(node->data > heap[1].data) //当前节点data大于最小堆的最小元素,更新堆数据
{
heap[1].data = node->data;
heap[1].key= node->key;
MIN_HEAPIFY(heap, heapSize, 1);
}
InOrderTraverse(node->right);
}
}
void RB_Destroy(RB_NODE * node)
int main()
{
RB_NODE * root = NULL;
RB_NODE * node = NULL;
// 初始化最小堆
for (int i = 1; i <= 10; ++i)
{
heap[i].key = i;
heap[i].data = -i;
}
BUILD_MINHEAP(heap, heapSize);
FILE* fp = fopen("data.txt","r");
int num;
while (!feof(fp))
{
int res = -1;
res = fscanf(fp,"%d", &num);
if(res > 0)
{
root = RB_Insert(num, 1, root);
}
else
{
break;
}
}
fclose(fp);
InOrderTraverse(root); //递归遍历红黑树
RB_Destroy(root);
for (i = 1; i <= 10; ++i)
{
printf("%d/t%d/n",heap[i].key, heap[i].data);
}
return 0;
}
由于在遍历红黑树采用的是递归方式比较耗内存,可以采用一个非递归的遍历的程序。
下面是用hash和堆解决此题,很明显比采用上面的红黑树,整个实现简洁了不少,部分源码如下:
#define HASHTABLESIZE 2807303
#define HEAPSIZE 10
#define A 0.6180339887 // (A )
#define M 16384 //m=2^14
typedef struct hash_node
{
int data;
int count;
struct hash_node* next;
}HASH_NODE;
HASH_NODE * hash_table[HASHTABLESIZE];
HASH_NODE * creat_node(int & data)
{
HASH_NODE * node = (HASH_NODE*)malloc(sizeof(HASH_NODE));
if (NULL == node)
{
printf("malloc node failed!/n");
exit(EXIT_FAILURE);
}
node->data = data;
node->count = 1;
node->next = NULL;
return node;
}
/**
* hash函数采用乘法散列法
* h(k)=int(m*(A*k mod 1))
*/
int hash_function(int & key)
{
double result = A * key;
return (int)(M * (result - (int)result));
}
void insert(int & data)
{
int index = hash_function(data);
HASH_NODE * pnode = hash_table[index];
while (NULL != pnode)
{ // 以存在data,则count++
if (pnode->data == data)
{
pnode->count += 1;
return;
}
pnode = pnode->next;
}
// 建立一个新的节点,在表头插入
pnode = creat_node(data);
pnode->next = hash_table[index];
hash_table[index] = pnode;
}
typedef struct min_heap
{
int count;
int data;
}MIN_HEAP;
MIN_HEAP heap[HEAPSIZE + 1];
/**
*traverse_hashtale函数遍历整个hashtable,更新最小堆
*/
void traverse_hashtale()
{
HASH_NODE * p = NULL;
for (int i = 0; i< HASHTABLESIZE; ++i)
{
p = hash_table[i];
while (NULL != p)
{ // 如果当前节点的数量大于最小堆的最小值,则更新堆
if (p->count >heap[1].count)
{
heap[1].count = p->count;
heap[1].data = p->data;
min_heapify(heap, HEAPSIZE, 1);
}
p = p->next;
}
}
}
intmain()
{
// 初始化最小堆
for (int i = 1; i <= 10; ++i)
{
heap[i].count = -i;
heap[i].data = i;
}
build_min_heap(heap, HEAPSIZE);
FILE* fp = fopen("data.txt","r");
int num;
while (!feof(fp))
{
intres = -1;
res =fscanf(fp, "%d", &num);
if(res> 0)
{
insert(num);
}
else
{
break;
}
}
fclose(fp);
traverse_hashtale();
for (i = 1; i <= 10; ++i)
{
printf("%d\t%d\n",heap[i].data, heap[i].count);
}
return 0;
}
四:海量数据处理问题一般总结
关于海量数据处理的问题,一般有Bloom filter,Hashing,bit-map,堆,trie树等方法来处理。更详细的介绍,请查看此文:十道海量数据处理面试题与十个方法大总结。
首先TopK问题,肯定需要有并发的,否则串行搞肯定慢,IO和计算重叠度不高。其次在IO上需要一些技巧,当然可能只是验证算法,在实践中IO的提升会非常明显。最后上文的代码可读性虽好,但机器的感觉可能就会差,这样会影响性能。(比如读文件的函数使用fscanf)
同时,TopK可以看成从地球上选拔k个跑的最快的,参加奥林匹克比赛,各个国家自行选拔,各个大洲选拔,层层选拔,最后找出最快的10个。发挥多机多核的优势。
http://blog.csdn.net/v_JULY_v/article/details/6403777
04Top K算法问题的更多相关文章
- 程序员编程艺术:第三章续、Top K算法问题的实现
程序员编程艺术:第三章续.Top K算法问题的实现 作者:July,zhouzhenren,yansha. 致谢:微软100题实现组,狂想曲创作组. 时间:2011年05月08日 ...
- 强连通分量【k 算法、t 算法】
连通分量就是一个各个顶点能互相达到的图 无向图的连通分量选取任意一个顶点使用DFS遍历即可,遍历完所有顶点所需的DFS的次数就是连通分量的数量 有向图的强连通分量由于是有向的[从A点开始DFS能访问到 ...
- P 算法与 K 算法
P 算法与 K 算法 作者:Grey 原文地址: 博客园:P 算法与 K 算法 CSDN:P 算法与 K 算法 说明 P 算法和 K 算法主要用来解决最小生成树问题,即:不破坏连通性删掉某些边,使得整 ...
- top k 算法
对于一个非有序的数组A[p..r],求数组中第k小的元素. 如何考虑 排序(部分排序)就不用说了..o(nlgn),当然如果在实际情况中要一直取值,当然要排序后,一次搞定,以后都是O(1) 我们这里提 ...
- 使用堆实现Top K 算法 JS 实现
1. 堆算法Top,时间复杂度 O(LogN) function top(arr,comp){ if(arr.length == 0){return ;} var i = arr.length / 2 ...
- Top K算法
应用场景: 搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节. 假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果 ...
- Top K 算法详解
http://xingyunbaijunwei.blog.163.com/blog/static/7653806720111149318357/ 问题描述 百度面试题: ...
- hihoCoder 1133 二分·二分查找之k小数(TOP K算法)
#1133 : 二分·二分查找之k小数 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 在上一回里我们知道Nettle在玩<艦これ>,Nettle的镇守府有很 ...
- 百度面试题——top K算法
需求 从一亿个数据中,找出其中最小的10个数. 分析 最笨的方法就是将这一亿个数据,按从小到大进行排序,然后取前10个.这样的话,即使使用时间复杂度为nlogn的快排或堆排,由于元素会频繁的移动,效率 ...
随机推荐
- 读书笔记--Struts 2 in Action 目录
1.Struts 2:现代Web框架 1.1 web应用程序:快速学习 21.1.1 构建web应用程序 21.1.2 基础技术简介 31.1.3 深入研究 61.2 web应用程序框架 71.2.1 ...
- 在C#应用中使用Common Logging日志接口
我在C#应用中一般使用log4net来记录日志,但如果项目中有个多个工程,那么没有工程都需要引用log4neg,感觉很不爽.不过今日在开spring.net的时候,看到了有个通用日志接口Common ...
- socker TCP UDP BIO NIO
BIO: Java 1.4 以前只有之中方式. bio:阻塞式IO, 一个 socker 连接占用一个 线程.如果 IO 阻塞,会在传输速度限制,这个线程也会一直等待在这里,等待从socker 的 ...
- Oracle存储函数,存储过程
一.Oracle存储函数:存储的PL/SQL语法块,完成特定的功能.1.语法: 函数关键字: function (1)创建函数 CREATE [OR REPLACE] FUNCTION <fun ...
- Dreamweaver CS5更改代码颜色方法代码
XP系统下: C:\Documents and Settings\Administrator\ApplicationData\Adobe\Dreamweaver CS4\zh_CN\Configura ...
- oracle-DML-2
1.update 语句 update table set [column,column......] where column ='' 示例: update customers set ...
- WPF ScrollViewer嵌套Listbox无法滚动
最近在做项目的时候,发现listBoxzi自带的垂直滚动条有问题,经常在Add(item)的时候下面会多出一些空白的部分,而且滚动条的长度也是无规则的,一会大一会小,而且无法控制横竖滚动条的显隐藏,并 ...
- 【机器学习PAI实战】—— 玩转人工智能之利用GAN自动生成二次元头像
前言 深度学习作为人工智能的重要手段,迎来了爆发,在NLP.CV.物联网.无人机等多个领域都发挥了非常重要的作用.最近几年,各种深度学习算法层出不穷, Generative Adverarial Ne ...
- Codeforces 442C
题目链接 C. Artem and Array time limit per test 2 seconds memory limit per test 256 megabytes input stan ...
- ES6 中字符串的扩展
1. 字符的Unicode表示法 JavaScript允许采用 \uxxxx 形式表示一个字符,其中 xxxx 表示字符的 Unicode 码点. "\u0061" // 表示小写 ...