海量數據處理基本方法總結

一. 海量數據處理的困難

海量數據處理的困難用一句話概括就是:時空(時間,空間)資源不夠。

  • 時間受限:無法在有限的時間內,完成針對海量數據的某項特定工作的處理;
  • 空間受限:無法將海量數據一次性讀入內存;

對於時間受限的問題,我們一般的解決辦法是高效的算法配合恰當的數據結構,比如哈希表,堆,二叉樹等;而對於空間受限的問題,一般的解決辦法就是 “大而化小,分而治之” 的策略,既然一次行不通,那就一部分一部分的讀取,每讀入一部分可以生成一個小文件,小文件是可以直接讀放入內存中去的,然後就這樣分割大數據之後,再依次處理各個的小文件的數據,最後歸併即可。

二.常用方法

1.哈希映射
  • 解決問題:海量數據不能一次性讀入內存,而我們需要對海量數據進行相應的操作(計數、排序等操作)
  • 使用工具:hash函數(hash表);堆

這種方法是典型的== “分而治之”== 的策略,也是解決空間限制最常用的方法,基本思路可以用下圖表示。先借助哈希算法,計算每一條數據的 hash 值,按照 hash 值將海量數據分佈存儲到多個桶中(所謂桶,一般可以用小文件實現)。根據 hash 函數的唯一性,相同的數據一定在同一個桶中。如此,我們再依次處理這些小文件,最後做合併運算即可。
在這裏插入圖片描述
注:一般用 hash 函數將數據映射到桶的方法是:

  • bucket_ID = H(mi) % n

其中 H(mi) 爲第 i 條數據,bucket_ID 爲桶標號,n 爲要設置的桶的數量。關於桶數量(即小文件數量)設置的基本的原則是:每個小文件的大小比內存限制要小。
比如處理 1G 的大文件,內存限制爲 1M,那就可以把大文件分成 2000 個小文件(甚至更多),這樣每個小文件的大小約 500K (甚至更小),我們就可以輕鬆讀入內存進行處理了。


2.分文件+哈希統計+內存處理(堆/快速/歸併排序)

對於固定大小的海量數據,通常可以採用 分文件+哈希統計+內存處理(堆/快速/歸併排序) 的方法。

  • 對於字符串數據,可以對字符串進行哈希(字符串.hashCode() ),哈希值%n ( n爲分成的小文件的數量 ),這樣來說同一個字符串必然分配到同一個小文件當中,然後如果哈希均勻的話(每個小文件的數量差不多一樣多,不會出現一個小文件的數量特別多,另一個小文件的數量特別少的情況),就能夠保證每個小文件都可以放到內存當中去,在內存當中採用通用方法進行處理,獲得每個小文件的結果,然後挨個寫到磁盤中,最後彙總或者直接在內存中彙總即可。
  • 對於數字,可以直接 num%n(n爲分的小文件的數量),方法和對字符串數據處理方法一樣。

3.Bloom filter(布隆過濾器)

  • 解決問題:數據字典的構建;判定目標數據是否存在於一個海量數據集;集合求交
  • 使用工具:Bloom Filter;hash函數
  • 以存在性判定爲例,Bloom Filter通過對目標數據的映射,能夠以 O(k)的 時間複雜度判定目標數據的存在性,其中 k 爲使用的 hash 函數的個數,這樣就能大大縮減遍歷查找所需的時間。

布隆過濾器:

  • 選 k 個哈希函數,記爲 {h1,h2,…,hk}
  • 假設現在有 n 個元素需要被映射到 bit 數組中,bit 數組的長度是 m . 初始時,將 m 大小的 bit 數組的每個位置的元素都置爲 0 。
  • 現在,把這個 n 個元素依次用第 1 步選取的 k 個哈希函數映射到 bit 數組的位置中去,bit 數組被映射到的位置的元素變爲1。顯然,一個元素能被映射到 k 個位置上。過程如圖所示,現在把元素集合 {x,y,z} 通過 3 個哈希函數映射到一個二進制數組中。
  • 最後,需要檢查一個元素是否在已有的集合中時,同樣用這 k 個哈希函數把要判斷的元素映射到 bit 數組上去,只要 bit 數組被映射到的位置中有一個位不是 1 ,那一定說明了這個元素不在已有的集合內。

