1. 程式人生 > >海量資料處理:經典例項分析

海量資料處理:經典例項分析

有關海量資料處理的問題,主要有以下3類:top K問題、重複問題、排序問題

top K 問題

在大規模資料處理中,經常會遇到的一類問題:在海量資料中找出出現頻率最高的前K個數,或者從海量資料中找出最大的前K個數,這類問題通常被稱為top K問題。例如,在搜尋引擎中,統計搜尋最熱門的10個查詢詞;在歌曲庫中統計下載率最高的前10首歌等。

針對top k 類問題,通常比較好的方案是分治+Trie樹+小頂堆,即現將資料集按照hash方法分解成多個小資料集,然後使用Trie樹或者Hash統計每個小資料集中的query詞頻,之後用小頂堆求出每個資料集中出現頻率最高的前K個數,最後在所有top K中求出最終的top K。

例子:有1億個浮點數,找出其中最大的10000個?

解決方案

將資料全部排序

最容易想到的方法是將資料全部排序,然後在排序後的集合中進行查詢,最快的排序演算法的時間複雜度一般為O(nlogn),如快速排序。而在32位機器上,每個float型別佔4個位元組,1億個浮點數就要佔用400MB的儲存空間,對於一些可用記憶體小於400MB的計算機而言,很顯然是不能一次將全部資料讀入記憶體進行排序的。其實即使記憶體能夠滿足要求,該方法也並不高效,因為題目要求是尋找出最大的10000個數即可,而排序卻是將所有的元素都排序了。

區域性淘汰法

該方法與排序方法類似,用一個容器儲存前10000個數,然後將剩餘的所有數字一一與容器內的最小數字相比,如果所有後續的元素都比容器內的1000個數還小,那麼容器內的這10000個數就是最大的10000個數。如果某一後續元素比容器內的最小數字大,則刪掉容器內最小元素,並將該元素插入容器,最後遍歷完這1億個數,得到的結果容器中儲存的數即為最終結果了。此時的時間複雜度為O(n+m^2),其中m為容器的大小,即10000.

分治法

將1億個資料分成100份,每份100萬個資料,找出每份資料中最大的10000個,最後在剩下的100*10000個數據裡面找出最大的10000個。如果100萬資料選擇足夠理想,那麼可以過濾掉1億資料裡面99%的資料。100萬個資料裡面查詢最大的10000個數據的方法如下: 
用快速排序的方法,將資料分為2堆,如果大的那堆個數N大於10000個,繼續對大堆快速排序一次分成2堆,如果大堆個數N小於10000,就在校的那堆裡面快速排序一次,找第10000-n大的數字;遞迴以上過程,就可以找到第1w大的數。參考上面的找出的第1w大數字,就可以類似的方法找出前10000大數字了。此種方法每次需要的記憶體空間為10^6*4=4MB,一共需要101此這樣的比較

Hash法

如果這1億個數裡面有很多重複的數,先通過Hash法,把著1億個數字去重複,這樣如果重複率很高的話,會減少很大的記憶體用量,從而縮小運算空間,然後通過分治法或最小堆法查詢最大的10000個數。

最小堆

首先讀入前10000個數來建立大小為10000的小頂堆,建堆的時間複雜度為O(mlogm)(m為陣列的大小即為10000),然後遍歷後續的數字,並與堆頂(最小)數字進行比較。如果比最小的數小,則繼續讀取後續數字;如果比堆頂數字大,則替換對頂元素並重新調整堆為小頂堆。整個過程直至1億個數全都遍歷完為止。然後按照中序遍歷的方式輸出當前堆中的所有10000個數字。該演算法的時間複雜度為O(nmlogm),空間複雜度是10000(常數)。

實際上,最優的解決方案應該是最腹黑實際設計需求的方案,在實際應用中,可能有足夠大的記憶體,那麼直接將資料扔到記憶體中一次性處理即可,也可能機器有多個核,這樣可以採用多執行緒處理整個資料集。

不同應用場景的解決方案

單機+單核+足夠大記憶體

如果需要查詢10億個查詢詞(每個佔8B)中出現頻率最高的10個,考慮到每個查詢詞佔8B,則10億個查詢詞所需的記憶體大約是10^9*8B = 8GB 記憶體。如果有這麼大的記憶體,直接在記憶體中對查詢詞進行排序,順序遍歷找出10個出現頻率最大的即可。這種方法簡單快速,更加實用。當然,也可以先用HashMap求出每個詞出現的頻率,然後求出頻率最大的10個詞。

