本章阐述寻找最小的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);
// 查找出现频率最大的10个单词
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