億萬級數據處理的高效解決方案

簡介

全文行文是基於面試題的分析基礎之上的,具體實踐過程中,還是得具體情況具體分析,且各個場景下需要考慮的細節也遠比本文所描述的任何一種解決方法複雜得多。
##何謂海量數據處理?
基於海量數據上的存儲、處理、操作。
何謂海量,就是數據量太大,導致要麼是無法在較短時間內迅速解決,要麼是數據太大,導致無法一次性裝入內存。

那解決辦法呢?

  • 針對時間,我們可以採用巧妙的算法搭配合適的數據結構,如Bloom filter/Hash/bit-map/堆/數據庫或倒排索引/trie樹
  • 針對空間,無非就一個辦法:大而化小,分而治之(hash映射),把規模大化爲規模小的,各個擊破

至於單機及集羣問題,通俗點來講

  • 單機就是處理裝載數據的機器有限(只需考慮CPU,內存,硬盤的數據交互)
  • 集羣,機器有多臺,適合分佈式處理,並行計算(更多考慮節點和節點間的數據交互)。

處理海量數據,不外乎

  1. 分而治之/hash映射 + hash統計 + 堆/快速/歸併排序
  2. 雙層桶劃分
  3. Bloom filter/Bitmap;
  4. Trie樹/數據庫/倒排索引;
  5. 外排序;
  6. 分佈式處理之Hadoop/Mapreduce。

本文第一部分、從set/map談到hashtable/hash_map/hash_set,簡要介紹下set/map/multiset/multimap,及hash_set/hash_map/hash_multiset/hash_multimap之區別(萬丈高樓平地起,基礎最重要),而本文第二部分,則針對上述那6種方法模式結合對應的海量數據處理面試題分別具體闡述。
#從set/map到hashtable/hashmap/hashset

  • 序列式容器
    vector/list/deque/stack/queue/heap
  • 關聯式容器。關聯式容器又分爲set(集合)和map(映射表)兩大類,還有第3類關聯式容器,如hashtable(散列表)
    類似關聯式數據庫,每筆數據或每個元素都有一個鍵值(key)和一個實值(value),即所謂的Key-Value(鍵-值對)
    ##set/map
    set,同map一樣,所有元素都會根據元素的鍵值自動被排序,值得注意的是,兩者都不允許兩個元素有相同的鍵值。
    不同的是:set的元素不像map那樣可以同時擁有實值(value)和鍵值(key),set元素的鍵值就是實值,實值就是鍵值,而map的所有元素同時擁有實值(value)和鍵值(key),pair的第一個元素被視爲鍵值,第二個元素被視爲實值。
    ##hash_set/hash_map
    hash_set/hash_map,兩者的一切操作都是基於hashtable之上。不同的是,hash_set同set一樣,同時擁有實值和鍵值,且實質就是鍵值,鍵值就是實值,而hash_map同map一樣,每一個元素同時擁有一個實值(value)和一個鍵值(key),所以其使用方式,和上面的map基本相同。
    但由於hash_set/hash_map都是基於hashtable之上,所以不具備自動排序功能。爲什麼?因爲hashtable沒有自動排序功能。

所以,綜上什麼樣的結構決定其什麼樣的性質,因爲set/map都是基於RB-tree之上,所以有自動排序功能,而hash_set/hash_map都是基於hashtable之上,所以不含有自動排序功能,至於加個前綴multi_無非就是允許鍵值重複而已。

祕技一:分而治之/Hash映射 + HashMap統計 + 堆/快速/歸併排序

Hash,就是把任意長度的輸入(又叫做預映射, pre-image),通過散列算法,變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小於輸入的空間,不同的輸入可能會散列成相同的輸出,而不可能從散列值來唯一的確定輸入值。簡單的說就是一種將任意長度的消息壓縮到某一固定長度的函數。

Hash主要用於信息安全領域中加密算法,它把一些不同長度的信息轉化成雜亂的128位的編碼,這些編碼值叫做Hash值. 也可以說,hash就是找到一種數據內容和數據存放地址之間的映射關係。

數組的特點是:尋址容易,插入和刪除困難
鏈表的特點是:尋址困難,插入和刪除容易。
那麼我們能不能綜合兩者的特性,做出一種尋址容易,插入刪除也容易的數據結構?答案是肯定的,這就是我們要提起的哈希表,哈希表有多種不同的實現方法,我接下來解釋的是最常用的一種方法——拉鍊法,我們可以理解爲“鏈表的數組”
億萬級數據處理的高效解決方案
左邊很明顯是個數組,數組的每個成員包括一個指針,指向一個鏈表的頭,當然這個鏈表可能爲空,也可能元素很多。我們根據元素的一些特徵把元素分配到不同的鏈表中去,也是根據這些特徵,找到正確的鏈表,再從鏈表中找出這個元素。

