算法学习(二)Top K 算法问题

参考学习结构之法,算法之道
上次谈论了寻找最小的k个数问题,如果反过来就是寻找最大的k个问题了。

Top K

题目描述:输入n个整数,输出其中最大的k个数
例如输入1,2,3,4,5,6,7这个7个数,最大的三个数为5,6,7.
这和寻找最小的k个数问题本质上差不多。这也引出了对于Top K算法的讨论。
题目描述:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门),请你统计最热门的10个查询串,要求使用的内存不能超过1G。
解析:
首先要统计每个检索串的次数,然后根据统计结果,找到TopK。一千万个记录,每条是255字节,需要占用内存2.39G,题目有内存限制,直接放在数组中是没戏了。
第一步:检索串的统计
1,直接排序法
由于内存的限制,不能再内存中完成排序,可以使用外排序。
外排序:指大文件排序,待排序的记录存储在外存储器上,待排序的文件无法一次装入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序整个 文件的目的。外部排序最常用的算法是多路归并排序,时间复杂度为O(NlogN)。
排序后再对有序文件进行遍历O(N),统计每个检索串出现的次数,再次写入文件中。
2,Hash Table
由于检索串的重复度比较高,事实上只有300万,可以放入内存。hash table的查询速度快。
key为字符串,value为串出现的次数,每次读取一个检索串,如果不在table中,加入且value置一,如果已经存在,value加一。最终在O(N)时间复杂度完成处理。
第二步:找出Top 10
一:普通排序
直接在内存中排序,时间复杂度为O(NlogN)
二:部分排序
这点和前一节的方法二有点类似,维护一个10个大小的数组,对这数组从大到小排序,然后遍历300万条记录,每读一条,与数组中的最小值比较,如果比它小就丢弃,如果比它大,就替换数组最小值,然后再对数组排序。时间复杂度为O(N* K)
三:使用堆来部分排序
维护一个10个大小的最小堆,在对堆操作的时候复杂度为logK,可以将复杂度降为N* logK
总的时间复杂度为:O(N) + O(N* logK)。
代码实现:

/************************************************************************* > File Name: hash_ktop.cpp > Author: zxl > mail: [email protected] > Created Time: 2016年04月12日 星期二 15时42分31秒 ************************************************************************/

#include <iostream>
#include <string.h> //包含strcmp strcpy
#include <stdio.h> //fopen
#include <assert.h>
#define HASHLEN 2807303 //哈希表的长度
#define WORDLEN 30
using namespace std;
typedef struct str_no_space * ptr_no_space;  //结构体指针
typedef struct str_has_space * ptr_has_space;
ptr_no_space head[HASHLEN];
struct str_no_space   //链表项构造hashtable
{
    char * word;
    int count;
    ptr_no_space next;
};
struct str_has_space    //构造K的最小堆
{
    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;
}
//向hashtable中添加单词
void append_word( const char *str)
{
    int index = hash_function(str);    //通过hash函数将内容映射到存放地址
    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 str_no_space;
    q->count = 1;
    q->word = new char [strlen(str)+1];
    strcpy(q->word,str);
    q->next = head[index];     //新结点的next设置为原来的链表头
    head[index] = q;          //将新建的结点成为链表头

}
//将统计的数据写入到文件中
void write_to_file()
{
    FILE *fp = fopen("result.txt","w");
    assert(fp);        //断言fp不为Null
    int i = 0;
    while(i < HASHLEN)
    {
        for(ptr_no_space p = head[i];p!=NULL;p = p->next)
            fprintf(fp,"%s %d\n",p->word,p->count);
        i++;
    }
    fclose(fp);
}
//维护最小堆
void Min_heapify(str_has_space heap[],int i,int len)
{
    int min_index;
    int left = 2*i;
    int right = 2*i+1;
    if(left <= len && heap[left].count < heap[i].count)
        min_index = left;
    else
        min_index = i;
    if(right <= len && heap[right].count < heap[min_index].count)
        min_index = right;
    if(min_index != i)
    {
        swap(heap[i].count,heap[min_index].count);
        char buffer[WORDLEN];
        strcpy(buffer,heap[i].word);
        strcpy(heap[i].word,heap[min_index].word);
        strcpy(heap[min_index].word,buffer);
        Min_heapify(heap,min_index,len);
    }
}
//建立最小堆
void build_min_heap(str_has_space heap[],int len)
{
    if(heap == NULL)
        return;
    int index = len/2;
    int i;
    for(i = index;i>=1;i--)
        Min_heapify(heap,i,len);
}
//去除字符首尾的符号标点
void handle_symbol(char * str,int n)
{
    while(str[n] < '0' || (str[n] > '9' && str[n] < 'A') || (str[n] > 'Z' && str[n] < 'a') || str[n] >'z' )
    {
        str[n] = '\0';
        n--;
    }
    while(str[n] < '0' || (str[n] > '9' && str[n] < 'A') || (str[n] > 'Z' && str[n] < 'a') || str[n] >'z' )
    {
        int i= 0;
        while(i<n)
        {
            str[i] = str[i+1]; //所有字符左移一位
            i++;
        }
        str[i] = '\0';
        n--;
    }

}

