1. 程式人生 > >演算法學習(二)Top K 演算法問題

演算法學習(二)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的建立是通過陣列加連結串列,也就是“連結串列的陣列”,使用拉鍊法。
這裡寫圖片描述
左邊為陣列,陣列的成員為一個指標,指向連結串列的開頭,或者為空。不同的值可能對映到相同的陣列下標下。

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+堆,而且處理整數比處理字串要舒服的多。