元素特徵轉變爲數組下標的方法就是散列法

  • 除法散列法
    最直觀的一種,上圖使用的就是這種散列法,公式:
    index = value % 16
    學過彙編的都知道,求模數其實是通過一個除法運算得到的,所以叫“除法散列法”。

  • 平方散列法
    求index是非常頻繁的操作,而乘法的運算要比除法來得省時,所以我們考慮把除法換成乘法和一個位移操作。公式:
    index = (value * value) >> 28
    如果數值分配比較均勻的話這種方法能得到不錯的結果,但我上面畫的那個圖的各個元素的值算出來的index都是0——非常失敗。也許你還有個問題,value如果很大,value * value不會溢出嗎?答案是會的,但我們這個乘法不關心溢出,因爲我們根本不是爲了獲取相乘結果,而是爲了獲取index。
  • 斐波那契(Fibonacci)散列法
    平方散列法的缺點是顯而易見的,所以我們能不能找出一個理想的乘數,而不是拿value本身當作乘數呢?答案是肯定的。
    1,對於16位整數而言,這個乘數是40503
    2,對於32位整數而言,這個乘數是2654435769
    3,對於64位整數而言,這個乘數是11400714819323198485

這幾個“理想乘數”是如何得出來的呢?這跟一個法則有關,叫黃金分割法則,而描述黃金分割法則的最經典表達式無疑就是著名的斐波那契數列,如果你還有興趣,就到網上查找一下“斐波那契數列”等關鍵字,我數學水平有限,不知道怎麼描述清楚爲什麼,另外斐波那契數列的值居然和太陽系八大行星的軌道半徑的比例出奇吻合,很神奇,對麼?

對我們常見的32位整數而言,公式:
index = (value * 2654435769) >> 28
如果用這種斐波那契散列法的話,那我上面的圖就變成這樣了:

億萬級數據處理的高效解決方案
很明顯,用斐波那契散列法調整之後要比原來的取模散列法好很多。

  • 適用範圍
    快速查找,刪除的基本數據結構,通常需要總數據量可以放入內存。
  • 基本原理及要點
    Hash函數選擇,針對字符串,整數,排列,具體相應的hash方法
    碰撞處理,一種是開放哈希法,亦拉鍊法;另一種就是closed hashing,也稱開地址法,opened addressing。
  • 擴展
    d-left hashing中的d是多個的意思,我們先簡化這個問題,看一看2-left hashing。2-left hashing指的是將一個哈希表分成長度相等的兩半,分別叫做T1和T2,給T1和T2分別配備一個哈希函數,h1和h2。在存儲一個新的key時,同 時用兩個哈希函數進行計算,得出兩個地址h1[key]和h2[key]。這時需要檢查T1中的h1[key]位置和T2中的h2[key]位置,哪一個 位置已經存儲的(有碰撞的)key比較多,然後將新key存儲在負載少的位置。如果兩邊一樣多,比如兩個位置都爲空或者都存儲了一個key,就把新key 存儲在左邊的T1子表中,2-left也由此而來。在查找一個key時,必須進行兩次hash,同時查找兩個位置。
    ##海量日誌數據,提取出某日訪問百度次數最多的那個IP
    無非分而治之/hash映射 + hash統計 + 堆/快速/歸併排序,說白了,就是先映射,後統計,最後排序
  • 分而治之/hash映射
    針對數據太大,內存受限,只能把大文件化成(取模映射)小文件
  • HashMap統計:當大文件轉化了小文件,便可以採用常規的HashMap(ip,value)進行頻率統計
  • 堆/快速排序
    統計完了之後,進行排序(可採取堆排序),得到次數最多的IP

首先是這一天,並且是訪問百度的日誌中的IP取出來,逐個寫入到一個大文件中。
注意到IP是32位的,最多有個2^32個IP。同樣可以採用映射的方法,比如%1000,把整個大文件映射爲1000個小文件,再找出每個小文中出現頻率最大的IP(可以採用HashMap對那1000個文件中的所有IP進行頻率統計,然後依次找出各個文件中頻率最大的那個IP)及相應的頻率。然後再在這1000個最大的IP中,找出那個頻率最大的IP,即爲所求。

還有幾個問題

  • Hash取模是一種等價映射,不會存在同一個元素分散到不同小文件中的情況,即這裏採用的是mod 1000算法,那麼相同的IP在hash取模後,只可能落在同一個文件中,不可能被分散
  • 那到底什麼是hash映射呢?
    簡單來說,就是爲了便於計算機在有限的內存中處理大數據,從而通過一種映射散列的方式讓數據均勻分佈在對應的內存位置(如大數據通過取餘的方式映射成小樹存放在內存中,或大文件映射成多個小文件),而這個映射散列方式便是我們通常所說的hash函數,好的hash函數能讓數據均勻分佈而減少衝突。儘管數據映射到了另外一些不同的位置,但數據還是原來的數據,只是代替和表示這些原始數據的形式發生了變化而已