單機+多核+足夠大記憶體

這時可以直接在記憶體中使用Hash方法將資料劃分成n個partition,每個partition交給一個執行緒處理,執行緒的處理邏輯是同(1)類似,最後一個執行緒將結果歸併。 
該方法存在一個瓶頸會明顯影響效率,即資料傾斜。每個執行緒的處理速度可能不同,快的執行緒需要等待慢的執行緒,最終的處理速度取決於慢的執行緒。而針對此問題,解決方法是,將資料劃分成c*n個partition(c>1),每個執行緒處理完當前partition後主動取下一個partition繼續處理,直到所有資料處理完畢,最後由一個執行緒進行歸併

單機+單核+受限記憶體

這種情況下,需要將原資料檔案切割成一個一個小檔案,如採用hash(x)%M,將原始檔中的資料切割成M小檔案,如果小檔案仍大於記憶體大小,繼續採用Hash的方法對資料檔案進行切割,直到每個小檔案小於記憶體大小,這樣每個檔案可放到記憶體中處理。採用(1)的方法依次處理每個小檔案。

多機+受限記憶體

這種情況下,為了合理利用多臺機器的資源,可將資料分發到多臺機器上,每臺機器採用(3)節中的策略解決本地的資料。可採用hash+socket方法進行資料分發。

小結

從實際應用考慮,上述的不同場景的解決方案並不可行,因為在大規模資料處理環境下,作業效率並不是首要考慮的問題,演算法的擴充套件性和容錯性才是首要考慮的。演算法應該具有良好的擴充套件性,以便資料量進一步加大(隨著業務的發展,資料量加大是必然的)時,在不修改演算法框架的前提下,可達到近似的線性比;演算法應該具有容錯性,即當前某個檔案處理失敗後,能自動將其交給另外一個執行緒繼續處理,而不是從頭開始處理。

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 即可。

直接將資料均分到不同的機器上進行處理時無法得到正確的結果的。因為一個數據可能被均分到不同的機器上,而另一個則可能完全聚集到一個機器上,同時還可能存在具有相同數目的資料。

Top K問題還有很多應用場景,尤其是在程式媛面試筆試中有很多例項,它們都可以採用上述方法解決。以下是一些歷年來經常被各大網際網路公司提及的該類問題。 
(1)有1億個記錄,這些查詢串的重複度比較高,如果除去重複後,不超過3百萬個。一個查詢串的重複度過高,說明查詢它的使用者越多,也就是越熱門。請統計最熱門的10個查詢串,要求使用的記憶體不能超過1GB 
(2)有10個檔案,每個檔案1GB,每個檔案的每一行存放的都是使用者的query,每個檔案的query都可能重複。按照query的頻度排序。 
(3)有一個1GB大小的檔案,裡面的每一行是一個詞,詞的大小不超過16個位元組,記憶體限制大小是1MB。返回頻數最高的100個詞 
(4)提取某日訪問網站次數最多的那個IP 
(5)10億個整數找出重複次數最多的100個整數 
(6)搜尋的輸入資訊是一個字串,統計300萬條輸入資訊中最熱門的前10條,每次輸入的一個字串為不超過255B,記憶體使用只有1GB 
(7)有1000萬個身份證號以及它們對應的資料,身份證號可能重複,找出出現次數最多的身份證號。

重複問題

海量資料中查找出重複出現的元素或者去除重複出現的元素 
針對此類問題,一般可以通過點陣圖法實現。

例如,已知某個檔案內包含一些電話號碼,每個號碼為8位數字,統計不同號碼的個數。

本題最好的解決方法是通過使用點陣圖法來實現。8位整數可以表示的最大十進位制數值為99999999.如果每個數字對應於點陣圖中一個bit位,那麼儲存8位整數大約需要99MB。因為1B=8bit,所以99Mbit摺合成記憶體為 99/8 = 12.375MB的記憶體表示所有的8位數電話號碼的內容。

#include<iostream>
#include<stdlib.h>
#include<time.h>

using namespace std;

#define BITWORD 32
#define ARRNUM 100
int mmin = 10000000;
int mmax = 99999999;

int N = (mmax-mmin+1);
#define BITS_PER_WORD 32
#define WORD_OFFSET(b) ( (b)/BITS_PER_WORD )
#define BIT_OFFSET(b) ((b)%BITS_PER_WORD )

void SetBit( int *words, int n )
{
    n -= mmin;
    words[WORD_OFFSET(n)] |= ( 1<< BIT_OFFSET(n));
}

