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的快排或堆排,由于元素会频繁的移动,效率 ...
随机推荐
- 通过Struts2Web应用框架深入理解MVC
Struts2是一个基于MVC设计模式的Web应用框架,它本质上相当于一个servlet. 一.用法简介: 1.Eclipse新建Dynamic Web Project, 项目名:Struts2Pro ...
- DTD约束与schema约束的不同
本篇笔记了解------Schema约束的语法 ------可以参考W3school之Schema教程. Schema:是基于 XML 的 DTD 替代者,用于描述XML文档结构.支持XML 命名空间 ...
- Python学习笔记(六)Python组合数据类型
在之前我们学会了数字类型,包括整数类型.浮点类型和复数类型,这些类型仅能表示一个数据,这种表示单一数据的类型称为基本数据类型.然而,实际计算中却存在大量同时处理多个数据的情况,这种需要将多个数据有效组 ...
- linux如何将分组权限置为空
两种方法 方法一:使用-符号 chmod g=- monkey.py#可以单独指定一个 方法二:简写方式,用0表示 chmod 740 monkey.py#必须同时指定三个的权限
- centos 安装nginx + 多个tomcat负载均衡
今天在centos上安装了两个tomcat和nginx,进行配置.今天记录的只是最基本的实现测试.(不包含使用redis进行session共享) Nginx 是一款轻量级的Web 服务器/反向代理服务 ...
- LINUX常见服务列表
服务名 必需(是/否)用途描述 注解 acon 否 语言支持 特别支持左手书写语言:阿拉伯语,波斯语和希伯莱语 acpi ...
- CodeVS3958 火车进站
3958 火车进站 时间限制: 1 s 空间限制: 256000 KB 题目等级 : 大师 Master 题目描述 Description 火车站内往往设有一些主干线分叉出去的铁路支路 ...
- CommonJS、requirejs、ES6的对比
文件路径 首先先搞清楚文件路径的写法,这里我总是记不住,有点晕,正好这次整理一下. 以 / 为起始,表示从根目录开始解析: 以 ./ 为起始,表示从当前目录开始解析: 以 ../ 为起始,表示从上级目 ...
- Django项目:CRM(客户关系管理系统)--02--01PerfectCRM基本配置ADMIN02
三.CRM项目表结构设计 from django.db import models # Create your models here. """ #运行 Terminal ...
- 如何解决Firefox浏览器地址栏中文搜索速度很慢
一.插件安装 之前使用Chrome浏览器,习惯在地址栏中直接进行中文搜索.转到Firefox之后,突然发现在地址栏进行中文搜索,访问速度会很慢. 可以使用插件解决这个问题:Omnibar 插件地址:h ...