大數據算法

大數據算法

參考:http://blog.csdn.net/hguisu/article/details/7856239
http://www.cnblogs.com/allensun/archive/2011/02/16/1956532.html
程序員代碼面試指南-第六章html

1、基本概念

  所謂海量,就是數據量很大,多是TB級別甚至是PB級別,致使沒法一次性載入內存或者沒法在較短期內處理完成。面對海量數據,咱們想到的最簡單方法便是分治法,即分開處理,大而化小,小而治之。咱們也能夠想到集羣分佈式處理。java

2、經常使用數據結構和算法

2.1 Bloom Filter

  即布隆過濾器,它能夠用於檢索一個元素是否在一個集合中。在垃圾郵件的黑白名單過濾、爬蟲(Crawler)的網址判重等中常常被用到。
  Bloom Filter(BF)是一種空間效率很高的隨機數據結構,它利用位數組很簡潔地表示一個集合,並能判斷一個元素是否屬於這個集合。它是一個判斷元素是否存在集合的快速的機率算法。Bloom Filter有可能會出現錯誤判斷,但不會漏掉判斷。也就是Bloom Filter判斷元素不在集合,那確定不在。若是判斷元素存在集合中,有必定的機率判斷錯誤。即:寧肯錯殺三千,毫不放過一個。所以,Bloom Filter不適合那些「零錯誤」的應用場合。而在能容忍低錯誤率的應用場合下,Bloom Filter相比其餘常見的算法(如hash,折半查找),極大的節省了空間。
  它的優勢是空間效率和查詢時間都優於通常的算法,缺點是有必定的誤識別率和刪除困難。
  add和query的時間複雜度都爲O(k),與集合中元素的多少無關,這是其餘數據結構都不能完成的。
  Bloom-Filter算法的核心思想就是利用多個獨立的Hash函數來解決「衝突」。Hash函數將一個元素(好比URL)映射到二進制位數組(位圖數組)中的某一位,若是該位已經被置爲1,只能說明該元素可能已經存在。由於Hash存在一個衝突(碰撞)的問題,即不一樣URL的Hash值有可能相同。爲了減小衝突,咱們能夠多引入幾個獨立的hash函數,若是經過其中的一個Hash值咱們得出某元素不在集合中,那麼該元素確定不在集合中。只有在全部的Hash函數告訴咱們該元素在集合中時,才能在很大機率上認爲該元素存在於集合中。
  原理要點:一是m位的bit數組, 二是k個獨立均勻分佈的hash函數,三是誤判機率p。程序員

  • m bits的bit數組:使用一個m比特的數組來保存信息,每個bit位都初始化爲0
  • k個獨立均勻分佈的hash函數:爲了添加一個元素,用k個hash函數將它hash獲得bloom filter中k個bit位,將這k個bit位置1(超過m的取餘%m)。
  • 誤判機率p:爲了查詢一個元素,即判斷它是否在集合中,用k個hash函數將它hash獲得k個bit位。若這k bits全爲1,則此元素以機率(1-p)在集合中;若其中任一位不爲1,則此元素必不在集合中(由於若是在,則在添加時已經把對應的k個bits位置爲1)。

  不容許移除元素,由於那樣的話會把相應的k個bits位全置爲0,而其中頗有可能有其餘元素對應的位。所以remove會引發誤報,這是絕對不被容許的。而刪除元素其實能夠經過引入白名單解決。
  當k很大時,設計k個獨立的hash function是不現實而且困難的。對於一個輸出範圍很大的hash function(例如MD5產生的128 bits數),若是不一樣bit位的相關性很小,則可把此輸出分割爲k份。或者可將k個不一樣的初始值(例如0,1,2, … ,k-1)結合元素,賦值給一個hash 函數從而產生k個不一樣的數。
  當add的元素過多時,即n/m過大時(n是元素數,m是bloom filter的bits數),會致使false positive(誤判)太高,此時就須要從新組建filter,但這種狀況相對少見。
  若元素總數爲n,誤判率爲p,則:
布隆過濾器的大小:

hash函數的個數:

誤判率p與m和n的關係:

舉個例子,咱們假設錯誤率爲p=0.01,則此時m大概是n的13倍,k大概是8個。
  這裏m與n的單位不一樣,m是bit爲單位,而n則是以元素個數爲單位(準確的說是不一樣元素的個數)。一般單個元素的長度都是有不少bit的,因此使用bloom filter內存上一般都是節省的。面試

誤判機率的證實和計算
  假設布隆過濾器中的hash function知足獨立均勻分佈地的假設:每一個元素都等機率地hash到m個bit中的任何一個,與其它元素被hash到哪一個bit無關。那麼對某一個bit位來講,一個輸入對象在被k個hash function散列後,這個位置依然爲0的機率爲:

通過n個輸入對象後,這個位置依然未被置1的機率爲:

該位置被置1的機率:

那麼在檢查階段,若對應某個待query元素的k bits所有置位爲1,則可斷定其在集合中。所以將某元素誤判的機率爲:
算法

因爲,而且當m很大時趨近於0,因此:
sql

如今計算對於給定的m和n,k爲什麼值時可使得誤判率最低。設誤判率爲k的函數爲:

, 則簡化爲,兩邊取對數:

, 兩邊對k求導:

下面求最值:








代入
\(a^{log_a^N}=N\),並兩邊取對數獲得:
數據庫

Bloom-Filter的應用
一、 key-value 加快查詢
通常key-value存儲系統的values存在硬盤,查詢就是件費時的事。將Storage的數據都插入Filter,在Filter中查詢都不存在時,那就不須要去Storage查詢了。當False Position出現時,只是會致使一次多餘的Storage查詢。
因爲Bloom-Filter所用的空間很是小,全部BF能夠常駐內存。這樣子的話,對於大部分不存在的元素,咱們只須要訪問內存中的Bloom-Filter就能夠判斷出來了,只有一小部分,咱們須要訪問在硬盤上的key-value數據庫,從而大大地提升了效率。編程

二、垃圾郵件地址過濾
像網易,QQ這樣的公衆電子郵件(email)提供商,老是須要過濾來自發送垃圾郵件的人(spamer)的垃圾郵件。數組