#堆
##概念
堆是一種特殊的二叉樹,具備以下兩種性質

  • 每個節點的值都大於(或者都小於,即最小堆)其子節點的值
  • 樹完全平衡的,並且最後一層的樹葉都在最左邊

這樣就定義了一個最大堆
數組表示堆

  • 二叉堆
    一種完全二叉樹,其任意子樹的左右節點(如果有的話)的鍵值一定比根節點大,上圖其實就是一個二叉堆

最小的一個元素就是數組第一個元素,那麼二叉堆這種有序隊列如何入隊呢
億萬級數據處理的高效解決方案
假設要在這個二叉堆裏入隊一個單元,鍵值爲2,那隻需在數組末尾加入這個元素,然後儘可能把這個元素往上挪,直到挪不動,經過了這種複雜度爲Ο(logn)的操作,二叉堆還是二叉堆。

那如何出隊呢
億萬級數據處理的高效解決方案
出隊一定是出數組的第一個元素,這麼來第一個元素以前的位置就成了空位,我們需要把這個空位挪至葉子節點,然後把數組最後一個元素插入這個空位,把這個“空位”儘量往上挪。這種操作的複雜度也是Ο(logn)

  • 適用範圍
    海量數據前n大,並且n比較小,堆可以放入內存
  • 基本原理及要點
    最大堆求前n小,最小堆求前n大。方法,比如求前n小,我們比較當前元素與最大堆裏的最大元素,如果它小於最大元素,則應該替換那個最大元 素。這樣最後得到的n個元素就是最小的n個。適合大數據量,求前n小,n的大小比較小的情況,這樣可以掃描一遍即可得到所有的前n元素,效率很高。
  • 擴展
    雙堆,一個最大堆與一個最小堆結合,可以用來維護中位數。
    ##100w個數中找最大的前100個數
    用一個100個元素大小的最小堆即可。

###尋找熱門查詢,300萬個查詢字符串中統計最熱門的10個查詢
搜索引擎會通過日誌文件把用戶每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度爲1-255字節。假設目前有一千萬個記錄(這些查詢串的重複度比較高,雖然總數是1千萬,但如果除去重複後,不超過3百萬個。一個查詢串的重複度越高,說明查詢它的用戶越多,也就是越熱門),請你統計最熱門的10個查詢串,要求使用的內存不能超過1G。

解答:由上題,我們知道,數據大則劃爲小的,如一億個IP求Top 10,可先%1000將IP分到1000個小文件中去,並保證一種IP只出現在一個文件中,再對每個小文件中的IP進行HashMap計數統計並按數量排序,最後歸併或者最小堆依次處理每個小文件的Top10以得到最後的結果

但如果數據規模比較小,能一次性裝入內存呢?比如這題,雖然有一千萬個Query,但是由於重複度比較高,因此事實上只有300萬的Query,每個Query 255字節,因此我們可以考慮把他們都放進內存中去(300萬個字符串假設沒有重複,都是最大長度,那麼最多佔用內存3M*1K/4=0.75G。所以可以將所有字符串都存放在內存中進行處理),而現在只是需要一個合適的數據結構,在這裏,HashMap絕對是我們優先的選擇。