如圖:W 元素就不在集合中
在這裏插入圖片描述
我們爲什麼用Trie樹?

  1. 節約字符串的存儲空間
    假設現在我們需要對海量字符串構建字典。所謂字典就是一個集合,這個集合包含了所有不重複的字符串,那麼現在就出現了一個問題,那就是字典對存儲空間的消耗過大,而當這些字符串中存在大量的串擁有重複的前綴時,這種消耗就顯得過於浪費了。比如:“ababc”,“ababd”,“ababrf”,“abab…”,…“ababc”,“ababd”,“ababrf”,“abab…”,…,這些字符串幾乎都擁有公共前綴 ”abab”。 我們直接的想法是,能不能通過一種存儲結構節約存儲成本,使得所有擁有重複前綴的串只存儲一遍。這種存儲的應用場景如果是對DNA序列的存儲,那麼出現重複前綴的可能性更大,空間需求也就更爲強烈。

  2. 字符串檢索
    檢索一個字符串是否屬於某個詞典時,我們當前一般有兩種思路:
    線性遍歷詞典,計算複雜度 O(n),n 爲詞典長度;利用 hash 表,預先處理字符串集合。這樣再搜索運算時,計算複雜度 O(1)。但是 hash 計算可能存在碰撞問題,所以,能不能設計一種高效的數據結構幫助解決字符串檢索的問題?


4.Trie樹(前綴樹)

  • 實現方式:節點孩子的表示方式
  • 擴展:壓縮實現。
  • 適用範圍:在字典的存儲,字符串的查找,求取海量字符串的公共前綴,以及字符串統計等方面發揮着重要的作用。
  • 適用範圍:Trie 樹是一種非常強大的處理海量字符串數據的工具。尤其是大量的字符串數據中存在前綴時,Trie 樹特別好用。用於存儲時,Trie 樹因爲不重複存儲公共前綴,節省了大量的存儲空間;用於字符串的查找時,Trie 樹依靠其特殊的性質,實現了在任意數據量的字符串集合中都能以 O(len) 的時間複雜度完成查找(len 爲要檢索的字符串長度);在字符串統計中,Trie 樹能夠快速記錄每個字符串出現的次數。

Trie樹是如圖所示的一棵多叉樹。其中存儲的字符串集合爲:
{“a”,“aa”,“ab”,“ac”,“aab”,“aac”,“bc”,“bd”,“bca”,“bcc”}