一個辦法就是記錄下那些發垃圾郵件的 email地址。因爲那些發送者不停地在註冊新的地址,全世界少說也有幾十億個發垃圾郵件的地址,將他們都存起來則須要大量的網絡服務器。緩存

若是用哈希表,每存儲一億個 email地址,就須要 1.6GB的內存(用哈希表實現的具體辦法是將每個 email地址對應成一個八字節的信息指紋,而後將這些信息指紋存入哈希表,因爲哈希表的存儲效率通常只有 50%,所以一個 email地址須要佔用十六個字節。一億個地址大約要 1.6GB,即十六億字節的內存)。所以存貯幾十億個郵件地址可能須要上百 GB的內存。

而Bloom Filter只須要哈希表 1/8到 1/4 的大小就能解決一樣的問題。

BloomFilter決不會漏掉任何一個在黑名單中的可疑地址。而至於誤判問題,常見的補救辦法是在創建一個小的白名單,存儲那些可能被誤判的郵件地址。

2.2 Hash

  Hash,通常翻譯作「散列」,也有直接音譯爲「哈希」的,就是把一個對象的關鍵字,經過散列算法,映射到一個固定長度的數組中。當咱們想要找到對應的對象時,只須要根據它的關鍵字在散列表中查找(再計算一次散列值)。這種轉換是一種壓縮映射,也就是,散列值的空間一般遠小於輸入的空間,不一樣的輸入可能會散列成相同的輸出,而不可能從散列值來惟一的肯定輸入值。輸入叫作關鍵字,輸出叫作散列值或者哈希地址(僅僅是在散列表中的地址)。但一般須要總數據量能夠放入內存。
   散列表是具備固定大小的數組,其中,表長(即數組的大小)應該爲質數。
   衝突:兩個不一樣的輸入計算出了相同的散列值。
散列函數通常應具有如下幾個特色:
運算簡單;函數的值域必須在散列表內;儘量減小衝突;不一樣輸入得到的散列值儘可能均勻分散。

經常使用的散列函數:

  • 直接定址法: 是以數據元素關鍵字k自己或它的線性函數做爲它的哈希地址,即:hash(k)=k 或 hash(k)=a*k+b ; (其中a,b爲常數)。此法僅適合於:地址集合的大小 = = 關鍵字集合的大小,好比以年齡爲key,以年齡對應的人數爲value
  • 數字分析法:假設關鍵字集合中的每一個關鍵字都是由 s 位數字組成 (u1, u2, …, us),分析關鍵字集中的全體,並從中提取分佈均勻的若干位或它們的組合做爲地址。它只適合於全部關鍵字值已知的狀況。
  • 摺疊法: 將關鍵字分割成若干部分,而後取它們的疊加和,留下t位做爲哈希地址。適用於關鍵字位數較多,並且關鍵字中每一位上數字分佈大體均勻的狀況。
  • 平方取中法: 這是一種經常使用的哈希函數構造方法。這個方法是先取關鍵字的平方,而後根據可以使用空間的大小,選取平方數是中間幾位爲哈希地址。
  • 減去法:數據的鍵值減去一個特定的數值以求得數據存儲的位置。
  • 除留餘數法:這是一種經常使用的哈希函數構造方法。假設哈希表長爲m,p爲小於等於m的最大素數,則哈希函數爲hash(k)=k % p ,其中%爲模p取餘運算。

Hash處理衝突方法:

  • 開放定址法:這種方法也稱探測散列法,其基本思想是:當關鍵字key的哈希地址p=hash(key)出現衝突時,以p爲基礎,產生另外一個哈希地址p1,若是p1仍然衝突,再以p1爲基礎,產生另外一個哈希地址p2,…,直到找出一個不衝突的哈希地址pi ,將相應元素存入其中。這種方法有一個通用的再散列函數形式:
    Hi=(hash(key)+f(i))% m i=1,2,…,n
      其中hash(key)爲關鍵字key的直接散列地址,m 爲表長,f(i)稱爲增量序列,表示每次再探測試時的地址增量。

這種方法如何查找元素x:
  好比咱們採用線性探測法,地址增量f(i)=2。其實就是按照計算hash位置的方法進行查找。首先計算hash(x),若是這裏有元素且不爲x,則嘗試hash(x)+2;若是這裏有元素且不爲x,則嘗試hash(x)+2+2,一直到查找到的位置爲null或者等於元素x。

  • 再散列法:首先構造長度爲length1的散列表table1,而後利用hash1(key)計算須要插入的元素的散列值。當散列表快要被插滿時(好比達到了必定的裝填因子,或者當插入失敗時),再構造一個長度爲length2=1+2*length1(實際不必定是2倍+1)的散列表table2,並將table1的元素按照hash2(key)散列到新表中,持續這樣的過程...。

  • 鏈地址法:基本思想是散列地址i保存一個單鏈表的頭指針,全部散列值爲i的元素都保存在i對應的單鏈表中,於是查找、插入和刪除主要在單鏈表中進行。鏈地址法適用於常常進行插入和刪除或者衝突比較嚴重的狀況。例如,已知一組關鍵字(32,40,36,53,16,46,71,27,42,24,49,64),哈希表長度爲13,哈希函數爲:H(key)= key % 13,則用鏈地址法處理衝突的結果如圖:

    本例的平均查找長度 ASL=(17+24+3*1)=1.5

  • 創建公共溢出區:將哈希表分爲基本表和溢出表兩部分,凡是和基本表發生衝突的元素,一概填入溢出表

Hash的應用
一、哈希對於檢測數據對象(例如消息)中的修改頗有用。好的哈希算法使得構造兩個相互獨立且具備相同哈希的輸入不能經過計算方法實現。典型的哈希算法包括MD5 和 SHA-1。
二、海量日誌數據分析,好比提取出某日訪問百度次數最多的那個IP。

2.3 Bit-Map(位圖)

  Bit-map法的基本原理是:使用位數組來表示某些元素是否存在,每個bit位能夠標記一個元素對應的Value。
  假設咱們要對0-7內的5個元素(4,7,2,5,3)排序(這裏假設這些元素沒有重複)。那麼咱們就能夠採用Bit-map的方法來達到排序的目的。要表示8個數,咱們就只須要8個bit(1Bytes),首先咱們開闢1Byte的空間,將這些空間的全部bit位都置爲0,以下圖:

而後遍歷這5個元素,首先第一個元素是4,那麼就把4對應的位置爲1,由於是從零開始的,因此要把第五位置爲一(以下圖):

而後再處理第二個元素7,將第八位置爲1,,接着再處理第三個元素,一直到最後處理完全部的元素,將相應的位置爲1,這時候的內存的bit位的狀態以下:

而後咱們遍歷一遍bit區域,將值爲1的位的編號輸出(2,3,4,5,7),這樣就達到了排序的目的。

對於排序,優勢是:運算效率高,不需進行比較和移位,時間複雜度是O(n);佔用內存少,好比N=10000000;只需佔用內存爲N/8=1250000Byte=1.25M。
      缺點:數據最好是惆集數據(否則空間浪費很大);數據不可重複(會將重複的數據覆蓋掉)

位圖的應用
一、判斷集合中是否存在重複:
好比:
若集合大小爲N,首先掃描一遍集合,找到集合中的最大元素max,而後建立一個長度爲max+1的bit數組。接着再次掃描原集合,每遇到一個元素,就將新數組中下標爲元素值的位置爲1,好比,若是遇到元素5,則將新數組的第6個元素置爲1,如此下去,當下次再遇到元素5想置位時,發現新數組的第6個元素已經被置爲1了,則這個元素必定重複了。該算法的最壞運算次數2N,但若是可以事先知道集合的最大元素值,則運算次數N。

再好比:已知某個文件內包含一些電話號碼,每一個號碼爲8位數字,統計不一樣號碼的個數。

8位最多99 999 999,大概須要99m個bit,大概10幾m字節的內存便可。 (能夠理解爲從0-99 999 999的數字,每一個數字對應一個bit位,因此只須要99M個bit=12MB多,這樣,就用了小小的12M多左右的內存表示了全部的8位數的電話)

二、判斷集合中某個元素是否存在:
好比:給你一個文件,裏面包含40億個非負整數,寫一個算法找出該文件中不包含的一個整數, 假設你有1GB內存可用。若是你只有10MB的內存呢?
   32位無符號整數的範圍是0~4294967295(即2 x 2147483647+1),所以能夠申請一個長度爲4294967295的bit數組bitArr,bitArr的每一個位置只能夠表示0或者1。8個bit爲1B,因此長度爲4294967295的數組佔用內存:40*10^8bit=0.5GB=500MB。
   而後遍歷這40億個無符號數,例如,遇到7000,就把bitArr[7000]置爲1。遍歷數字完成後,遍歷bitArr,哪一個位置的值爲0,哪一個數就不在這40億個數內。

如今咱們來看若是內存要求是10MB呢?
   咱們能夠將全部0~4294967295的數據平均分紅64個區間,每一個區間保存67108864個數,好比:第0區間[0~67108863],第1區間[67108864~134217727],第i區間爲[67108864i~67108864(i+1)-1]...,實際上咱們並不保存這些數,而是給每個區間設置一個計數器。這樣每讀入一個數,咱們就在它所在的區間對應的計數器加1。處理結束以後, 咱們找到一個區間,它的計數器值小於區間大小(67108864), 說明了這一段裏面必定有數字是文件中所不包含的。而後咱們單獨處理這個區間便可。接下來咱們就能夠用Bit Map算法了。申請長度爲67108864的bitArr,記爲bitArr[0..67108863],咱們再遍歷一遍數據, 把落在這個區間的數對應的位置1(固然數據要通過處理,即落在區間i的數num,則bitArr[num-67108864*i]=1)。 最後咱們找到這個區間中第一個爲0的位,其對應的數就是一個沒有出如今該文件中的數。

再好比:
2.5億個整數中找出不重複的整數的個數,內存空間不足以容納這2.5億個整數。
將bit-map擴展一下,用2bit表示一個數便可,00表示未出現,01表示出現一次,10表示出現2次及以上,在遍歷這些數的時候,若是對應位置的值是00,則將其置爲01;若是是01,將其置爲10;若是是10,則保持不變。或者咱們不用2bit來進行表示,咱們用兩個bit-map便可模擬實現這個2bit-map,都是同樣的道理。

2.4 堆(Heap)

   實現能夠看排序算法
   概念:堆是一種特殊的二叉樹,具有如下兩種性質
1)每一個節點的值都大於等於(或者都小於等於,稱爲小頂堆或最小堆)其子節點的值,稱爲大頂堆(或最大堆)。
2)樹是徹底二叉樹,而且最後一層的樹葉都在最左邊

一個典型的堆結構:

   插入和刪除的時間複雜度O(logn)
適用範圍
   海量數據前n大(最小堆,不用最大堆是爲了保證咱們只操做堆頂元素)或者前n小(最大堆)或者中位數(雙堆,一個最大堆與一個最小堆結合),而且n比較小,堆能夠放入內存。

   好比海量數據求前n小,利用大頂堆,咱們比較當前元素與最大堆裏的最大元素(即堆頂元素),若是它小於最大元素,則應該替換那個最大元素,並調整堆的結構,持續這一過程,最後獲得的n個元素就是最小的n個,這樣能夠掃描一遍便可獲得全部的前n小元素,效率很高,具體算法:找到無序數組中最小的k個數
   好比海量數據求前n大,利用小頂堆,咱們比較當前元素與最小堆裏的最小元素(即堆頂元素),若是它大於最小元素,則應該替換那個最小元素,並調整堆的結構,持續這一過程,最後獲得的n個元素就是最大的n個。
   雙堆求中位數:
一、建立兩個堆(一個大頂堆、一個小頂堆),並設置兩個變量分別記錄兩個堆的元素的個數;
二、假定變量mid用來保存中位數,取第一個元素,賦值給mid,做爲初始的中位數;
三、依次遍歷後面的每個數據,若是比mid小,則插入大頂堆;不然插入小頂堆,並調整堆的結構;
四、若是大頂堆和小頂堆上的數據之差的絕對值爲2,則將mid插入到元素個數較少的堆中,而後從元素個數較多的堆中刪除根節點,並將根節點賦值給mid;
五、重複步驟3和4,直到全部的數據遍歷結束;

  此時,mid保存了一個數,再加上兩個堆中保存的數,就構成了給定數據的集合。
  若是兩個堆中元素個數相等,則mid即爲最終的中位數;不然,元素較多的堆的根節點元素與mid的和求平均值,即爲最終的中位數。時間複雜度:nlog(n)