所以我們放棄分而治之hash映射的步驟,直接上hash統計,然後排序。針對此類典型的TOP K問題,採取的對策往往是:HashMap + 堆

  • HashMap統計
    對這批海量數據預處理
    維護一個Key爲Query字串,Value爲該串出現次數的HashMap,即HashMap(Query,Value),每次讀取一個Query,如果該字串不在HashMap中,則加入該串,並將Value設1
    若該串在HashMap,則將該串的計數加一
    最終我們在O(N)的時間複雜度內用HashMap完成了統計
  • 堆排序
    藉助堆這個數據結構,找出Top K,時間複雜度爲N*logK,即藉助堆結構,我們可以在log量級的時間內查找和調整。
    因此,維護一個K(該題目中是10)大小的小根堆,然後遍歷300萬的Query,分別和根元素進行對比。
    所以,我們最終的時間複雜度是O(N) + N' * O(logK),(N爲1000萬,N’爲300萬)。

  • 堆排序思路
    維護k個元素的最小堆,即用容量爲k的最小堆存儲最先遍歷到的k個數,並假設它們即是最大的k個數,建堆O(k),調整堆O(logk)後,有
    k1>k2>...kmin(kmin設爲小頂堆中最小元素)
    繼續遍歷數列,每次遍歷一個元素x,與堆頂元素比較,若x>kmin,則更新堆(x入堆,用時logk),否則不更新堆。這樣下來,總費時O(k*logk+(n-k)*logk)=O(n*logk)
    此方法得益於在堆中,查找等各項操作時間複雜度均爲logk
    也可以採用trie樹,關鍵字域存該查詢串出現的次數,沒有出現爲0
    最後用10個元素的最小堆來對出現頻率進行排序。
    ###有一個1G的文件,每一行是一個詞,詞的大小不超過16字節,內存限制大小是1M。返回頻數最高的100個詞
    由上面那兩個例題,分而治之 + hash統計 + 堆/快速排序這個套路再多多驗證下。此題又是文件很大,又是內存受限,無非還是
  • 分而治之/hash映射
    順序讀文件中,對於每個詞x,取hash(x)%5000,然後按照該值存到5000個小文件(記爲x0,x1,...x4999)中。這樣每個文件大概是200k。
    如果其中的有的文件超過了1M,還可以按照類似的方法繼續下分,直到分解得到的小文件都不超過1M
  • HashMap統計
    對每個小文件,採用trie樹/HashMap等統計每個文件中出現的詞以及相應的頻率
  • 堆/歸併排
    取出出現頻率最大的100個詞(可以用含100個結點的最小堆)後,再把100個詞及相應的頻率存入文件,這樣又得到了5000個文件。最後就是把這5000個文件進行歸併(類似於歸併排序)的過程了。
    ###海量數據分佈在10臺電腦中,想個辦法高效統計出這批數據的TOP10,如果每個數據元素只出現一次,而且只出現在某一臺機器中,那麼可以採取以下步驟統計出現次數TOP10的數據元素:
  • 堆排序
    在每臺電腦上求出TOP10,可以採用包含10個元素的堆完成(TOP10小,用最大堆,TOP10大,用最小堆,比如求TOP10大,我們首先取前10個元素調整成最小堆,如果發現,然後掃描後面的數據,並與堆頂元素比較,如果比堆頂元素大,那麼用該元素替換堆頂,然後再調整爲最小堆。最後堆中的元素就是TOP10大)。
  • 求出每臺電腦上的TOP10後,然後把這100臺電腦上的TOP10組合起來,共1000個數據,再利用上面類似的方法求出TOP10就可以了。
    ####如果同一個元素重複出現在不同的電腦中呢
     這個時候,你可以有兩種方法
  • 遍歷所有數據,重新hash取模,使同一個元素只出現在單獨的一臺電腦中,然後採用上面所說的方法,統計每臺電腦中各個元素的出現次數找出TOP10,繼而組合100臺電腦上的TOP10,找出最終的TOP10
  • 暴力求解:直接統計每臺電腦中各個元素的出現次數,然後把同一個元素在不同機器中的出現次數相加,最終從所有數據中找出TOP10
    ###10個文件,每個1G,每個文件的每一行存放的都是用戶的query,每個文件的query都可能重複。要求按照query的頻度排序
    ####方案1
  • Hash映射
    順讀10個文件,按照hash(query)%10query寫到另外10個文件(記爲a0,a1,..a9)中
    這樣新生成的文件每個的大小大約也1G)(假設hash函數較好)
  • HashMap統計
    找一臺內存在2G左右機器,依次用HashMap(query, query_count)統計每個query頻度
    注:HashMap(query,query_count)是統計每個query的出現次數,不是存儲他們的值,出現一次,則count+1
  • 堆/快速/歸併排序
    利用快速/堆/歸併排序按頻率排序,將排序好的query和對應的query_cout輸出到文件,就得到了10個排好序的文件億萬級數據處理的高效解決方案
    最後,對這10個文件進行歸併排序(內/外排相結合)
    ####方案2
    一般query的總量是有限的,只是重複的次數比較多而已,可能對於所有的query,一次性就可以加入到內存了。這樣,我們就可以採用trie樹/HashMap等直接統計每個query出現的次數,然後按次數做快速/堆/歸併排序
    ####方案3
    與方案1類似,但在做完hash,分成多個文件後,可以交給多個文件來處理,採用分佈式的架構來處理(比如MapReduce),最後再進行合併
    ###給定a、b兩個文件,各存放50億個url,每個url各佔64字節,內存限制是4G,找出a、b文件共同的url
    可估計每個文件的大小爲5G×64=320G,遠遠大於內存限制。所以不可能將其完全加載到內存中處理。考慮採取分而治之的方法
  • 分而治之/hash映射
    遍歷文件a,對每個url求取億萬級數據處理的高效解決方案
    然後根據所取得的值將url分別存儲到1000個小文件億萬級數據處理的高效解決方案
    (漏個a1)中。
    這樣每個小文件大約300M
    遍歷文件b,採取和a相同方式將url分別存儲到1000個小文件
    億萬級數據處理的高效解決方案
    這樣處理後,所有可能相同的url都在對應的小文件億萬級數據處理的高效解決方案
    不對應的小文件不可能有相同的url。然後我們只要求出1000對小文件中相同的url即可
  • HashSet統計
    求每對小文件中相同的url時,可以把其中一個小文件的url存儲到HashSet
    然後遍歷另一個小文件的url,看其是否在剛纔構建的HashSet中,如果是,那麼就是共同的url,存到文件即可