void ClearBit( int *words, int n )
{
    words[WORD_OFFSET(n)] &= ~( 1<< BIT_OFFSET(n));
}
int GetBit( int *words, int n )
{
    int bit = words[WORD_OFFSET(n)] & ( 1<< BIT_OFFSET(n));
    return bit != 0 ;
}


int main()
{
    int arr[ ARRNUM ];
    int *words = new int[ 1+N/BITS_PER_WORD ];
    if( words == NULL )
    {
        cout << " new error \n " << endl;
        exit(0);
    }

    int count = 0;
    for(int i=0; i<N; i++ )
        ClearBit( words,i);
    srand( (unsigned)time(NULL));
    cout << " the size of the array : " << ARRNUM << endl;

    for(int j=0; j<ARRNUM; j++ )
    {
        arr[j] = rand() % N;
        arr[j] += mmin;
        cout << arr[j] << "\t";
    }

    for(int j=0; j<ARRNUM; j++ )
        SetBit( words, arr[j]);

    cout << "after sort, a : " << endl;
    for( int i=0; i<N; i++ )
        if( GetBit( words,i) )
        {
            cout << i+mmin << "\t";
            count++;
        }
    cout << " sum is : " << count << endl;
    delete[] words;
    words = NULL;

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72

上例中,採用時間作為種子,產生了100個隨機數,對這100個數進行點陣圖法排序,進而找出其中重複的資料。與此問題相似的面試筆試題還有: 
(1)10億個正整數,只有1個數重複出現過,要求在O(n)的時間裡找出這個數 
(2)給定a、b兩個檔案,各存放50億個url,每個url各佔用64B,要求在O(n)的時間裡找出a、b檔案共同的url 
(3)給40億個不重複的unsigned int的整數,沒拍過序的,然後再給一個數,如何快速判斷這個數是否在那40億個數中?

排序問題

海量資料處理中一類常見的問題就是排序問題,即對海量資料中的資料進行排序。例如,一個檔案中有9億條不重複的9位整數,對這個檔案中的數字進行排序。

針對這個問題,最容易想到的方法是將所有資料匯入到記憶體中,然後使用常規的排序方法,如插入排序、快速排序、歸併排序等各種排序方法對資料進行排序,最後將排序好的資料存入檔案。但這些方法卻不能在此適用,由於資料量巨大,在32位機器中,一個整數佔用4個位元組,而9億條資料共佔用9*10^8*4B,大約需要佔用3.6GB記憶體,對於32位機器而言,很難將這麼多資料一次載入到記憶體,所以,需要考慮其他方法。

資料庫排序法

將文字檔案匯入到資料庫中,讓資料庫進行索引排序操作後提取資料到檔案。該種方法雖然操作簡單、方便,但是運算速度較慢,而且對資料庫裝置要求比較高。

分治法

通過hash將9億條資料分為20段,每段大約5000萬條,大約佔用5*10^6*4B=200MB空間,在檔案中依次搜尋0~5000萬,50000001~1億。。。將排序的結果存入檔案。該方法要裝滿9位整數,一共需要20此,所以一共要進行20次排序,需要對檔案進行20次讀操作。該方法雖然縮小了每次使用的記憶體空間大小,但是編碼複雜,速度也慢。

點陣圖法

考慮到最大的9位整數為 999999999,由於9億條資料是不重複的,可以把這些資料組成一個佇列或陣列,讓它有0~999999999(一共10億個數)個元素陣列下標表示數值,結點中用0表示沒有這個數,1表示存在這個數,判斷0或1只用一個bit儲存就夠了,而宣告一個可以包含9位整數的bit陣列,一共需要10億/8,大約120MB記憶體,把記憶體中的資料全部初始化為0,讀取檔案中的資料,並將資料放入記憶體。比如讀到一個數據為341245909,那就先在記憶體中找到341245909這個bit,並將bit值置為1,遍歷整個bit陣列,將bit為1的陣列下標存入檔案,最終得到排序後的內容。

此類排序問題的求解方法一般都是採用上述方法。海量資料處理中與此類似的問題還有以下幾種: 
(1)一年的全國高考考生人數為500萬,分數使用標準分,最低100分,最高900分,不存在成績為小數的情況,把這500萬考生的分數排序 
(2)一個包含n個正整數的檔案,每個正整數小於n,n等於1000萬,並且檔案內的正整數沒有重複和關聯資料u,輸出整數的升序排列。