2.5 雙層桶劃分

  雙層桶劃分不是一種數據結構,而是一種算法設計思想,相似於分治思想。面對大量的數據咱們沒法處理的時候,能夠將其分紅一個個小的單元,而後根據必定的策略來處理這些小單元,從而達到目的。
  基本原理及要點:由於元素範圍很大,不能利用直接尋址表,因此經過屢次劃分,逐步肯定範圍,最後在一個能夠接受的範圍內進行操做。能夠經過屢次縮小,雙層只是一個形式,分治纔是其根本(只是「只分不治」)。
  常規方法:把大文件經過哈希函數分配到不一樣的機器,或者經過哈希函數把大文件拆成小文件,一直進行這種劃分,直到劃分的結果知足資源限制的要求(即分流)。
  適用範圍:
  第k大,中位數,不重複或重複的數字

2.6 數據庫優化法

2.6.1 索引

  Mysql索引
  索引是對數據庫表中一列或多列的值進行排序的一種結構,使用索引可快速訪問數據庫表中的特定信息。
  數據庫索引比如是一本書前面的目錄,能加快數據庫的查詢速度。
  例如這樣一個查詢:select * from table1 where id=44。若是沒有索引,必須遍歷整個表,直到ID等於44的這一行被找到爲止;有了索引以後(必須是在ID這一列上創建的索引),直接在索引裏面找44(也就是在ID這一列找),就能夠得知這一行的位置,也就是找到了這一行,可見,索引是用來定位的。
  優勢:
    第一,經過建立惟一性索引,能夠保證數據庫表中每一行數據的惟一性。
    第二,能夠大大加快數據的檢索速度,這也是建立索引的最主要的緣由。
    第三,能夠加速表和表之間的鏈接(快速查詢到須要鏈接的數據行),特別是在實現數據的參考完整性方面特別有意義。
    第四,在使用分組和排序子句進行數據檢索時,一樣能夠顯著減小查詢中分組和排序的時間。
    第五,經過使用索引,能夠在查詢的過程當中,使用優化隱藏器,提升系統的性能。
  缺點:
    第一,建立和維護索引要耗費時間,這種時間隨着數據量的增長而增長。
    第二,增長了數據庫的存儲空間。
    第三,當對錶中的數據進行增長、刪除和修改的時候,索引也要動態的維護,這樣就下降了數據的維護速度。

通常來講,應該在這些列上建立索引:

  • 在常常須要搜索的列上,能夠加快搜索的速度;
  • 在做爲主鍵的列上,強制該列的惟一性和組織表中數據的排列結構;
  • 在常常用在鏈接的列上,這些列主要是一些外鍵,能夠加快鏈接的速度;
  • 在常常須要根據範圍進行搜索或者須要排序的列上建立索引,由於索引已經排序,其指定的範圍是連續的;
  • 在常用WHERE子句的列上面建立索引,加快條件的判斷速度。

2.6.2 緩存機制

  配置緩存能夠有效的下降數據庫查詢讀取次數,從而緩解數據庫服務器壓力。

2.6.3 數據分區

  對海量數據進行分區,以下降須要處理的數據規模。好比,針對按年存取的數據,能夠按年進行分區。

2.6.4 切表

  分表包括兩種方式:橫向分表和縱向分表,其中,橫向分表比較有使用意義,故名思議,橫向切表就是指把記錄分到不一樣的表中,而每條記錄仍舊是完整的(縱向切表後每條記錄是不完整的),例如原始表中有100條記錄,我要切成2個表,那麼最簡單也是最經常使用的方法就是ID取摸切表法,本例中,就把ID爲1,3,5,7。。。的記錄存在一個表中,ID爲2,4,6,8,。。。的記錄存在另外一張表中。雖然橫向切表能夠減小查詢強度,可是它也破壞了原始表的完整性,若是該表的統計操做比較多,那麼就不適合橫向切表。橫向切表有個很是典型的用法,就是每一個用戶的用戶數據通常都比較龐大,可是每一個用戶數據之間的關係不大,所以這裏很適合橫向切表。最後,要記住一句話就是:分表會形成查詢的負擔,所以在數據庫設計之初,要想好是否真的適合切表的優化。

2.6.5 分批處理

  對海量數據進行分批處理,再對處理後的數據進行合併操做,分而治之。

2.6.6 用排序來取代非順序存取

  磁盤存取臂的來回移動使得非順序磁盤存取變成了最慢的操做,儘可能保證存取數據的有序性,保證數據良好的局部性

2.6.7 日誌分析

  在數據庫運行了較長一段時間之後,會積累大量的LOG日誌,其實這裏面的蘊涵的有用信息仍是不少的。經過分析日誌,能夠找到系統性能的瓶頸,從而進一步尋找優化方案。

2.7 倒排索引(搜索引擎之基石)

  倒排索引(英語:Inverted index),也常被稱爲反向索引、置入檔案或反向檔案,被用來存儲在全文搜索下某個單詞在一個文檔或者一組文檔中的存儲位置的映射。它是目前搜索引擎公司對搜索引擎最經常使用的存儲方式。

有兩種不一樣的反向索引形式:

  • 一條記錄的水平反向索引:包含每一個引用單詞的文檔的列表。
  • 一個單詞的水平反向索引:包含單詞的文檔列表和每一個單詞在一個文檔中的位置。

後者的形式提供了更多的兼容性(好比短語搜索),可是須要更多的時間和空間來建立。

例如如今咱們要對三篇文檔創建索引(實際應用中,文檔的數量是海量的):
文檔1(D1):中國移動互聯網發展迅速
文檔2(D2):移動互聯網將來的潛力巨大
文檔3(D3):中華民族是個勤勞的民族
那麼文檔中的詞典集合爲:{中國,移動,互聯網,發展,迅速,將來,的,潛力,巨大,中華,民族,是,個,勤勞}
建好的索引以下圖:

在上面的索引中,存儲了兩個信息,文檔號和出現的次數。創建好索引之後,咱們就能夠開始查詢了。例如如今有一個Query是」中國移動」。首先分詞獲得Term集合{中國,移動},查倒排索引,分別計算query和d1,d2,d3的距離。倒排索引創建好之後,就不須要再檢索整個文檔庫,而是直接從字典集合中找到「中國」和「移動」,而後遍歷後面的列表直接計算。

2.8 外排序

  當待排序的對象數目特別多時,在內存中不能一次處理,必須把它們以文件的形式存放於外存,排序時再把他們一部分一部分的調入內存進行處理,這種方式就是外排序法。
  基本原理及要點:
外部排序的兩個獨立階段:
1)首先按內存大小,將外存上含n個記錄的文件分紅若干長度爲L的子文件或段。依次將子文件讀入內存並利用有效的內部排序對他們進行排序,並將排序後獲得的有序子文件從新寫入外存,一般稱這些子文件爲歸併段。
2)對這些歸併段進行逐趟歸併,使歸併段逐漸由小到大,最後在外存上造成整個文件的單一歸併段,也就完成了文件的外排序。
  適用範圍:
  大數據的排序,去重

一個典型實現:
假設文件被分紅L段子文件,須要將全部數據從大到小進行排序(即先將最大的輸出,再輸出第二大的,直到全部元素的順序被得到)。
(1)依次讀入每一個文件塊,在內存中對當前文件塊進行排序(應用恰當的內排序算法),並將排序後的結果直接寫入外存文件(分別寫到不一樣的子文件)。此時,每塊文件至關於一個由大到小排列的有序隊列。
(2)接下來進行多路歸併排序,在內存中創建一個L個元素的大頂堆(注意這裏的要求不只僅是要得到topK,還要按照從大到小的排序輸出,因此沒使用小頂堆),建堆的過程就是把L塊文件中每一個文件的隊列頭(每一個文件的最大值)依次加入到堆裏,並調整成大頂堆。
(3)彈出堆頂元素,若是堆頂元素來自第i塊(怎麼知道堆頂元素來自哪一塊?能夠在內存中創建一個hashMap,以第幾個子文件爲key,以最近一個加入的元素爲value,每次新加入元素則更新value),則從第i塊文件中補充一個元素到大頂堆,並調整大頂堆結構。彈出的元素暫存至臨時數組。
(4)當臨時數組存滿時,將數組寫至磁盤,並清空數組內容。
(5)重複過程(3)、(4),直至全部文件塊讀取完畢。

2.9 Trie 樹

讀音:[t'ri:]
  基本原理及要點:
  Trie樹也稱字典樹,它的優勢是:利用字符串的公共前綴來下降存儲的空間開銷和查詢的時間開銷。 在字符串查找、統計、排序、前綴匹配等方面應用很普遍。
  它有3個基本性質:

  • 根節點不包含字符,除根節點外每個節點都只包含一個字符。
  • 從根節點到某一節點,路徑上通過的字符鏈接起來,爲該節點對應的字符串。
  • 每一個節點的全部子節點包含的字符都不相同。

好比:字符串abc,bd,dda

  適用範圍:
  數據量大,重複多,可是數據種類小能夠放入內存

問題實例:
1).有10個文件,每一個文件1G, 每一個文件的每一行都存放的是用戶的query,每一個文件的query均可能重複。要你按照query的頻度排序 。
2).1000萬字符串,其中有些是相同的(重複),須要把重複的所有去掉,保留沒有重複的字符串。請問怎麼設計和實現?
3).尋找熱門查詢:查詢串的重複度比較高,雖然總數是1千萬,但若是除去重複後,不超過3百萬個,每一個不超過255字節。

實現代碼,大量遞歸的應用:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Trie {
    private TrieNode root = new TrieNode();

    protected class TrieNode {
        protected int words; //以此字符爲結尾的單詞的個數
        protected int prefixes; //以此字符(包括此字符)的全部祖先字符組成的字符串爲前綴的單詞個數
        protected TrieNode[] childNodes; //此節點的全部子節點

        public TrieNode() {
            this.words = 0;
            this.prefixes = 0;
            childNodes = new TrieNode[26];
            for (int i = 0; i < childNodes.length; i++) {
                childNodes[i] = null;
            }
        }
    }

    /**
     * 獲取tire樹中全部的詞
     */
    public List<String> listAllWords() {
        List<String> words = new ArrayList<String>();
        TrieNode[] childNodes = root.childNodes;

        for (int i = 0; i < childNodes.length; i++) {
            if (childNodes[i] != null) {
                String word = "" + (char) ('a' + i);
                depthFirstSearchWords(words, childNodes[i], word);
            }
        }
        return words;
    }

    private void depthFirstSearchWords(List<String> words, TrieNode trieNode, String wordSegment) {
        if (trieNode.words != 0) {
            words.add(wordSegment);
        }
        TrieNode[] childNodes = trieNode.childNodes;
        for (int i = 0; i < childNodes.length; i++) {
            if (childNodes[i] != null) {
                String newWord = wordSegment + (char) ('a' + i);
                depthFirstSearchWords(words, childNodes[i], newWord);
            }
        }
    }

    /**
     * 計算指定前綴單詞的個數
     */
    public int countPrefixes(String prefix) {
        return countPrefixes(root, prefix);
    }

    private int countPrefixes(TrieNode trieNode, String prefixSegment) {
        if (prefixSegment.length() == 0) { 
            return trieNode.prefixes;
        }

        char c = prefixSegment.charAt(0);
        int index = c - 'a';
        if (trieNode.childNodes[index] == null) {
            return 0;
        } else {
            return countPrefixes(trieNode.childNodes[index], prefixSegment.substring(1));
        }
    }

    /**
     * 計算徹底匹配單詞的個數
     */
    public int countWords(String word) {
        return countWords(root, word);
    }

    private int countWords(TrieNode trieNode, String wordSegment) {
        if (wordSegment.length() == 0) {
            return trieNode.words;
        }

        char c = wordSegment.charAt(0);
        int index = c - 'a';
        if (trieNode.childNodes[index] == null) {
            return 0;
        } else {
            return countWords(trieNode.childNodes[index], wordSegment.substring(1));
        }
    }

    /**
     * 向tire樹添加一個詞
     */
    public void addWord(String word) {
        addWord(root, word);
    }

    private void addWord(TrieNode trieNode, String word) {
        if (word.length() == 0) {
            trieNode.words++;
        } else {
            trieNode.prefixes++;
            char c = word.charAt(0);
            c = Character.toLowerCase(c);
            int index = c - 'a';
            if (trieNode.childNodes[index] == null) {
                trieNode.childNodes[index] = new TrieNode();
            }
            addWord(trieNode.childNodes[index], word.substring(1)); 
        }
    }

    /**
     * 返回指定字段前綴匹配最長的單詞。
     */
    public String getMaxMatchWord(String word) {
        String s = "";
        String temp = "";// 記錄最近一次匹配最長的單詞
        char[] w = word.toCharArray();
        TrieNode trieNode = root;
        for (int i = 0; i < w.length; i++) {
            char c = w[i];
            c = Character.toLowerCase(c);
            int index = c - 'a';
            if (trieNode.childNodes[index] == null) {// 若是沒有子節點
                if (trieNode.words != 0)// 若是是一個單詞,則返回
                    return s;
                else
                    // 若是不是一個單詞則返回null
                    return null;
            } else {
                if (trieNode.words != 0){
                    temp = s;
                }
                s += c;
                trieNode = trieNode.childNodes[index];
            }
        }
        // trie中存在比指定單詞更長(包含指定詞)的單詞
        if (trieNode.words == 0)//
            return temp;
        return s;
    }

    public static void main(String args[]){
        Trie trie = new Trie();
        trie.addWord("abcedfddddddd");
        trie.addWord("a");
        trie.addWord("ba");
        trie.addWord("abce");
        trie.addWord("abce");
        trie.addWord("abcedfdddd");
        trie.addWord("abcef");

        String maxMatch = trie.getMaxMatchWord("abcedfddd");
        System.out.println("最大前綴匹配的單詞:"+maxMatch);
        List<String> list = trie.listAllWords();
        Iterator<String> listiterator = list.listIterator();
        System.out.println("全部字符串列表:");
        while (listiterator.hasNext()) {
            String s = (String) listiterator.next();
            System.out.println(s);
        }

        int count = trie.countPrefixes("ab");
        int count1 = trie.countWords("abce");
        System.out.println("以ab爲前綴的單詞數:"+ count);
        System.out.println("單詞abce的個數爲:" + count1);
    }
}

