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 ");

                  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 %d ",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

原文地址:https://www.cnblogs.com/gqtcgq/p/7247192.html