此即第一個祕技
分而治之/hash映射 + hash統計 + 堆/快速/歸併排序
再看最後4道題
###在海量數據中找出重複次數最多的

  • 先hash
  • 然後求模映射爲小文件,求出每個小文件中重複次數最多的,並記錄重複次數
  • 最後找出上一步求出的數據中重複次數最多的即爲所求
    ###千萬或上億數據(有重複),統計次數最多的前N個數據
  • 上千萬或上億的數據,現在的機器的內存應該能存下
  • 考慮採用HashMap/搜索二叉樹/紅黑樹等來進行統計次數
  • 最後利用堆取出前N個出現次數最多的數據
    ###一個文本文件,約一萬行,每行一個詞,統計出其中最頻繁的10個詞,給出思想及時間複雜度分析
    ####方案1
  • 如果文件較大,無法一次性讀入內存,可採用hash取模,將大文件分解爲多個小文件
  • 對於單個小文件利用HashMap統計出每個小文件中10個最常出現的詞
  • 然後歸併
  • 找出最終的10個最常出現的詞
       
    ####方案2
  • 通過hash取模將大文件分解爲多個小文件後
    -用trie樹統計每個詞出現的次數,時間複雜度O(n*le)(le:單詞平均長度),最終同樣找出出現最頻繁的前10個詞(可用堆來實現),時間複雜度是O(n*lg10)。

10. 1000萬字符串,其中有些是重複的,需要把重複的全部去掉,保留沒有重複的字符串。請怎麼設計和實現?

  • 方案1:這題用trie樹比較合適,hash_map也行。
  • 方案2:from xjbzju:,1000w的數據規模插入操作完全不現實,以前試過在stl下100w元素插入set中已經慢得不能忍受,覺得基於hash的實現不會比紅黑樹好太多,使用vector+sort+unique都要可行許多,建議還是先hash成小文件分開處理再綜合。

一個文本文件,找出前10個經常出現的詞,但這次文件比較長,說是上億行或十億行,總之無法一次讀入內存,問最優解

方案1:首先根據用hash並求模,將文件分解爲多個小文件,對於單個文件利用上題的方法求出每個文件件中10個最常出現的詞。然後再進行歸併處理,找出最終的10個最常出現的詞。
###100w個數中找出最大的100個數
####方案1:局部淘汰法

  • 取前100個元素,並排序,記爲序列L
  • 然後一次掃描剩餘的元素x,與排好序的100個元素中最小的元素比,如果比這個最小的要大,那麼把這個最小的元素刪除,並把x利用插入排序的思想,插入到序列L中。依次循環,知道掃描了所有的元素。複雜度爲O(100w100)。
    ####方案2
    快速排序的思想,每次分割之後只考慮比軸大的部分,知道比軸大的一部分在比100多的時候,採用傳統排序算法排序,取前100個。複雜度爲O(100w
    100)
    ####方案3
    在前面的題中,我們已經提到了,用一個含100個元素的最小堆完成。複雜度爲O(100w*lg100)。
    接下來看第二種方法,雙層桶劃分

    祕技二:雙層桶劃分

    一種算法設計思想。面對大量的數據我們無法處理時,可以將其分成一個個小任務,然後根據一定的策略來處理這些小任務,從而達到目的。

  • 適用場景
    第k大,中位數,不重複或重複的數字
  • 基本原理及要點
    因爲元素範圍很大,不能利用直接尋址表,所以通過多次劃分,逐步確定範圍,然後最後在一個可以接受的範圍內進行。可以通過多次縮小,雙層只是一個例子,分治纔是其根本(只是“只分不治”)。

【擴展】 當有時候需要用一個小範圍的數據來構造一個大數據,也是可以利用這種思想,相比之下不同的,只是其中的逆過程。

【問題實例】 1).2.5億個整數中找出不重複的整數的個數,內存空間不足以容納這2.5億個整數。

有點像鴿巢原理,整數個數爲2^32,也就是,我們可以將這2^32個數,劃分爲2^8個區域(比如用單個文件代表一個區域),然後將數據分離到不同的區域,然後不同的區域在利用bitmap就可以直接解決了。也就是說只要有足夠的磁盤空間,就可以很方便的解決。 當然這個題也可以用我們前面講過的BitMap方法解決,正所謂條條大道通羅馬~~~

2).5億個int找它們的中位數。