2.10 分佈式處理 MapReduce

雲計算的核心技術之一,是一種簡化並行計算的分佈式編程模型。
基本原理及要點:
其核心操做爲Map和Reduce。Map(映射):將數據經過 Map程序映射到不一樣的區塊。Reduce(化簡):將不一樣的區塊劃分到不一樣的機器上進行並行處理。最後再對每臺機器上的結果進行整合。即數據劃分,結果規約。

MapReduce實例:上千萬或億數據,統計其中出現次數最多的前N個數據。
講解:首先能夠根據數據值或者把數據hash後的值,按照範圍劃分到不一樣的子機器,最好讓數據劃分後能夠一次性讀入內存,這樣不一樣的子機器負責處理各自的數值範圍。獲得結果後,各個子機器只需拿出各自的出現次數最多的前N個數據,而後彙總,選出全部的數據中出現次數最多的前N個數據。

  適用範圍:
  數據量很大(一般大於1TB)

3、經典題目

3.1 top K問題

  海量數據中找出出現頻度最高的前K的數或者字符串,或者海量數據中找出最大的前K個數。

  如何選擇hash函數分流:
一、能夠根據數據值或者把數據hash(md5)後的值,按照範圍劃分到不一樣的子集,可是計算md5代價是比較高的,能夠考慮其餘比較簡單的hash。通常應用查詢的是字符串,關於String的hash函數,因爲字符串計算hashCode相似於31進制轉10進制,所以7個左右的字符(接近2^52^52^5...,不用32的緣由是最後算的值有效位太少)就能夠達到最大int值,而後利用hashCode的低16位和高16位異或,可見自學Java HashMap源碼,再%表長,能夠有效分流。
二、直接hash(url)%1000這種形式說明,若是分流後仍是一些集合很大,則再次分流。

  針對top K類問題,一般比較好的方案是分治+Trie樹/HashMap+小頂堆,即hash映射分流 + hashMap統計 + 小頂堆求topK,即先將數據集按照Hash映射分解成多個小數據集,而後使用Trie樹或者HashMap統計每一個小數據集中的query詞頻,以後用小頂堆求出每一個數據集中出現頻率最高的前K個數,最後在全部top K中求出最終的top K。固然也能夠:分流到不一樣機器+Trie樹/HashMap+小頂堆,即先將數據集按照Hash方法分解成多個小數據集,而後分流到不一樣的機器上,具體多少臺機器由面試官限制決定。對每一臺機器,若是分到的數據量依然很大,好比內存不夠或者其餘問題,能夠再用hash函數把每臺機器的分流文件拆成更小的文件處理。而後使用Trie樹或者Hash統計每一個小數據集中的query詞頻,以後用小頂堆求出每一個數據集中出現頻率最高的前K個數,最後在全部top K中求出最終的top K。

