參考學習結構之法,算法之道
上次談論了尋找最小的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+堆,而且處理整數比處理字符串要舒服的多。