這個例子比上面那個更明顯。首先我們將int劃分爲2^16個區域,然後讀取數據統計落到各個區域裏的數的個數,之後我們根據統計結果就可以判斷中位數落到那個區域,同時知道這個區域中的第幾大數剛好是中位數。然後第二次掃描我們只統計落在這個區域中的那些數就可以了。

實際上,如果不是int是int64,我們可以經過3次這樣的劃分即可降低到可以接受的程度。即可以先將int64分成2^24個區域,然後確定區域的第幾 大數,在將該區域分成2^20個子區域,然後確定是子區域的第幾大數,然後子區域裏的數的個數只有2^20,就可以直接利用direct addr table進行統計了。

3).現在有一個0-30000的隨機數生成器。請根據這個隨機數生成器,設計一個抽獎範圍是0-350000×××中獎號碼列表,其中要包含20000箇中獎號碼。

這個題剛好和上面兩個思想相反,一個0到3萬的隨機數生成器要生成一個0到35萬的隨機數。那麼我們完全可以將0-35萬的區間分成35/3=12個區間,然後每個區間的長度都小於等於3萬,這樣我們就可以用題目給的隨機數生成器來生成了,然後再加上該區間的基數。那麼要每個區間生成多少個隨機數呢?計算公式就是:區間長度隨機數密度,在本題目中就是30000(20000/350000)。最後要注意一點,該題目是有隱含條件的:×××,這意味着你生成的隨機數裏面不能有重複,這也是我爲什麼用雙層桶劃分思想的另外一個原因。