eg:有1億個浮點數,若是找出其中最大的10000個?
  第一種是將數據所有排序,而後在排序後的集合中進行查找,最快的排序算法的時間複雜度通常爲O(nlogn),如快速排序。可是在32位的機器上,每一個float類型佔4個字節,1億個浮點數就要佔用(4B*10^8=0.4GB)400MB的存儲空間,對於一些可用內存小於400M的計算機而言,很顯然是不能一次將所有數據讀入內存進行排序的。其實即便內存可以知足要求,該方法也並不高效,由於題目的目的是尋找出最大的10000個數便可,而排序倒是將全部的元素都排序了,作了不少的無用功。

  第二種方法是分治+快排變體法,將1億個數據分紅100份,每份100萬個數據,找到每份數據中最大的10000個,最後在剩下的100 * 10000個數據裏面找出最大的10000個。若是100萬數據選擇足夠理想,那麼能夠過濾掉1億數據裏面99%的數據。100萬個數據裏面查找最大的10000個數據的方法以下:用快速排序的方法,首先,隨便選一箇中軸,比他大的數據放到右邊,比他小的放到左邊,若是大的那堆個數N大於10000個,繼續對大堆快速排序一次分紅2堆。。。若是大堆個數N小於10000個,就在小的那堆裏面快速排序一次,找第10000-N大的數字;遞歸以上過程,就能夠找到第10000大的數(利用快排的變體找到第k大,平均時間複雜度O(N),能夠看找到無序數組中第k個最小的數),而後一遍遍歷就能夠找到前10000大。此種方法須要每次的內存空間爲\(10^6*4B=4MB\),一共須要101次這樣的比較。

  第三種方法是Hash去重+最小堆法。若是這1億個數裏面有不少重複的數,先經過hashMap,把這1億個數字去重複,這樣若是重複率很高的話,會減小很大的內存用量,從而縮小運算空間,而後經過分治+最小堆法或直接最小堆法查找最大的10000個數。

  第四種方法直接最小堆法。首先讀入前10000個數來建立大小爲10000的最小堆,建堆的時間複雜度爲O(m)(m爲數組的大小即爲10000),而後遍歷後續的數字,並與堆頂(最小)數字進行比較。若是比最小的數小,則繼續讀取後續數字;若是比堆頂數字大,則替換堆頂元素並從新調整堆爲最小堆。重複整個過程直至1億個數所有遍歷完爲止。而後按照中序遍歷的方式輸出當前堆中的全部10000個數字。對每個輸入,堆調整的時間複雜度是O(logm),最終時間複雜度爲:1次建堆時間+n次堆調整時間=O(m+nlogm)=O(nlogm),空間複雜度是10000(常數)。

實際運行:
  實際上,最優的解決方案應該是最符合實際設計需求的方案,在實際應用中,可能有足夠大的內存,那麼直接將數據扔到內存中一次性處理便可,也可能機器有多個核,這樣能夠採用多線程處理整個數據集。
  下面針對不一樣的應用場景,分析了適合相應應用場景的解決方案。
(1)單機+單核+足夠大內存
  若是須要查找10億個查詢詞(每一個佔8B)中出現頻率最高的10個,每一個查詢詞佔8B,則10億個查詢詞所需的內存大約是10^9 * 8B=8GB內存。若是有這麼大內存,直接在內存中先用HashMap求出每一個詞出現的頻率,而後用小頂堆求出頻率最大的10個詞。

(2)單機+多核+足夠大內存
  這時能夠直接在內存中使用Hash方法將數據劃分紅n個partition,每一個partition交給一個線程處理,線程的處理邏輯同(1)相似,最後一個線程將結果歸併。
  該方法存在一個瓶頸會明顯影響效率,即數據傾斜。每一個線程的處理速度可能不一樣,快的線程須要等待慢的線程,最終的處理速度取決於慢的線程。而針對此問題,解決的方法是,將數據劃分紅c×n個partition(c>1),每一個線程處理完當前partition後主動取下一個partition繼續處理,直到全部數據處理完畢,最後由一個線程進行歸併。

(3)單機+單核+受限內存
  這種狀況下,須要用hash函數將原數據文件映射到一個一個小文件,若是小文件仍大於內存大小,繼續採用Hash的方法對數據文件進行分割,直到每一個小文件小於內存大小,這樣每一個文件可放到內存中處理。採用(1)的方法依次處理每一個小文件。

(4)多機+受限內存
  這種狀況,爲了合理利用多臺機器的資源,可用hash函數將原數據文件映射到一個一個小文件,而後分發到多臺機器上,每臺機器採用(3)中的策略解決本地的數據,最後將全部機器處理結果彙總,並利用小頂堆求出頻率最大的10個詞。

  從實際應用的角度考慮,(1)(2)(3)(4)方案並不可行,由於在大規模數據處理環境下,做業效率並非首要考慮的問題,算法的擴展性和容錯性纔是首要考慮的。算法應該具備良好的擴展性,以便數據量進一步加大(隨着業務的發展,數據量加大是必然的)時,在不修改算法框架的前提下,可達到近似的線性比;算法應該具備容錯性,即當前某個文件處理失敗後,能自動將其交給另一個線程繼續處理,而不是從頭開始處理。

   top K問題很適合採用MapReduce框架解決,用戶只需編寫一個Map函數和兩個Reduce 函數,而後提交到Hadoop(採用Mapchain和Reducechain)上便可解決該問題。具體而言,就是首先根據數據值或者把數據hash(MD5)後的值按照範圍劃分到不一樣的機器上,最好可讓數據劃分後一次讀入內存,這樣不一樣的機器負責處理不一樣的數值範圍,實際上就是Map。獲得結果後,各個機器只需拿出各自出現次數最多的前N個數據,而後彙總,選出全部的數據中出現次數最多的前N個數據,這實際上就是Reduce過程。對於Map函數,採用Hash算法,將Hash值相同的數據交給同一個Reduce task;對於第一個Reduce函數,採用HashMap統計出每一個詞出現的頻率,對於第二個Reduce 函數,統計全部Reduce task,輸出數據中的top K便可。

  直接將數據均分到不一樣的機器上進行處理是沒法獲得正確的結果的。由於一個數據可能被均分到不一樣的機器上,而另外一個則可能徹底彙集到一個機器上,同時還可能存在具備相同數目的數據。

如下是一些常常被說起的該類問題。
(1)有一個1G大小的文件,裏面每一行是一個詞,詞的大小不超過16字節,內存限制大小是1M。返回頻數最高的100個詞。
方案1:
順序讀文件,對於每一個詞x,取hash(x)%5000,而後按照該值存到5000個小文件中。這樣每一個文件大概是200k左右。若是其中有的文件超過了1M大小,還能夠按照相似的方法繼續往下分,直到分解獲得的小文件的大小都不超過1M。對每一個小文件,統計每一個文件中出現的詞以及相應的頻率(能夠採用trie樹/hash_map等),並取出出現頻率最大的100個詞(能夠用含100個結點的最小堆),並把100詞及相應的頻率存入文件,這樣又獲得了5000個文件。下一步就是把這5000個文件進行歸併(相似於歸併排序)的過程了。

