1. 程式人生 > >海量資料處理-重新思考排序

海量資料處理-重新思考排序

海量資料處理--重新思考排序(1)

海量資料處理常用技術概述

如今網際網路產生的資料量已經達到PB級別,如何在資料量不斷增大的情況下,依然保證快速的檢索或者更新資料,是我們面臨的問題。 所謂海量資料處理,是指基於海量資料的儲存、處理和操作等。因為資料量太大無法在短時間迅速解決,或者不能一次性讀入記憶體中。

在解決海量資料的問題的時候,我們需要什麼樣的策略和技術,是每一個人都會關心的問題。今天我們就梳理一下在解決大資料問題 的時候需要使用的技術,但是注意這裡只是從技術角度進行分析,只是一種思想並不代表業界的技術策略。 常用到的演算法策略

  1. 分治:多層劃分、MapReduce
  2. 排序:快速排序、桶排序、堆排序
  3. 資料結構:堆、點陣圖、布隆過濾器、倒排索引、二叉樹、Trie樹、B樹,紅黑樹
  4. Hash對映:hashMap、simhash、區域性敏感雜湊

海量資料處理–重新思考排序(1)

定義排序

排序:   將一組無序的集合,根據某個給定的條件,將其變成有序的方法就是排序。從這個我給出的不嚴謹的定義中排序是方法,目的是讓原來無序的集合滿足條件有序。   這裡我們基於海量資料的考慮重新思考排序,不會詳述每一種排序方法的原理,主要面向的是如何在海量資料情況下使用排序方法。

常用的排序方法:   插入排序,選擇排序,氣泡排序,希爾排序,快速排序,歸併排序,堆排序,桶排序,計數排序,基數排序。 下面給出幾種排序演算法的簡單介紹圖。      

Screenshot-from-2018-11-05-14-04-03

既然有這麼多的排序方法,我們可以直接讀取資料到記憶體中直接呼叫語言中封裝好的排序方法即可。但是資料量很大,不能將資料同時讀入記憶體。 這就出現了所有的外排序,我們可以用歸併排序的思想來解決這個問題,也可以基於資料範圍用"計數排序"的思想來解決。 排序真的很重要嗎?我一直相信一句話:沒有排序解決不了的問題。這裡給出幾個需求,例如:

  • 取最大的k個數,直接降序排序取前k個即可;
  • 推薦、搜尋業務,我們也可以直接排序(精度不高)
  • 二分查詢之前也要求資料有序

但是在這裡我們不講排序,只說排序中用到的思想,以及在海量資料處理的過程中,如何用到的排序。依然接著 上次的文章,我們求top k的時候,最後用到了一個數據結構,叫做堆,堆可以找個top k,就能找到n個數中的 top n,這樣就是有序,叫做堆排序。

堆排序

在top k中我們用到了一個數據結構堆(有最大堆和最小堆),這裡就先介紹一下這個資料結構的性質,基於最大 堆進行介紹。堆是一個完全二叉樹,對於任意的節點,我們可以使用資料來表示最大堆,設定下標從0開始, 滿足以下性質:

  • root > left && root > right. (左右節點存在)
  • 根節點:root_index; 左孩子節點:left_index; 右孩子節點:right_index
  • left_index = root_index * 2 + 1
  • right_index = root_index * 2 + 2
  • root_index = (*_index - 1) / 2

在堆的資料結構進行增刪改查的過程中,我們始終維護堆的資料結構,定義MaxheapFy(int *A, int i)表示維護第i個 節點滿足最大堆的性質,注意這裡沒有考慮到泛型程式設計,正常應該提供一個比較方法的函式,讓使用者自己設定比較方式。 從下面的虛擬碼中,我們可以知道對於一個大小為n的堆,維護一次堆的性質,最壞時間為O(logn),但是必須保證 在改變之前,他是滿足堆的性質的。