在這裏插入圖片描述
從上圖我們可以看出,Trie 樹有如下3點特徵:

  • 根節點不代表字符,除根節點外每一個節點都只代表一個字符串(一般的解釋是:除根節點外所有節點只 “包含” 一個字符串。
  • 從根節點到某一節點,路徑上經過的字符連接起來,爲該節點對應的字符串。
  • 每個節點的所有子節點包含的字符都不相同。
  • 其實,一棵完整的 Trie 樹應該每個非葉節點都擁有 26 個子節點,正好對應着英文的 26 個字母,這樣整棵樹的空間成本爲 26^l ,l 爲最長字符串的長度。但是爲了節省空間,我們可以根據字符串集本身爲每個非葉節點,“量身定做”子節點。以上面的圖爲例,以 ”a” 開頭的字符串中,第二個字符只有 ”a, b, c” 3 種可能,因此我們當然沒有必要爲節點 u1 生成 26 個子節點,所以 3 個子節點就夠了。

除此之外,由於有些字符串就是集合中其他字符串的前綴,爲了能夠分辨清楚集合中到底有哪些字符串,我們還需要爲每個節點賦予一個判斷終止與否的 boolean 值,記爲 end 。比如上圖,由於同時存在字符{“a”,“ab”,“ac”,“aa”,“aab”,“aac”} 我們就令節點 u1,u2 的 end 值爲 True,表示從根節點到 u1,u2 的路徑上的字符按順序可以構成集合中一個完整的字符串(如”a”, “aa”)圖中,我們將end == True的節點標紅。


5.外排序

  • 解決問題:海量數據的排序
  • 實用工具:歸併算法
  • 針對內容:外部排序是針對海量數據無法一次性放入內存中而產生的一種方法。
  • 基本算法是:將海量數據拆分成多個小文件,每個小文件可以一次性放入內存當中,然後對於每一個小文件進行排序後(這裏採用任何排序算法都可以),再採用多路歸併排序即可。多路歸併也就是構造一個大小爲小文件數量的堆(升序構造小堆,降序構造大堆)。

6.多層桶結構

  • 解決問題:海量數據求取第 k 大的數
  • 使用工具:hash 函數
  • 多層桶結構其實和用 hash 映射,分割大文件的思路是一致的,都是一種“分而治之” 的策略。只不過多層桶結構是對有些桶分割之後再分割,構成了一種層次化的結構,它主要應用於一次分割的結果依然不能解決內存限制的情況。

三.常見問題

1.分而治之+hash映射+快速/歸併/堆排序

給定 a、b 兩個文件,各存放 50 億個 url ,每個 url 各佔 64 個字節,內存限制是 4G,讓你找出 a、b 文件共同的 url?

分析:50億*64字節 = 320G 大小空間。
算法思想:hash 分解+ 分而治之 + 歸併

  • 遍歷文件 a,對每個 url 根據某種 hash 規則求取 hash(url)/1024,然後根據所取得的值將 url 分別存儲到1024個小文件( a0- a1023)中,這樣每個小文件的大約爲 300M 。如果 hash 結果很集中使得某個文件 ai 過大,可以在對 ai 進行二級 hash( ai0~ ai1024)。
  • 這樣 url 就被 hash 到 1024 個不同級別的目錄中。然後可以分別比較文件,a0 VS b0……a1023 VS b1023,求每對小文件中相同的 url 時,可以把其中一個小文件的 url 存儲到 hash_map 中。然後遍歷另一個小文件的每個 url ,看其是否存在於剛纔構建的 hash_map 中,如果是,那麼就是共同的url ,存到文件裏面就可以了,如果不是則刪除。
  • 把 1024 個級別目錄下相同的 url 合併起來(存儲到 hash_map 中)。
  • 最後 hash_map 中的就是最終結果。

有 10 個文件,每個文件 1G,每個文件的每一行存放的都是用戶的 query ,每個文件的 query 都可能重複,要求你按照 query 的頻度排序。

解決思想1:hash分解+ 分而治之 +歸併

  • 順序讀取 10 個文件 a0-a9 ,按照 hash(query)%10 的結果將 query 寫入到另外 10 個文件(記爲 b0~b9)中。這樣新生成的文件每個的大小也約爲1G(假設 hash 函數是隨機的)。
  • 找一臺內存 2G 左右的機器,依次對用 hash_map(query, query_count) 來統計每個 query 出現的次數。利用 快速/堆/歸併排序 按照出現次數進行排序。將排序好的 query 和對應的 query_cout 輸出到文件中。這樣得到了10個排好序的文件 c0~c9 。
  • 對這 10 個文件 c0-c9 進行 歸併排序(內排序與外排序相結合)。每次取c0~c9 文件的 m 個數據放到內存中,進行 10m 個數據的歸併,即使把歸併好的數據存到 d 結果文件中。如果 ci 對應的 m 個數據全歸併完了,再從 ci餘下的數據中取 m 個數據重新加載到內存中,直到所有 ci 文件的所有數據全部歸併完成。

解決思想2: Trie樹

  • 如果 query 的總量是有限的,只是重複的次數比較多而已,可能對於所有的 query,一次性就可以加入到內存了。在這種假設前提下,我們就可以採用 trie樹/hash_map 等直接來統計每個 query 出現的次數,然後按出現次數做 快速/堆/歸併排序 就可以了。

有一個 1G 大小的一個文件,裏面每一行是一個詞,詞的大小不超過 16 字節,內存限制大小是 1M,返回頻數最高的100個詞。
類似問題:怎麼在海量數據中找出重複次數最多的一個?

解決思想: hash分解+ 分而治之+歸併

  • 順序讀文件,對於每個詞 x,按照 hash(x)/(1024*4) 存到 4096個小文件中。這樣每個文件大概是 250k 左右。如果哈希衝突嚴重,其中有的文件大小超過了 1M ,該文件還可以按照 hash 繼續往下分,直到分解得到的小文件的大小都不超過 1M。
  • 對每個小文件,統計每個文件中出現的詞以及相應的頻率(可以採用trie樹/hash_map 等),並存入4096個文件。
  • 下一步就是把這4096個文件進行歸併的過程了。(類似與歸併排序)
  • 然後去前100個此就行。

在 2.5 億個整數中找出不重複的整數,內存不足以容納這 2.5 億個整數。

解決思路 : hash 分解+ 分而治之 + 歸併

  • 2.5 億個 int 數據 hash 到 1024 個小文件中 (a0-a1023),如果某個小文件大小還大於內存,則對這個小文件進行多級 hash。每個小文件讀進內存,找出只出現一次的數據,輸出到 b0~b1023 文件中。
  • 最後數據合併即可。

上千萬或上億數據(有重複),統計其中出現次數最多的前 N 個數據。

解決思路: 紅黑樹 + 堆排序

  • 如果是上千萬或上億的 int 數據,現在的機器 4G 內存可以能存下,所以考慮採用 hash_map/搜索二叉樹/紅黑樹 等來進行統計重複次數。
  • 然後取出前 N 個出現次數最多的數據。

此處附上原文連接:https://blog.csdn.net/hong2511/article/details/80842704

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章