其本質上還是分而治之思想,重在"分"

  • 適用範圍:第k大,中位數,不重複或重複的數字
  • 基本原理及要點:元素範圍很大,不能利用直接尋址表,所以多次劃分,逐步確定範圍,然後最後在一個可以接受的範圍內進行
    ##2.5億個整數中找出不重複的整數的個數,內存空間不足以容納這2.5億個整數
    整數個數爲2^32, 也就是,我們可以將這2^32個數,劃分爲2^8個區域(如用單個文件代表一個區域),然後將數據分離到不同的區域,然後不同的區域再利用bitmap(https://www.jianshu.com/p/a0294f9a321e)就可直接解決
    也就是說只要有足夠的磁盤空間,就可以很方便的解決。
    ##5億個int找它們的中位數
    ###思路一
  • 將int劃分爲2^16個區域
  • 讀取數據,統計落到各個區域裏的數的個數
  • 根據統計結果判斷中位數落到哪個區域,同時知道這個區域中的第幾大數剛好是中位數
  • 第二次掃描,只統計落在這個區域中的那些數即可

實際上,如果是long,我們可以經過3次這樣的劃分即可降低到可以接受的程度
即可以先將long分成2^24個區域,然後確定區域的第幾大數,在將該區域分成2^20個子區域,然後確定是子區域的第幾大數,然後子區域裏的數的個數只有2^20,就可以直接利用direct addr table進行統計了。
###思路二
同樣需要做兩遍統計,如果數據存在硬盤上,就需要讀取2次
方法同基排,開一個大小爲65536的Int數組,第一遍讀取,統計Int的高16位,也就是

  • 0-65535,都算作0
  • 65536 - 131071都算作1
    就相當於用該數除以65536
    Int除以 65536的結果不會超過65536種情況,因此開一個長度爲65536的數組計數即可
    每讀取一個數,數組中對應計數+1,考慮有負數的情況,需要將結果加32768(因爲只用一半)後,記錄在相應的數組內。

第一遍統計之後,遍歷數組累加,看中位數處於哪個區間
比如處於區間k,那麼0~k-1內數字的數量sum應該<n/2(2.5億)
而k+1 ~ 65535的計數和也<n/2
第二遍統計同上面方法,但這次只統計處於區間k的情況,也就是說(x / 65536) + 32768 = k。統計只統計低16位的情況。並且利用剛纔統計的sum,比如sum = 2.49億,那麼現在就是要在低16位裏面找100萬個數(2.5億-2.49億)。這次計數之後,再統計一下,看中位數所處的區間,最後將高位和低位組合一下就是結果
#祕技三:Bloom filter/Bitmap
##Bloom filter

  • 適用範圍
    可以用來實現數據字典,數據判重,集合求交集
  • 基本原理及要點
    對於原理來說很簡單,位數組+k個獨立hash函數。
    將Hash函數對應的值的位數組置1,查找時如果發現所有Hash函數對應位都是1說明存在
    很明顯這個過程並不保證查找的結果100%正確的。
    同時也不支持刪除一個已經插入的關鍵字,因爲該關鍵字對應的位會牽動到其他的關鍵字。
    所以一個簡單的改進就是 counting Bloom filter,用一個counter數組代替位數組,就可以支持刪除了
    Bloom filter將集合中的元素映射到位數組中,用k(哈希函數個數)個映射位是否全1表元素是否在該集合
    Counting bloom filter(CBF)將位數組中的每一位擴展爲一個counter,從而支持了元素的刪除操作。Spectral Bloom Filter(SBF)將其與集合元素的出現次數關聯。SBF採用counter中的最小值來近似表示元素的出現頻率。
    ##A,B兩個文件,各存放50億條URL,每條URL佔用64B,內存限制4G,求A,B文件URL交集。如果是三個乃至n個文件呢
  • 先計算下內存佔用,4G=2^32大概40億*8大概340億bit
    n=50億,若按出錯率0.01算需要大概650億bit
    現在可用340億,相差不多,可能會使出錯率上升
    另外如果這些url與ip是一一對應的,就可以轉換成ip,則大大簡單了

同時本題若允許有一定的錯誤率,可使用Bloom filter
將其中一個文件中的url使用Bloom filter映射爲340億bit,然後挨個讀取另外一個文件的url,檢查是否在Bloom filter,如果是,那麼該url應該是共同的url(注意會有一定的錯誤率)
#BitMap
用一個bit位標記某個元素對應的Value, 而Key即是該元素
由於採用了bit爲單位來存儲數據,因此在存儲空間方面,相對於 HashMap大大節省

看一個具體的例子,假設我們要對0-7內的5個元素(4,7,2,5,3)排序(假設這些元素沒有重複)。
要表示8個數,我們就只需要8個Bit(1Byte),首先我們開闢1Byte的空間,將這些空間的所有Bit位都置爲0
億萬級數據處理的高效解決方案
然後遍歷這5個元素,首先第一個元素是4,那麼就把4對應的位置爲1,因爲是從0開始的,所以要把第5位置1
億萬級數據處理的高效解決方案
然後遍歷一遍bit區域,將是1的位的編號輸出(2,3,4,5,7),就達到了排序的目的。下面的代碼給出了一個BitMap的用法:排序

//定義每個Byte中有8個Bit位
#include <memory.h>
#define BYTESIZE 8
void SetBit(char *p, int posi)
{
    for(int i=0; i < (posi/BYTESIZE); i++)
    {
        p++;
    }

    *p = *p|(0x01<<(posi%BYTESIZE));//將該Bit位賦值1
    return;
}

void BitMapSortDemo()
{
    //爲了簡單起見,我們不考慮負數
    int num[] = {3,5,2,10,6,12,8,14,9};

    //BufferLen這個值是根據待排序的數據中最大值確定的
    //待排序中的最大值是14,因此只需要2個Bytes(16個Bit)
    //就可以了。
    const int BufferLen = 2;
    char *pBuffer = new char[BufferLen];

    //要將所有的Bit位置爲0,否則結果不可預知。
    memset(pBuffer,0,BufferLen);
    for(int i=0;i<9;i++)
    {
        //首先將相應Bit位上置爲1
        SetBit(pBuffer,num[i]);
    }

    //輸出排序結果
    for(int i=0;i<BufferLen;i++)//每次處理一個字節(Byte)
    {
        for(int j=0;j<BYTESIZE;j++)//處理該字節中的每個Bit位
        {
            //判斷該位上是否是1,進行輸出,這裏的判斷比較笨。
            //首先得到該第j位的掩碼(0x01<<j),將內存區中的
            //位和此掩碼作與操作。最後判斷掩碼是否和處理後的
            //結果相同
            if((*pBuffer&amp;(0x01<<j)) == (0x01<<j))
            {
                printf("%d ",i*BYTESIZE + j);
            }
        }
        pBuffer++;
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    BitMapSortDemo();
    return 0;
}
  • 適用範圍
    可進行數據的快速查找,判重,刪除,一般來說數據範圍是int的10倍以下
  • 基本原理及要點
    使用bit數組來表示某些元素是否存在,比如8位電話號碼
  • 擴展
    Bloom filter可以看做是對BitMap的擴展
    ##已知某個文件內包含一些電話號碼,每個號碼爲8位數字,統計不同號碼的個數
    8位最多99 999 999,大概需要99m個bit,大概十幾M字節的內存即可(可理解爲從0~99 999 999的數字,每個數字對應一個bit位,所以只需要99M個bit約12.4M的Bytes,這樣就用了小小的12.4M左右的內存表示了所有的8位數的電話)
    ##在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題類似的方法,進行劃分小文件的方法。然後在小文件中找出不重複的整數,並排序。然後再進行歸併,注意去除重複的元素
##40億個不重複的非負int的整數,沒排過序,然後再給一個數,如何快速判斷這個數是否在那40億個數當中
申請512M內存,一個bit位代表一個int非負值。讀入40億個數,設置相應的bit位,讀入要查詢的數,查看相應bit位是否爲1,爲1表示存在,爲0表示不存在。

祕技四 Trie樹/數據庫/倒排索引

##Trie樹

  • 適用範圍
    數據量大,重複多,但數據種類少可放入內存
  • 基本原理及要點
    實現方式,節點孩子的表示方式
  • 擴展
    壓縮實現
    ###一個文本文件,大約一萬行,每行一個詞,要求統計出其中最頻繁出現的前10個詞
    用trie樹統計每個詞出現的次數,時間複雜度是O(n*le)(le表示單詞的平準長度),然後找出出現最頻繁的10個
    ##數據庫索引
  • 適用範圍
    大數據量的增刪改查
  • 基本原理及要點
    利用數據的設計實現方法,對海量數據的增刪改查
    ###倒排索引(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"將對應集合的交集。

  正向索引開發出來用來存儲每個文檔的單詞的列表。正向索引的查詢往往滿足每個文檔有序頻繁的全文查詢和每個單詞在校驗文檔中的驗證這樣的查詢。在正向索引中,文檔佔據了中心的位置,每個文檔指向了一個它所包含的索引項的序列。也就是說文檔指向了它包含的那些單詞,而反向索引則是單詞指向了包含它的文檔,很容易看到這個反向的關係。
  擴展:
  問題實例:文檔檢索系統,查詢那些文件包含了某單詞,比如常見的學術論文的關鍵字搜索。

祕技五 外排序

  • 適用範圍
    大數據的排序,去重
  • 基本原理及要點
    外排序的歸併方法,置換選擇敗者樹原理,最優歸併樹
    ##1G大小的一個文件,每一行一個詞,詞大小不超過16B,內存限制大小是1M。返回頻數最高的100詞
    這個數據具有很明顯的特點,詞的大小爲16B,但內存只有1M,做hash明顯不夠,所以可以用來排序。內存可以當輸入緩衝區使用。

    祕技六 MapReduce

    計算模型,簡單的說就是將大批量的工作(數據)分解(MAP)執行,然後再將結果合併成最終結果(REDUCE)。這樣做的好處是可以在任務被分解後,可以通過大量機器進行並行計算,減少整個操作的時間原理就是一個歸併排序。

  • 適用範圍
    數據量大,但是數據種類小可以放入內存
  • 基本原理及要點
    將數據交給不同的機器去處理,數據劃分,結果歸約給讀者看最後一道題,如下:

#非常大的文件,裝不進內存。每行一個int類型數據,現在要你隨機取100個數。
發現上述這道題,無論是以上任何一種模式/方法都不好做,那有什麼好的別的方法呢?我們可以看看:操作系統內存分頁系統設計(說白了,就是映射+建索引)。

Windows 2000使用基於分頁機制的虛擬內存。每個進程有4GB的虛擬地址空間。基於分頁機制,這4GB地址空間的一些部分被映射了物理內存,一些部分映射硬盤上的交換文 件,一些部分什麼也沒有映射。程序中使用的都是4GB地址空間中的虛擬地址。而訪問物理內存,需要使用物理地址。 關於什麼是物理地址和虛擬地址,請看:

  • 物理地址 (physical address): 放在尋址總線上的地址。放在尋址總線上,如果是讀,電路根據這個地址每位的值就將相應地址的物理內存中的數據放到數據總線中傳輸。如果是寫,電路根據這個 地址每位的值就將相應地址的物理內存中放入數據總線上的內容。物理內存是以字節(8位)爲單位編址的。 
  • 虛擬地址 (virtual address): 4G虛擬地址空間中的地址,程序中使用的都是虛擬地址。 使用了分頁機制之後,4G的地址空間被分成了固定大小的頁,每一頁或者被映射到物理內存,或者被映射到硬盤上的交換文件中,或者沒有映射任何東西。對於一 般程序來說,4G的地址空間,只有一小部分映射了物理內存,大片大片的部分是沒有映射任何東西。物理內存也被分頁,來映射地址空間。對於32bit的 Win2k,頁的大小是4K。CPU用來把虛擬地址轉換成物理地址的信息存放在叫做頁目錄和頁表的結構裏。 

    物理內存分頁,一個物理頁的大小爲4K字節,第0個物理頁從物理地址 0x00000000 處開始。由於頁的大小爲4KB,就是0x1000字節,所以第1頁從物理地址 0x00001000 處開始。第2頁從物理地址 0x00002000 處開始。可以看到由於頁的大小是4KB,所以只需要32bit的地址中高20bit來尋址物理頁。 

    返回上面我們的題目:非常大的文件,裝不進內存。每行一個int類型數據,現在要你隨機取100個數。針對此題,我們可以借鑑上述操作系統中內存分頁的設計方法,做出如下解決方案:

OS中的方法,先生成4G的地址表,在把這個表劃分爲小的4M的小文件做個索引,二級索引。30位前十位表示第幾個4M文件,後20位表示在這個4M文件的第幾個,等等,基於key value來設計存儲,用key來建索引。

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