void MaxheapFy(int *A,int i) {
    // i 要在A的範圍之內,
    assert(i >= 0);
    assert(i < n) // 堆的大小
    l = LEFT(i), r = RIGHT(i); // 得到左右子節點,如果存在
    now = i;

    // 找到左右孩子的最大值
    if(l<=heapsize&&A[l]>A[now]){
        now=l;//交換A[l]和A[i],並遞迴維護下一個當前結點now
    }
    if(r<=heapsize&&A[r]>A[now]){
        now=r;//交換A[l]和A[i],並遞迴維護下一個當前結點now
    }

    if(now != i) { // 交換,遞迴維護
        swap(A[i], A[now]);
        MaxheapFy(A, now);
    }
}

基於上面的這個維護的性質,我們可以直接對於長度為n的陣列建立最大堆,我們知道當只有一個元素的時候,一定滿足最大堆的性質, 基於這個性質,我們對於長度為n的陣列A,從 n / 2向前維護每一個節點的性質,就可以得到最大堆.從下面給出的最大堆 的構建程式碼,我們可以分析建堆的時間複雜度是O(nlogn).因為每次維護是O(logn),維護n次,(這裡計算時間複雜度的時候,忽略常數係數)。

void BuildMaxHeap(int *A,int n){//A[1..n]
    heapsize=n;//全域性變數,表示最大堆的大小
    for(int i=n/2;i>=1;i--){//從n/2..1維護堆中每個節點的最大堆性質:結點的值大於起孩子的值
        MaxheapFY(A,i);
    }
}

建成最大堆之後,從最大堆的性質我們知道,A[0]一定是最大值,如果要堆A升序排序,就可以swap(A[0], A[n-1]); 繼續維護A[0],直到堆中只是一個元素,這就完成了堆排序。從這個思路出發,對於top k問題,我們為什麼要維護一個 最小堆呢,因為我們要過濾所有的資料,保證每次彈出一個最小值,之後剩下的k個一定是top k的最大值,但是這k個不一定 有序,如果需要我們可以堆這k進行任何排序,因為我們通過過濾,資料已經很少了,時間複雜度就是從n箇中過濾出來k個。 首先任選k個構建最小堆, 時間複雜度O(klogk), 用最小堆過濾n-k個數字,每次維護堆的性質,時間O((n-k)logk). 總的時間複雜度O(klogk + (n-k)logk)。(注意當k多大時,我們不在使用堆的資料結構,這裡留給讀者計算)。

void HeapSort(int *A,int n){
    BuildMaxHeap(A,n);//建立最大堆
    for(int i=n;i>=2;i--){
        //cout<<A[1]<<" ";
        swap(A[1],A[i]);//互動A[1]和A[i],使得A[i]中為當前最大的元素
        heapsize--;//堆大小減去1,便於下次操作去掉已經排好序的元素
        MaxheapFY(A,1);//此時A[1]不一定滿足最大堆的性質,重新維護下標1的最大堆的性質
    }
}

我們可以知道堆的使用可以很好的找到top k,堆使用場景都有什麼呢?這裡我們給出工業界常用到的使用場景。 推薦系統大家都很瞭解,例如手機百度、今日頭條等會推薦使用者喜歡的新聞,在推薦系統中就有用到堆。為了很好的瞭解到堆的 應用,我簡單介紹一個簡化推薦系統,至少有recall和rank兩個部分,在recall(召回的階段)),對於使用手機百度的使用者, 當你進行一次重新整理的時候,後天會根據的你各種profile等靜態和動態的特徵,請求後端,後端會從多個方面召回一可能感興趣 的文章,例如基於地域,性別,當天熱門,學歷,瀏覽記錄等,這樣就會返回多個帶有權重的佇列,下一步會從這多個佇列中,選擇 100個得分最高的傳到下一層,這個過程中就要用到堆。這裡用到的是大根堆。

問題抽象 例如我們有100個有序(降序)的陣列,現在從這個100個數組中找到最大的k個元素。這就是上述問題的抽象。使用100路歸併(後面的歸併排序)。 用一個大小為k的最大堆,每次彈出一個最大值,記錄是那個佇列中的值,直到出現k個數,就結束。這裡裡面的兩個思想,

  • 歸併,不能處理的大問題,分成多個小問題並行處理,之後歸併結果,比如外排序
  • 堆,幫助我們找到top k,k要相對n較小。

Screenshot-from-2018-11-05-16-53-45

參考: 資料結構: 構建和使用堆 《演算法導論》第六章:堆排序 《程式設計之美:面試與演算法心得》