(2)搜索的輸入信息是一個字符串,統計300萬條輸入信息中最熱門的前10條,每次輸入的一個字符串爲不超過255B,內存使用只有1GB。
典型的Top K算法,第一步、先對這批海量數據預處理,在O(N)的時間複雜度內用HashMap完成統計;第二步、藉助最小堆找出Top K,時間複雜度爲N‘logK。 即最終的時間複雜度是:O(N) + N'*O(logK),(N爲1000萬,N’爲300萬)

(3)最大間隙問題
給定n個實數,,求這n個實數在實軸上相鄰2個數之間的最大差值,要求線性的時間算法。
最早想到的方法就是先對這n個數據進行排序,而後一遍掃描便可肯定相鄰的最大間隙。但該方法不能知足線性時間的要求。故採起以下方法:

  • 找到n個數據中最大和最小數據max和min。
  • 用n-2個點等分區間[min, max],即將[min, max]等分爲n-1個區間(前閉後開區間),將這些區間看做桶,編號爲,且桶i的上界和桶i+1的下界相同,即每一個桶的大小相同。每一個桶的大小爲:,且認爲將min放入第一個桶,將max放入第n-1個桶。
  • 將n個數放入n-1個桶中:將每一個元素x[i]分配到某個桶(編號爲index),其中,而且只須要記錄分到每一個桶的最大最小數據
  • 最大間隙:除最大最小數據max和min之外的n-2個數據放入n-1個桶中,由抽屜原理可知至少有一個桶是空的,又由於每一個桶的大小相同,因此最大間隙不會在同一桶中出現,必定是某個桶的最小數據和另外一個桶的最大數據之差,且該兩桶之間的桶必定是空桶。也就是說,最大間隙在桶i的最大數據和桶j的最小數據之間產生。向前掃描一遍,找到全部有空桶的地方的最大間隙的最大值,一遍掃描便可完成。

3.2 重複問題

  在海量數據中查找出重複出現的元素或者去除重複出現的元素也是常考的問題。針對此類問題,通常能夠經過位圖法實現。例如,已知某個文件內包含一些電話號碼,每一個號碼爲8位數字,統計不一樣號碼的個數。

如下是一些常常被說起的該類問題。
(1)給定a、b兩個文件,各存放50億個url,每一個url各佔64字節,內存限制是4G,讓你找出a、b文件共同的url?
方案1:
能夠估計每一個文件的大小爲5G×64=320G,遠遠大於內存限制的4G。因此不可能將其徹底加載到內存中處理。考慮採起分而治之的方法。

  • 遍歷文件a,對每一個url求hash(url)%1000,而後根據所取得的值將url分別存儲到1000個小文件(記爲a0,a1,a2...a999)中,這樣每一個小文件的大約爲300M。
  • 遍歷文件b,採起和a相同的方式將url分別存儲到1000小文件(記爲b0,b1,b2...b999)中。這樣處理後,全部可能相同的url都在對應(a0-b0,a1-b1,...,a999-b999)的小文件中,不對應的小文件不可能有相同的url。而後咱們只要求出1000對小文件中相同的url便可。
  • 求每對小文件中相同的url時,能夠把其中一個小文件的url存儲到hash_set中。而後遍歷另外一個小文件的每一個url,看其是否在剛纔構建的hash_set中,若是是,那麼就是共同的url,存到文件裏面就能夠了。

方案2:
若是容許有必定的錯誤率,可使用Bloom filter,4G內存大概能夠表示340億bit。將其中一個文件中的url使用Bloom filter映射爲這340億bit,而後逐個讀取另一個文件的url,檢查它是否在Bloom filter表示的集合中,若是是,那麼該url應該是共同的url(注意會有必定的錯誤率)。

(2)在25億個整數中找出不重複的整數,內存不足以容納這25億個整數。
方案1:
採用2-Bitmap(每一個數分配2bit,00表示不存在,01表示出現一次,10表示屢次,11無心義)進行,共需\(2^{32}*2bit=2^{30}B\)=1GB內存,還能夠接受。而後掃描這2.5億個整數,查看Bitmap中相對應位,若是是00變01,01變10,10保持不變。全部整數遍歷完成後,查看bitmap,把對應位是01的整數輸出便可。

方案2:
採用映射的方法,好比模1000,把整個大文件映射爲1000個小文件,再找出每一個小文件中不重複的整數。對於每一個小文件,用hash_map(int,count)來統計每一個整數出現的次數,輸出便可。

(3)1000萬字符串,其中有些是重複的,須要把重複的所有去掉,保留沒有重複的字符串。請怎麼設計和實現?
方案1:這題用trie樹比較合適,hash_map也應該能行。

3.3 排序問題

通常採用位圖(若爲int型的數,最多\(2^{32}\),\(2^{32}*1bit=2^{30}/2B\)=0.5GB=500MB,徹底能夠放入內存)或者外排序。
(1)有10個文件,每一個文件1G,每一個文件的每一行存放的都是用戶的query,每一個文件的query均可能重複。要求你按照query的頻度排序。
方案1:

  • 順序讀取10個文件,按照hash(query)%10的結果將query寫入到另外10個文件(記爲a0,a1,a2...a9)中。這樣新生成的文件每一個的大小大約也1G(假設hash函數是隨機的)。
  • 找一臺內存在2G左右的機器(若是可用內存很小,則分到更多的文件中),依次對a0,a1,a2...a9用hash_map(query, query_count)來統計每一個query出現的次數,而後利用快速/堆/歸併排序按照出現次數進行排序。將排序好的query和對應的query_cout輸出到文件中。這樣獲得了10個排好序的文件(記爲b0,b1,b2...b9)。
  • 對b0,b1,b2...b9這10個文件進行歸併排序(內排序與外排序相結合)。

方案2:
通常query的總量是有限的,只是重複的次數比較多而已,可能對於全部的query,一次性就能夠加入到內存了。這樣,咱們就能夠採用trie樹/hash_map等直接來統計每一個query出現的次數,而後按出現次數作快速/堆/歸併排序就能夠了

方案3: 與方案1相似,但在作完hash,分紅多個文件後,能夠交給多個機器來處理,採用分佈式的架構來處理(好比MapReduce),最後再進行合併。

相關文章
相關標籤/搜索