所謂海量數據處理,其實很簡單,海量,海量,何謂海量,就是數據量太大,因此致使要麼是沒法在較短期內迅速解決,要麼是數據太大,致使沒法一次性裝入內存。html
那解決辦法呢?針對時間,咱們能夠採用巧妙的算法搭配合適的數據結構,如Bloom filter/Hash/bit-map/堆/數據庫或倒排索引/trie/,針對空間,無非就一個辦法:大而化小:分而治之/hash映射,你不是說規模太大嘛,那簡單啊,就把規模大化爲規模小的,各個擊破不就完了嘛。mysql
至於所謂的單機及集羣問題,通俗點來說,單機就是處理裝載數據的機器有限(只要考慮cpu,內存,硬盤的數據交互),而集羣,機器有多輛,適合分佈式處理,並行計算(更多考慮節點和節點間的數據交互)。ios
再者,經過本blog內的有關海量數據處理的文章,咱們已經大體知道,處理海量數據問題,無非就是:面試
下面,本文第一部分、從set/map談到hashtable/hash_map/hash_set,簡要介紹下set/map/multiset/multimap,及hash_set/hash_map/hash_multiset/hash_multimap之區別(萬丈高樓平地起,基礎最重要),而本文第二部分,則針對上述那6種方法模式結合對應的海量數據處理面試題分別具體闡述算法
稍後本文第二部分中將屢次提到hash_map/hash_set,下面稍稍介紹下這些容器,以做爲基礎準備。通常來講,STL容器分兩種,
sql
所謂關聯式容器,相似關聯式數據庫,每筆數據或每一個元素都有一個鍵值(key)和一個實值(value),即所謂的Key-Value(鍵-值對)。當元素被插入到關聯式容器中時,容器內部結構(RB-tree/hashtable)便依照其鍵值大小,以某種特定規則將這個元素放置於適當位置。數據庫
包括在非關聯式數據庫中,好比,在MongoDB內,文檔(document)是最基本的數據組織形式,每一個文檔也是以Key-Value(鍵-值對)的方式組織起來。一個文檔能夠有多個Key-Value組合,每一個Value能夠是不一樣的類型,好比String、Integer、List等等。
{ "name" : "July",
"sex" : "male",
"age" : 23 } 編程
set/map/multiset/multimapwindows
set,同map同樣,全部元素都會根據元素的鍵值自動被排序,由於set/map二者的全部各類操做,都只是轉而調用RB-tree的操做行爲,不過,值得注意的是,二者都不容許兩個元素有相同的鍵值。
不一樣的是:set的元素不像map那樣能夠同時擁有實值(value)和鍵值(key),set元素的鍵值就是實值,實值就是鍵值,而map的全部元素都是pair,同時擁有實值(value)和鍵值(key),pair的第一個元素被視爲鍵值,第二個元素被視爲實值。
至於multiset/multimap,他們的特性及用法和set/map徹底相同,惟一的差異就在於它們容許鍵值重複,即全部的插入操做基於RB-tree的insert_equal()而非insert_unique()。數組
hash_set/hash_map/hash_multiset/hash_multimap
hash_set/hash_map,二者的一切操做都是基於hashtable之上。不一樣的是,hash_set同set同樣,同時擁有實值和鍵值,且實質就是鍵值,鍵值就是實值,而hash_map同map同樣,每個元素同時擁有一個實值(value)和一個鍵值(key),因此其使用方式,和上面的map基本相同。但因爲hash_set/hash_map都是基於hashtable之上,因此不具有自動排序功能。爲何?由於hashtable沒有自動排序功能。
至於hash_multiset/hash_multimap的特性與上面的multiset/multimap徹底相同,惟一的差異就是它們hash_multiset/hash_multimap的底層實現機制是hashtable(而multiset/multimap,上面說了,底層實現機制是RB-tree),因此它們的元素都不會被自動排序,不過也都容許鍵值重複。
因此,綜上,說白了,什麼樣的結構決定其什麼樣的性質,由於set/map/multiset/multimap都是基於RB-tree之上,因此有自動排序功能,而hash_set/hash_map/hash_multiset/hash_multimap都是基於hashtable之上,因此不含有自動排序功能,至於加個前綴multi_無非就是容許鍵值重複而已。
此外,
OK,接下來,請看本文第二部分、處理海量數據問題之六把密匙。
一、海量日誌數據,提取出某日訪問百度次數最多的那個IP。
首先是這一天,而且是訪問百度的日誌中的IP取出來,逐個寫入到一個大文件中。注意到IP是32位的,最多有個2^32個IP。一樣能夠採用映射的方法,好比模1000,把整個大文件映射爲1000個小文件,再找出每一個小文中出現頻率最大的IP(能夠採用hash_map進行頻率統計,而後再找出頻率最大的幾個)及相應的頻率。而後再在這1000個最大的IP中,找出那個頻率最大的IP,即爲所求。
或者以下闡述(雪域之鷹):
算法思想:分而治之+Hash
1.IP地址最多有2^32=4G種取值狀況,因此不能徹底加載到內存中處理;
2.能夠考慮採用「分而治之」的思想,按照IP地址的Hash(IP)%1024值,把海量IP日誌分別存儲到1024個小文件中。這樣,每一個小文件最多包含4MB個IP地址;
3.對於每個小文件,能夠構建一個IP爲key,出現次數爲value的Hash_map,同時記錄當前出現次數最多的那個IP地址;
4.能夠獲得1024個小文件中的出現次數最多的IP,再依據常規的排序算法獲得整體上出現次數最多的IP;
分析:有的網友提出如下疑問:我感受這個值不該該是要求的那個。由於可能某一個ip在某一個小文件中可能出現頻率很高,可是在其餘小文件中可能沒出現幾回,即分佈不均,但由於某一個小文件中特別多而被選出來了;而另外一個ip可能在每一個小文件中都不是出現最多的,可是它在每一個文件中都出現不少次,即分佈均勻,所以很是有可能它就是總的出現次數最多的,可是由於在每一個小文件中出現的次數都不是最多的而被刷掉了。因此我感受上面的方案不行。
這就考慮到「分而治之」中的「分」到底怎麼分。。在第二步中咱們提到按照IP地址的Hash(IP)%1024的值,將海量IP日誌分別存儲到1024個小文件中。。這樣就會導致類似的IP或者同一IP被分到同一小文件中。。知足分而治之的要求。。故不存在分佈均勻狀況。。
還有一位網友給出了具體的方法:計數法(原理同上:分而治之)
假設一天以內某個IP訪問百度的次數不超過40億次,則訪問次數能夠用unsigned表示.用數組統計出每一個IP地址出現的次數, 便可獲得訪問次數最大的IP地址。
IP地址是32位的二進制數,因此共有N=2^32=4G個不一樣的IP地址, 建立一個unsigned count[N];的數組,便可統計出每一個IP的訪問次數,而sizeof(count) == 4G*4=16G, 遠遠超過了32位計算機所支持的內存大小,所以不能直接建立這個數組.下面採用劃分法解決這個問題.
假設容許使用的內存是512M, 512M/4=128M 即512M內存能夠統計128M個不一樣的IP地址的訪問次數.而N/128M =4G/128M = 32 ,因此只要把IP地址劃分紅32個不一樣的區間,分別統計出每一個區間中訪問次數最大的IP, 而後就能夠計算出全部IP地址中訪問次數最大的IP了.
由於2^5=32, 因此能夠把IP地址的最高5位做爲區間編號, 剩下的27爲做爲區間內的值,創建32個臨時文件,表明32個區間,把相同區間的IP地址保存到同一的臨時文件中.
例如:
ip1=0x1f4e2342
ip1的高5位是id1 = ip1 >>27 = 0x11 = 3
ip1的其他27位是value1 = ip1 &0x07ffffff = 0x074e2342
因此把 value1 保存在tmp3文件中。
由id1和value1能夠還原成ip1, 即 ip1 =(id1<<27)|value1
按照上面的方法能夠獲得32個臨時文件,每一個臨時文件中的IP地址的取值範圍屬於[0-128M),所以能夠統計出每一個IP地址的訪問次數.從而找到訪問次數最大的IP地址
程序源碼:
#include <fstream> #include <iostream> #include <ctime> using namespace std; #define N 32 //臨時文件數 #define ID(x) (x>>27) //x對應的文件編號 #define VALUE(x) (x&0x07ffffff) //x在文件中保存的值 #define MAKE_IP(x,y) ((x<<27)|y) //由文件編號和值獲得IP地址. #define MEM_SIZE 128*1024*1024 //需分配內存的大小爲 MEM_SIZE*sizeof(unsigned) char* data_path="D:/test/ip.dat"; //ip數據 //產生n個隨機IP地址 void make_data(const int& n) { ofstream out(data_path,ios::out|ios::binary); srand((unsigned)(time(NULL))); if (out) { for (int i=0; i<n; ++i) { unsigned val=unsigned(rand()); val = (val<<24)|val; //產生unsigned類型的隨機數 out.write((char *)&val,sizeof (unsigned)); } } } //找到訪問次數最大的ip地址 int main() { //make_data(100); // make_data(100000000); //產生測試用的IP數據 fstream arr[N]; for (int i=0; i<N; ++i) //建立N個臨時文件 { char tmp_path[128]; //臨時文件路徑 sprintf(tmp_path,"D:/test/tmp%d.dat",i); arr[i].open(tmp_path, ios::trunc|ios::in|ios::out|ios::binary); //打開第i個文件 if( !arr[i]) { cout<<"open file"<<i<<"error"<<endl; } } ifstream infile(data_path,ios::in|ios::binary); //讀入測試用的IP數據 unsigned data; while(infile.read((char*)(&data), sizeof(data))) { unsigned val=VALUE(data); int key=ID(data); arr[ID(data)].write((char*)(&val), sizeof(val)); //保存到臨時文件件中 } for(unsigned i=0; i<N; ++i) { arr[i].seekg(0); } unsigned max_ip = 0; //出現次數最多的ip地址 unsigned max_times = 0; //最大隻出現的次數 //分配512M內存,用於統計每一個數出現的次數 unsigned *count = new unsigned[MEM_SIZE]; for (unsigned i=0; i<N; ++i) { memset(count, 0, sizeof(unsigned)*MEM_SIZE); //統計每一個臨時文件件中不一樣數字出現的次數 unsigned data; while(arr[i].read((char*)(&data), sizeof(unsigned))) { ++count[data]; } //找出出現次數最多的IP地址 for(unsigned j=0; j<MEM_SIZE; ++j) { if(max_times<count[j]) { max_times = count[j]; max_ip = MAKE_IP(i,j); // 恢復成原ip地址. } } } delete[] count; unsigned char *result=(unsigned char *)(&max_ip); printf("出現次數最多的IP爲:%d.%d.%d.%d,共出現%d次", result[0], result[1], result[2], result[3], max_times); }
運行結果:
二、尋找熱門查詢
搜索引擎會經過日誌文件把用戶每次檢索使用的全部檢索串都記錄下來,每一個查詢串的長度爲1-255字節。假設目前有一千萬個記錄,這些查詢串的重複讀比較 高,雖然總數是1千萬,可是若是去除重複和,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的用戶越多,也就越熱門。請你統計最熱門的10個查詢 串,要求使用的內存不能超過1G。
(1) 請描述你解決這個問題的思路;
(2) 請給出主要的處理流程,算法,以及算法的複雜度。
方案一:
分析:此問題的解決分爲如下倆個步驟:
第一步:Query統計
Query統計有如下倆個方法,可供選擇:
1)、直接排序法
首先咱們最早想到的的算法就是排序了,首先對這個日誌裏面的全部Query都進行排序,而後再遍歷排好序的Query,統計每一個Query出現的次數了。
可是題目中有明確要求,那就是內存不能超過1G,一千萬條記錄,每條記錄是255Byte,很顯然要佔據2.375G內存,這個條件就不知足要求了。
讓咱們回憶一下數據結構課程上的內容,當數據量比較大並且內存沒法裝下的時候,咱們能夠採用外排序的方法來進行排序,這裏咱們能夠採用歸併排序,由於歸併排序有一個比較好的時間複雜度O(NlgN)。
排完序以後咱們再對已經有序的Query文件進行遍歷,統計每一個Query出現的次數,再次寫入文件中。
綜合分析一下,排序的時間複雜度是O(NlgN),而遍歷的時間複雜度是O(N),所以該算法的整體時間複雜度就是O(N+NlgN)=O(NlgN)。
2)、Hash Table法
在第1個方法中,咱們採用了排序的辦法來統計每一個Query出現的次數,時間複雜度是NlgN,那麼能不能有更好的方法來存儲,而時間複雜度更低呢?
題目中說明了,雖然有一千萬個Query,可是因爲重複度比較高,所以事實上只有300萬的Query,每一個Query255Byte,所以咱們能夠考慮把他們都放進內存中去,而如今只是須要一個合適的數據結構,在這裏,Hash Table絕對是咱們優先的選擇,由於Hash Table的查詢速度很是的快,幾乎是O(1)的時間複雜度。
那麼,咱們的算法就有了:維護一個Key爲Query字串,Value爲該Query出現次數的HashTable,每次讀取一個Query,若是該字串不在Table中,那麼加入該字串,而且將Value值設爲1;若是該字串在Table中,那麼將該字串的計數加一便可。最終咱們在O(N)的時間複雜度內完成了對該海量數據的處理。
本方法相比算法1:在時間複雜度上提升了一個數量級,爲O(N),但不只僅是時間複雜度上的優化,該方法只須要IO數據文件一次,而算法1的IO次數較多的,所以該算法2比算法1在工程上有更好的可操做性。
第二步:找出Top 10
算法一:普通排序
我想對於排序算法你們都已經不陌生了,這裏不在贅述,咱們要注意的是排序算法的時間複雜度是NlgN,在本題目中,三百萬條記錄,用1G內存是能夠存下的。
算法二:部分排序
題目要求是求出Top 10,所以咱們沒有必要對全部的Query都進行排序,咱們只須要維護一個10個大小的數組,初始化放入10個Query,按照每一個Query的統計次數由大到小排序,而後遍歷這300萬條記錄,每讀一條記錄就和數組最小一個Query對比,若是小於這個Query,那麼繼續遍歷,不然,將數組中最後一條數據淘汰,加入當前的Query(並尋找最小元素)。最後當全部的數據都遍歷完畢以後,那麼這個數組中的10個Query即是咱們要找的Top10了。
不難分析出,這樣,算法的最壞時間複雜度是N*K, 其中K是指top多少。
算法三:堆
在算法二中,咱們已經將時間複雜度由NlogN優化到NK,不得不說這是一個比較大的改進了,但是有沒有更好的辦法呢?
分析一下,在算法二中,每次比較完成以後,須要的操做複雜度都是K,由於要把元素插入到一個線性表之中,並且採用的是順序比較。這裏咱們注意一下,該數組是有序的,一次咱們每次查找的時候能夠採用二分的方法查找,這樣操做的複雜度就降到了logK,但是,隨之而來的問題就是數據移動,由於移動數據次數增多了。不過,這個算法仍是比算法二有了改進。
基於以上的分析,咱們想一想,有沒有一種既能快速查找,又能快速移動元素的數據結構呢?回答是確定的,那就是堆。
藉助堆結構,咱們能夠在log量級的時間內查找和調整/移動。所以到這裏,咱們的算法能夠改進爲這樣,維護一個K(該題目中是10)大小的小根堆,而後遍歷300萬的Query,分別和根元素進行對比。
思想與上述算法二一致,只是算法在算法三,咱們採用了最小堆這種數據結構代替數組,把查找目標元素的時間複雜度有O(K)降到了O(logK)。
那麼這樣,採用堆數據結構,算法三,最終的時間複雜度就降到了N‘logK,和算法二相比,又有了比較大的改進。
總結:
至此,算法就徹底結束了,通過上述第一步、先用Hash表統計每一個Query出現的次數,O(N);而後第二步、採用堆數據結構找出Top 10,N*O(logK)。因此,咱們最終的時間複雜度是:O(N) + N'*O(logK)。(N爲1000萬,N’爲300萬)。
方案二:採用trie樹,關鍵字域存該查詢串出現的次數,沒有出現爲0。最後用10個元素的最小推來對出現頻率進行排序。
三、有一個1G大小的一個文件,裏面每一行是一個詞,詞的大小不超過16字節,內存限制大小是1M。返回頻數最高的100個詞。
分而治之 + hash統計 + 堆/快速排序這個套路,咱們已經開始有了屢試不爽的感受。下面,再拿幾道再多多驗證下。請看此第3題:又是文件很大,又是內存受限,咋辦?還能怎麼辦呢?無非仍是:
讀者反饋@ylqndscylq:本文評論下,有讀者ylqndscylq反應:每一個小文件取前100會有問題。是否真如此,我們先且看下一道題,第4題(稍後,咱們將意識到,這第3題給出的算法有問題)。
有網友提出:呵呵
普通解法:分治,hash,歸併,最大(小)堆,map reducer等算法,你的小內存致使了只能用時間換空間的作法, 好比屢次的遍歷,big set分裂成小set,使用磁盤索引等。
2B解法: lucene
文藝解法(ibm研究院提供):基於priori algorithm.
http://rakesh.agrawal-family.com/papers/vldb94apriori.pdf
四、一個文本文件,大約有一萬行,每行一個詞,要求統計出其中最頻繁出現的前10個詞,請給出思想,給出時間複雜度分析。
方案1:這題是考慮時間效率。用trie樹統計每一個詞出現的次數,時間複雜度是O(n*le)(le表示單詞的平準長度)。而後是找出出現最頻繁的前10個詞,能夠用堆來實現,前面的題中已經講到了,時間複雜度是O(n*lg10)。因此總的時間複雜度,是O(n*le)與O(n*lg10)中較大的哪個。
a 49次b 50次c 2次d 1次
a 9次b 1次c 11次d 10次
方案1:在前面的題中,咱們已經提到了,用一個含100個元素的最小堆完成。複雜度爲O(100w*lg100)。
方案2:採用快速排序的思想,每次分割以後只考慮比軸大的一部分,知道比軸大的一部分在比100多的時候,採用傳統排序算法排序,取前100個。複雜度爲O(100w*100)。
算法以下:根據快速排序劃分的思想
(1) 先對全部數據分紅[a,b)b(b,d]兩個區間,(b,d]區間內的數都是大於[a,b)區間內的數
(2) 對(b,d]重複(1)操做,直到最右邊的區間個數小於100個。注意[a,b)區間不用劃分
(3) 向左邊的第一個區間取前100-n.n爲已取出的元素個數。方法仍然是對其劃分,取[c,d]區間。若是個數不夠,繼續(3)操做
(4) 有必要的話,對取出的100個數進行快速排序。over~
方案3:採用局部淘汰法。選取前100個元素,並排序,記爲序列L。而後一次掃描剩餘的元素x,與排好序的100個元素中最小的元素比,若是比這個最小的要大,那麼把這個最小的元素刪除,並把x利用插入排序的思想,插入到序列L中。依次循環,知道掃描了全部的元素。複雜度爲O(100w*100)。
進一步:1億數據找出最大的1w個
1. 分塊法
解法:A. 採用分塊法,將1億數據分紅100w一塊,共100塊。
B. 對每塊進行快速排序,分紅兩堆,若是大堆大於1w個,則對大堆再次進行快速排序,直到小於等於1w中止
(假設此時大堆有N個),此時對小堆進行排序,取最大的10000-N個,這樣就找到了這100w中最大的1w個。
C. 100塊,每塊選出最大的1w,再對這100w使用一樣的方法,找出最大的1w個
2. Bit-Map
適用範圍:可進行數據的快速查找,判重,刪除,通常來講數據範圍是int的10倍如下
解法:用一個例子來講明吧,這樣直觀一點。
假設對7, 6, 3, 5這四個數進行排序,首先初始化一個byte,8位,可表示爲0 0 0 0 0 0 0 0
對於7,將第七位置1,對剩下幾個數執行一樣操做,則最後該byte變爲 0 0 1 0 1 1 1 0
最後一步,遍歷,將置1位的序號逐個輸出,即3,5, 6,7
3. 紅黑樹
解法:用一個紅黑樹維護這1w個數,而後遍歷其餘數字,來替換紅黑樹中最小的數(這是在網上看到的算法,
我感受用贏 者樹也是能夠的)
若是數據中有重複,則對於Bit-Map,找出前1w個數,對這1w個數創建Hash Table,而後再次遍歷這一億個數,同時對Hash Table中的數字 計數,最後根據計數找出前1w個(包含重複)
直接上:
方案3:與方案1相似,但在作完hash,分紅多個文件後,能夠交給多個文件來處理,採用分佈式的架構來處理(好比MapReduce),最後再進行合併。
八、 給定a、b兩個文件,各存放50億個url,每一個url各佔64字節,內存限制是4G,讓你找出a、b文件共同的url?
方案一:能夠估計每一個文件安的大小爲5G×64=320G,遠遠大於內存限制的4G。因此不可能將其徹底加載到內存中處理。考慮採起分而治之的方法。
OK,此第一種方法:分而治之/hash映射 + hash統計 + 堆/快速/歸併排序,再看最後三道題,以下:
方案二:若是容許有必定的錯誤率,可使用Bloom filter,4G內存大概能夠表示340億bit。將其中一個文件中的url使用Bloom filter映射爲這340億bit,而後挨個讀取另一個文件的url,檢查是否與Bloom filter,若是是,那麼該url應該是共同的url(注意會有必定的錯誤率)。
九、怎麼在海量數據中找出重複次數最多的一個?
方案1:先作hash,而後求模映射爲小文件,求出每一個小文件中重複次數最多的一個,並記錄重複次數。而後找出上一步求出的數據中重複次數最多的一個就是所求(具體參考前面的題)。
十、上千萬或上億數據(有重複),統計其中出現次數最多的錢N個數據。
方案1:上千萬或上億的數據,如今的機器的內存應該能存下。因此考慮採用hash_map/搜索二叉樹/紅黑樹等來進行統計次數。而後就是取出前N個出現次數最多的數據了,能夠用第2題提到的堆機制完成。
十一、 1000萬字符串,其中有些是重複的,須要把重複的所有去掉,保留沒有重複的字符串。請怎麼設計和實現?
首先映射爲內存能夠處理的n個小文件,這時相同的字符串確定在同一個文件中,在每一個小文件中使用hash_set取出重複的字符串,以後寫 到一個文件中,依次處理n個文件,便可獲得結果。。
或者小數據量時用map,構造快,大數據量時用hash_map?
rbtree PK hashtable
據朋友№邦卡貓№的作的紅黑樹和hash table的性能測試中發現:當數據量基本上int型key時,hash table是rbtree的3-4倍,但hash table通常會浪費大概一半內存。
由於hash table所作的運算就是個%,而rbtree要比較不少,好比rbtree要看value的數據 ,每一個節點要多出3個指針(或者偏移量) 若是須要其餘功能,好比,統計某個範圍內的key的數量,就須要加一個計數成員。
OK,更多請待後續實驗論證。接下來,我們來看第二種方法,雙層捅劃分。
雙層桶劃分----其實本質上仍是分而治之的思想,重在「分」的技巧上!
適用範圍:第k大,中位數,不重複或重複的數字
基本原理及要點:由於元素範圍很大,不能利用直接尋址表,因此經過屢次劃分,逐步肯定範圍,而後最後在一個能夠接受的範圍內進行。能夠經過屢次縮小,雙層只是一個例子。
擴展:
問題實例:
十一、2.5億個整數中找出不重複的整數的個數,內存空間不足以容納這2.5億個整數。
有點像鴿巢原理,整數個數爲2^32,也就是,咱們能夠將這2^32個數,劃分爲2^8個區域(好比用單個文件表明一個區域),而後將數據分離到不一樣的區域,而後不一樣的區域在利用bitmap就能夠直接解決了。也就是說只要有足夠的磁盤空間,就能夠很方便的解決。
#include<stdio.h> #include<memory.h> //用char數組存儲2-Bitmap,不用考慮大小端內存的問題 unsigned char flags[1000]; //數組大小自定義 unsigned get_val(int idx) { int i = idx/4; int j = idx%4; unsigned ret = (flags[i]&(0x3<<(2*j)))>>(2*j); return ret; } unsigned set_val(int idx, unsigned int val) { int i = idx/4; int j = idx%4; unsigned tmp = (flags[i]&~((0x3<<(2*j))&0xff)) | (((val%4)<<(2*j))&0xff); flags[i] = tmp; return 0; } unsigned add_one(int idx) { if (get_val(idx)>=2) { return 1; } else { set_val(idx, get_val(idx)+1); return 0; } } //只測試非負數的狀況; //假如考慮負數的話,需增長一個2-Bitmap數組. int a[]={1, 3, 5, 7, 9, 1, 3, 5, 7, 1, 3, 5,1, 3, 1,10,2,4,6,8,0}; int main() { int i; memset(flags, 0, sizeof(flags)); printf("原數組爲:"); for(i=0;i < sizeof(a)/sizeof(int); ++i) { printf("%d ", a[i]); add_one(a[i]); } printf("\r\n"); printf("只出現過一次的數:"); for(i=0;i < 100; ++i) { if(get_val(i) == 1) printf("%d ", i); } printf("\r\n"); return 0; }
除了用2-Bitmap來計數標記之外,也能夠用兩個1-Bitmap來實現(若是考慮正負數的狀況,就四個1-Bitmap)
十二、5億個int找它們的中位數。
關於什麼是Bloom filter,請參看blog內此文:
適用範圍:能夠用來實現數據字典,進行數據的判重,或者集合求交集
基本原理及要點:
對於原理來講很簡單,位數組+k個獨立hash函數。將hash函數對應的值的位數組置1,查找時若是發現全部hash函數對應位都是1說明存在,很明顯這個過程並不保證查找的結果是100%正確的。同時也不支持刪除一個已經插入的關鍵字,由於該關鍵字對應的位會牽動到其餘的關鍵字。因此一個簡單的改進就是 counting Bloom filter,用一個counter數組代替位數組,就能夠支持刪除了。
還有一個比較重要的問題,如何根據輸入元素個數n,肯定位數組m的大小及hash函數個數。當hash函數個數k=(ln2)*(m/n)時錯誤率最小。在錯誤率不大於E的狀況下,m至少要等於n*lg(1/E)才能表示任意n個元素的集合。但m還應該更大些,由於還要保證bit數組裏至少一半爲0,則m應該>=nlg(1/E)*lge 大概就是nlg(1/E)1.44倍(lg表示以2爲底的對數)。
舉個例子咱們假設錯誤率爲0.01,則此時m應大概是n的13倍。這樣k大概是8個。
注意這裏m與n的單位不一樣,m是bit爲單位,而n則是以元素個數爲單位(準確的說是不一樣元素的個數)。一般單個元素的長度都是有不少bit的。因此使用bloom filter內存上一般都是節省的。
擴展:
Bloom filter將集合中的元素映射到位數組中,用k(k爲哈希函數個數)個映射位是否全1表示元素在不在這個集合中。Counting bloom filter(CBF)將位數組中的每一位擴展爲一個counter,從而支持了元素的刪除操做。Spectral Bloom Filter(SBF)將其與集合元素的出現次數關聯。SBF採用counter中的最小值來近似表示元素的出現頻率。
1三、給你A,B兩個文件,各存放50億條URL,每條URL佔用64字節,內存限制是4G,讓你找出A,B文件共同的URL。若是是三個乃至n個文件呢?
根據這個問題咱們來計算下內存的佔用,4G=2^32大概是40億*8大概是340億,n=50億,若是按出錯率0.01算須要的大概是650億個bit。如今可用的是340億,相差並很少,這樣可能會使出錯率上升些。另外若是這些urlip是一一對應的,就能夠轉換成ip,則大大簡單了。
同時,上文的第5題:給定a、b兩個文件,各存放50億個url,每一個url各佔64字節,內存限制是4G,讓你找出a、b文件共同的url?若是容許有必定的錯誤率,可使用Bloom filter,4G內存大概能夠表示340億bit。將其中一個文件中的url使用Bloom filter映射爲這340億bit,而後挨個讀取另一個文件的url,檢查是否與Bloom filter,若是是,那麼該url應該是共同的url(注意會有必定的錯誤率)。
下面關於Bitmap的應用,直接上題,以下第九、10道:
14/11題、在2.5億個整數中找出不重複的整數,注,內存不足以容納這2.5億個整數。
方案1:採用2-Bitmap(每一個數分配2bit,00表示不存在,01表示出現一次,10表示屢次,11無心義)進行,共需內存2^32 * 2 bit=1 GB內存,還能夠接受。而後掃描這2.5億個整數,查看Bitmap中相對應位,若是是00變01,01變10,10保持不變。所描完過後,查看bitmap,把對應位是01的整數輸出便可。
方案2:也可採用與第1題相似的方法,進行劃分小文件的方法。而後在小文件中找出不重複的整數,並排序。而後再進行歸併,注意去除重複的元素。
1五、騰訊面試題:給40億個不重複的unsigned int的整數,沒排過序的,而後再給一個數,如何快速判斷這個數是否在那40億個數當中?
第一反應時快速排序+二分查找。如下是其它更好的方法:
方案1:frome oo,用位圖/Bitmap的方法,申請512M的內存,一個bit位表明一個unsigned int值。讀入40億個數,設置相應的bit位,讀入要查詢的數,查看相應bit位是否爲1,爲1表示存在,爲0表示不存在。
方案2:這個問題在《編程珠璣》裏有很好的描述,你們能夠參考下面的思路,探討一下:
又由於2^32爲40億多,因此給定一個數可能在,也可能不在其中;
這裏咱們把40億個數中的每個用32位的二進制來表示
假設這40億個數開始放在一個文件中。
而後將這40億個數分紅兩類:
1.最高位爲0
2.最高位爲1
並將這兩類分別寫入到兩個文件中,其中一個文件中數的個數<=20億,而另外一個>=20億(這至關於折半了);
與要查找的數的最高位比較並接着進入相應的文件再查找
再而後把這個文件爲又分紅兩類:
1.次最高位爲0
2.次最高位爲1
並將這兩類分別寫入到兩個文件中,其中一個文件中數的個數<=10億,而另外一個>=10億(這至關於折半了);
與要查找的數的次最高位比較並接着進入相應的文件再查找。
…….
以此類推,就能夠找到了,並且時間複雜度爲O(logn),方案2完。
1六、給40億個unsigned int的整數,如何判斷這40億個數中哪些數重複?
同理,能夠申請512M的內存空間,而後讀取40億個整數,而且將相應的bit位置1。若是是第一次讀取某個數據,則在將該bit位置1以前,此bit位一定是0;若是是第二次讀取該數據,則可根據相應的bit位是否爲1判斷該數據是否重複。
附:這裏,再簡單介紹下,位圖方法:
使用位圖法判斷整形數組是否存在重複
判斷集合中存在重複是常見編程任務之一,當集合中數據量比較大時咱們一般但願少進行幾回掃描,這時雙重循環法就不可取了。
位圖法比較適合於這種狀況,它的作法是按照集合中最大元素max建立一個長度爲max+1的新數組,而後再次掃描原數組,遇到幾就給新數組的第幾位置上 1,如遇到5就給新數組的第六個元素置1,這樣下次再遇到5想置位時發現新數組的第六個元素已是1了,這說明此次的數據確定和之前的數據存在着重複。這 種給新數組初始化時置零其後置一的作法相似於位圖的處理方法故稱位圖法。它的運算次數最壞的狀況爲2N。若是已知數組的最大值即能事先給新數組定長的話效 率還能提升一倍。
Trie樹
適用範圍:數據量大,重複多,可是數據種類小能夠放入內存
基本原理及要點:實現方式,節點孩子的表示方式
擴展:壓縮實現。
問題實例:
更多有關Trie樹的介紹,請參見此文:從Trie樹(字典樹)談到後綴樹。
數據庫索引
適用範圍:大數據量的增刪改查
基本原理及要點:利用數據的設計實現方法,對海量數據的增刪改查進行處理。
倒排索引(Inverted index)
適用範圍:搜索引擎,關鍵字查詢
基本原理及要點:爲什麼叫倒排索引?一種索引方法,被用來存儲在全文搜索下某個單詞在一個文檔或者一組文檔中的存儲位置的映射。
以英文爲例,下面是要被索引的文本:
T0 = "it is what it is"
T1 = "what is it"
T2 = "it is a banana"
咱們就能獲得下面的反向文件索引:
"a": {2}
"banana": {2}
"is": {0, 1, 2}
"it": {0, 1, 2}
"what": {0, 1}
檢索的條件"what","is"和"it"將對應集合的交集。
正向索引開發出來用來存儲每一個文檔的單詞的列表。正向索引的查詢每每知足每一個文檔有序頻繁的全文查詢和每一個單詞在校驗文檔中的驗證這樣的查詢。在正向索引中,文檔佔據了中心的位置,每一個文檔指向了一個它所包含的索引項的序列。也就是說文檔指向了它包含的那些單詞,而反向索引則是單詞指向了包含它的文檔,很容易看到這個反向的關係。
擴展:
問題實例:文檔檢索系統,查詢那些文件包含了某單詞,好比常見的學術論文的關鍵字搜索。
關於倒排索引的應用,更多請參見:
適用範圍:大數據的排序,去重
基本原理及要點:外排序的歸併方法,置換選擇敗者樹原理,最優歸併樹
擴展:
問題實例:
1).有一個1G大小的一個文件,裏面每一行是一個詞,詞的大小不超過16個字節,內存限制大小是1M。返回頻數最高的100個詞。
這個數據具備很明顯的特色,詞的大小爲16個字節,可是內存只有1M作hash明顯不夠,因此能夠用來排序。內存能夠當輸入緩衝區使用。
關於多路歸併算法及外排序的具體應用場景,請參見blog內此文:
MapReduce是一種計算模型,簡單的說就是將大批量的工做(數據)分解(MAP)執行,而後再將結果合併成最終結果(REDUCE)。這樣作的好處是能夠在任務被分解後,能夠經過大量機器進行並行計算,減小整個操做的時間。但若是你要我再通俗點介紹,那麼,說白了,Mapreduce的原理就是一個歸併排序。
適用範圍:數據量大,可是數據種類小能夠放入內存
基本原理及要點:將數據交給不一樣的機器去處理,數據劃分,結果歸約。
擴展:
問題實例:
更多具體闡述請參見blog內:
操做系統中的方法,先生成4G的地址表,在把這個表劃分爲小的4M的小文件作個索引,二級索引。30位前十位表示第幾個4M文件,後20位表示在這個4M文件的第幾個,等等,基於key value來設計存儲,用key來建索引。
但若是如今只有10000個數,而後怎麼去隨機從這一萬個數裏面隨機取100個數?請讀者思考。更多海里數據處理面試題,請參見此文第一部分:http://blog.csdn.net/v_july_v/article/details/6685962。