int main()
{
    char str[WORDLEN];
    int i;
    for(i = 0;i<HASHLEN;i++)
        head[i] = NULL;
    FILE *fp_passage = fopen("string.txt","r");
    assert(fp_passage);
    while(fscanf(fp_passage,"%s",str) != EOF)   //读取源文件,将字符串读入str
    {
        int n= strlen(str)-1;
        if(n > 0)
            handle_symbol(str,n);
        append_word(str);         //将str添加到hashtable
    }
    fclose(fp_passage);
    write_to_file();
    int n= 5;
    ptr_has_space min_heap = new str_has_space[n+1];
    int c;
    FILE *fp_word = fopen("result.txt","r");
    assert(fp_word);
    int j;
    for(j = 1;j<=n;j++)            //从hashtable中取出k个建立最小堆
    {
        fscanf(fp_word,"%s %d",str,&c);
        min_heap[j].count = c;
        strcpy(min_heap[j].word,str);
    }
    build_min_heap(min_heap,n);
    while(fscanf(fp_word,"%s %d",str,&c) != EOF)  //从剩余的N-K中依次取出一个字符串与堆顶元素比较,如果比它大,就与堆顶元素交换,然后更新最小堆
    {
        if(c > min_heap[1].count)  
        {
            min_heap[1].count = c;
            strcpy(min_heap[1].word,str);
            Min_heapify(min_heap,1,n);
        }
    }
    fclose(fp_word);
    int k;
    for( k = 1;k<=n;k++)
        cout << min_heap[k].word << " " << min_heap[k].count << endl;
    return 0;

}

代码中hashtable的建立是通过数组加链表,也就是“链表的数组”,使用拉链法。
算法学习(二)Top K 算法问题_第1张图片
左边为数组,数组的成员为一个指针,指向链表的开头,或者为空。不同的值可能映射到相同的数组下标下。

hashtable简介

Hash,叫做”散列”,或者“哈希”,把任意长度的输入,通过散列算法,变换成固定长度的输出,该输出就是散列值。散列值一般就作为数据存放地址的依据,实现从内容到地址的映射关系。
数组的特点:寻址方便,插入和删除困难
链表的特点:寻址困难,插入和删除方便
哈希表:两者有点的结合。
重要的是散列算法的选取:
常用的有三种
1.除法散列法
index = value % 16
上图使用的就是这种
2,平方散列法
求index是非常频繁的操作,乘法的运算要比除法来的省‘
index = (value * value) >> 28
关键在意的是数值分配是否均匀
3,斐波那契散列法
找到一个理想的乘数,而不是拿value本身当作乘数。
1,对于16位整数,乘数为40503
2,对于32位整数,乘数为2654435769
3,对于64位整数而言,乘数为1140071481932198485
例如对于32位整数而言,
index = (value * 2654435769)

适用范围:快速查找,删除的基本数据结构,通常需要总数据量可以放入内存。

hashtable和hashmap的区别

HashMap 是Hashtable的轻量级实现,他们都完成了Map接口,
主要区别是HashMap允许空键值,由于是非线程安全的,效率较高。
两者的比较

统计出现次数最多的数据

题目描述:给你上亿的数据,统计其中出现次数最多的前N个数据
分析:
和上面的思路一样,hash+堆,而且处理整数比处理字符串要舒服的多。

你可能感兴趣的:(算法,Hashtable)