這篇博客源自對一個內存無法處理的詞頻統計問題的思考,最后給出的解決辦法是自己想的,可以肯定這不是最好的解法。但是通過和同學的討論,仍然感覺這是一個有意義及有意思的問題,所以和大家分享與探討。
如果有誤,請大家指正。如果有更好的方法,望不吝賜教。
1、提出問題
實際問題:
當前有10T中文關鍵詞數據,需要統計出詞頻最高的1000個詞。可用的只有1G內存和磁盤。那么如何提取?
大概估算一下這個問題,設中文詞匯平均長度2.3,每次漢字用utf-8編碼是3B,那么10T數據大概有 10T/7B ~ 1.4 * 2^40 條詞匯。
1G內存即便對詞匯一 一對應,再加上4B的整形記錄詞頻,那么1G內存可以最多紀錄 1G/4B * 2^8 ~ 2^36 條詞匯。所以即便最理想的hash(一 一對應)也無法將所有詞匯對應到1G內存中。
抽象問題:
假設當前內存只能存放1000個詞,但是一共有100w詞匯需要處理,問如何返回前100個高頻詞匯?(只能使用單機內存和磁盤)
下面我們以抽象的問題為基礎進行分析。
2、提煉解決辦法
這個問題大的方向有兩步,1 、 統計詞頻(重點思考) ; 2 、返回topK。
如果這兩步中涉及的數據都能放入內存中,那么最不濟 O(N^2) 便可以解決。不過由於內存無法承受,這兩步的解決都出現了問題。
2.1 統計詞頻
2.1.1 壓縮數據(不可行)
由上所訴,如果能壓縮放入內存便可以解決問題。所以先考慮能否找到一種壓縮數據的辦法。但是通過上面的分析,即使轉換為hash也無法存入內存,所以需要換一種思路。
2.1.2 歸並統計(不可行)
既然hash不能把所有數據放入內存,那么至少可以放一部分。再加上利用磁盤的存儲,把hash值在一定范圍內的詞存放到第 i 個文件中。這樣每個文件直接互不相交,而且每個文件都能在內存中處理,一個一個文件的進行統計,那么統計詞頻的問題自然可以解決了。
這個想法看似可行,其實不然。
對於抽象問題所述,100w/1000 = 1000,所以至少需要1000個文件存放互不相交的詞。但是這里有兩個問題,1 是如何尋找這個hash函數,保證100w個詞在hash后能平均分配到1000個文件中。 2 是需要保證這個hash函數不會產生沖突,不然的話會導致不同的詞的統計到一起。
因此這個hash函數是比較難找的。(如果大家知道分享下啊)
2.1.3 仍然是歸並(可行的解決辦法)
上述2中的歸並,是需要人為的把詞分到不同的桶里。但是hash函數太難設計了。
不過受《數據結構》書中對數值外排序的啟發,我想到了類似歸並排序的解決辦法。
即對於抽象問題所述,首先按內存大小,把100w數據順序分詞1000份,這1000份是均勻大小的桶,上述2.1.2中難以實現的均分桶在這里可以很簡單的實現。對於每個桶中的數據,因為每個詞在內存中對應了一個2進制數值,可以通過移位比較兩兩詞之間的數值大小。這樣,詞直接可以進行比較了,因此就可以進行排序了。再利用外排序的方法,就可以構成一個歸並的解,因此可以將所有詞匯進行詞頻統計。
時間復雜度為 O(M/N * log(N) + M *log(M/N) ) 。(M是所有數據量,N是每個桶中的數據量)
具體到抽象問題,即 O( 100w/100 * log(1000) (排序) + 100w * log(100w/1000) (歸並) ) 。
2.2 返回topK
topK的方法有兩種,一是 堆排序。 二是 K查找。
二者對比:
堆排序 K查找
時間復雜度 O(logN) O(N)
空間復雜度 O(K) O(N) (K就是topK的K)
topK結果 有序 無序
2.2.1 堆排序(適合本題)
對於提到的抽象數據,使用堆排序更合適。首先拿出第一個文件中的1000條數據構造一個 top100的堆樹。然后剩下的999個文件,每次從每個文件取500條數據放入內存,和top100的堆樹重新構造堆。那么需要一遍文件讀取就能得到top100的詞。所以對於文件讀取為主要時間消耗的外排來說,堆排序無疑更合適。
本題時間復雜度 100*log100 + 100w * log100 + 1000 read file time
2.2.2 K查找(不可行)
似乎關於K查找相關文章不多,這里提一下。
K查找類似快排,因為快排的每一趟處理,都會得到樞紐值得位置,如果這個位置正好為所求的K,便可以返回topK的結果。
算法大概思想:一個頭指針,一個尾指針。選擇某值作為平衡點,調整平衡點兩邊數值位置(到這里和快排一致)。然后根據平衡點和K值的關系,選擇頭一半或尾一半進行上述遞歸的查找(快排是兩邊都查找)。直到平衡點和K相等退出。可見K值只是划分了兩個邊界,每一個邊界內的數據並不是有序的。
因此K查找的時間復雜度: N + N/2 + N/4 ... 1 ~ 2N ,為 O(N)
K查找代碼如下:
1 int Kfind(int array[], int lowPos, int highPos, int K){ 2 int first = lowPos; 3 int last = highPos; 4 int key = array[first]; 5 while(first < last){ 6 while(first<last && array[last]>=key) 7 last--; 8 array[first] = array[last]; 9 while(first<last && array[first]<=key) 10 first++; 11 array[last] = array[first]; 12 } 13 array[first] = key; 14 if(first == K-1) 15 return first+1; 16 if(lowPos < first && first > K-1) 17 return Kfind(array, lowPos, first-1, K); 18 if(first < highPos && first < K-1) 19 return Kfind(array, first+1, highPos, K); 20 }
測試樣例及結果:
可見,topK內的數據並不一定有序。
但是對於本題,如果強行在文件中直接進行K查找,那么時間消耗會非常大。因此不適合。
3、擴展
如果本題不限制完成條件,使用hadoop的MapReduce框架很適合解決本問題,詞之間沒有聯系,分治是很好的選擇。
當然,即使對於單機環境來說,詞頻統計也已經不算大數據量了,但是作為一個問題思考還是有意義的。
轉載請注明出處,謝謝~ http://www.cnblogs.com/xiaoboCSer/p/4202394.html