大資料量獲取TopK的幾種方案

yoylee_web發表於2018-09-30

一:介紹

    生活中經常會遇到求TopK的問題,在小資料量的情況下可以先將所有資料排序,最後進行遍歷。但是在大資料量情況下,這種的時間複雜度最低的也就是O(NlogN)此處的N可能為10億這麼大的數字,時間複雜度過高,那麼什麼方法可以減少時間複雜度呢,以下幾種方式,與大家分享。

二:區域性淘汰法 -- 藉助“氣泡排序”獲取TopK

  1. 思路:

    • 可以避免對所有資料進行排序,只排序部分

    • 氣泡排序是每一輪排序都會獲得一個最大值,則K輪排序即可獲得TopK

  2. 時間複雜度空間複雜度

  3. 程式碼比較簡單就不貼了,只要會寫冒泡就ok了

三:區域性淘汰法 -- 藉助資料結構"堆"獲取TopK

  1. 思路:

    • 堆:分為大頂堆(堆頂元素大於其他所有元素)和小頂堆(堆頂其他元素小於所有其他元素)

    • 我們使用小頂堆來實現,為什麼不適用大頂堆下面會介紹~

    • 取出K個元素放在另外的陣列中,對這K個元素進行建堆 ps:堆排序請參考:https://blog.csdn.net/CSDN___LYY/article/details/81454613

    • 然後迴圈從K下標位置遍歷資料,只要元素大於堆頂,我們就將堆頂賦值為該元素,然後重新調整為小頂堆

    • 迴圈完畢後,K個元素的堆陣列就是我們所需要的TopK

  2. 為什麼使用小頂堆呢?

    • 我們在比較的過程中使用堆頂是最小值的小頂堆,元素大於堆頂我們對堆頂進行重新賦值,那麼堆頂永遠是這K個值中最小的值,當我們下一個元素和堆頂比較時,如果不大於堆頂的話,那麼一定不屬於topK範圍的

  3. 時間複雜度與空間複雜度

    • 時間複雜度:每次對K個元素進行建堆,時間複雜度為:O(KlogK),加上N-K次的迴圈,則總時間複雜度為O((K+(N-K))logK),即O(NlogK),其中K為想要獲取的TopK的數量N為總資料量

    • 空間複雜度:O(K),只需要新建一個K大小的陣列用來儲存topK即可

  4. 適用環境

    • 適用於單核單機環境,不會發揮多核的優勢

    • 也可用於分治法中獲取每一份元素的Top,下面會介紹

  5. 程式碼實現

    • 使用的java程式碼實現的,程式碼內每一步都有註釋便於理解

 

import java.util.Arrays;

/**
* 通過堆這種資料結構
* 獲得大資料量中的TopK
*/

public class TopKStack {
    public static void main(String[] args) {
        //定義一個陣列,找出該陣列中的topK,大資料量不好搞到,先用這個陣列測試
        int [] datas = {2,3,42,1,34,5,6,67,3,243,8,246,123,6,32,3451,23,5,6,31,5,6,2346,36};
        int [] re = getTopK(datas,10);
        System.out.println(Arrays.toString(re));
    }

    /**
    * 獲取前topk的方法
    * @param datas 原陣列
    * @param num 前topNum
    * @return 最後的topNum的堆陣列
    */
    static int[] getTopK(int[] datas,int num){
        //定義儲存前num個元素的陣列,用於建堆
        int[] res = new int[num];
        //初始化陣列
        for (int i = 0; i < num; i++) {
            res[i] = datas[i];
        }
        //建造初始化堆
        for (int i = (num - 1)/2; i >= 0 ; i--) {
            shift(res,i);
        }
        //遍歷查詢num個最大值
        for (int i = num; i < datas.length; i++) {
            if (datas[i] > res[0]){
                res[0] = datas[i];
                shift(res,0);
           }
        }
        return res;
    }

    /**
    * 調整元素滿足堆結構
    * @param datas
    * @param index
    * @return
    */
    static int[] shift(int[] datas ,int index){
        while(true){
            int left = (index<<1) + 1; //左孩子
            int right = (index<<1) + 2; //右孩子

            int min_num = index; //標識自身節點和孩子節點中最小值的位置
            //判斷是否存在左右孩子,並且得到左右孩子和自身的最小值
            if (left <= datas.length-1&&datas[left] < datas[index]){
                min_num = left;
            }
            if (right <= datas.length-1&&datas[right] < datas[min_num]){
                min_num = right;
        }
        //如果最小值不等於自身,則將最小值與自身交換
        if (min_num != index){
            int temp = datas[index];
            datas[index] = datas[min_num];
            datas[min_num] = temp;
        }else{
            //此處break是因為我們是從樹的最下面進行調整的,如果上層節點符合堆,則下層節點一定符合!
            break;
        }

        //執行到此處,說明可能需要調整下面的節點,則將初始節點賦值為最小值所在的節點位置,
        // 因為最大值點的位置進行了交換,可能下層節點就不滿足堆性質
        index = min_num;
    }
    return datas;
   }
}

四:分治法 -- 藉助”快速排序“方法獲取TopK

  1. 思路:

    • 比如有10億的資料,找處Top1000,我們先將10億的資料分成1000份,每份100萬條資料

    • 在每一份中找出對應的Top 1000,整合到一個陣列中,得到100萬條資料,這樣過濾掉了999%%的資料

    • 使用快速排序對這100萬條資料進行”一輪“排序,一輪排序之後指標的位置指向的數字假設為S,會將陣列分為兩部分,一部分大於S記作Si,一部分小於S記作Sj。 ps:快速排序請參考:https://blog.csdn.net/CSDN___LYY/article/details/81478583

    • 如果Si元素個數大於1000,我們對Si陣列再進行一輪排序,再次將Si分成了Si和Sj。如果Si的元素小於1000,則我們需要在Sj中獲取1000-count(Si)個元素的,也就是對Sj進行排序

    • 如此遞迴下去即可獲得TopK

  2. 和第一種方法有什麼不同呢?相對來說的優點是什麼?

    • 第二種方法中我們可以採用多核的優勢,建立多個執行緒,分別去操作不同的資料。

    • 當然我們在分治的第二步可以使用第一種方法去獲取每一份的Top。

  3. 適用環境

    • 多核多機的情況,分治法會將多核的作用發揮到最大,節省大量時間

  4. 時間複雜度與空間複雜度

    • 時間複雜度:一份獲取前TopK的時間複雜度:O((N/n)logK)。則所有份數為:O(NlogK),但是分治法我們會使用多核多機的資源,比如我們有S個執行緒同時處理。則時間複雜度為:O((N/S)logK)。之後進行快排序,一次的時間複雜度為:O(N),假設排序了M次之後得到結果,則時間複雜度為:O(MN)。所以 ,總時間複雜度大約為O(MN+(N/S)logK) 。

    • 空間複雜度:需要每一份一個陣列,則空間複雜度為O(N)

五:其他情況

  • 通常我們要根據資料的情況去判斷我們使用什麼方法,在獲取TopK前我們可以做什麼操作減少資料量。

  • 比如:資料集中有許多重複的資料並且我們需要的是前TopK個不同的數,我們可以先進行去重之後再獲取前TopK。如何進行大資料量的去重操作呢,簡單的說一下:

    1. 採用bitmap來進行去重。

    2. 一個char型別的資料為一個位元組也就是8個字元,而每個字元都是用0\1標識,我們初始化所有字元為0。

    3. 我們申請N/8+1容量的char陣列,總共有N+8個字元。

    4. 對資料進行遍歷,對每個元素S進行S/8操作獲得char陣列中的下標位置,S%8操作獲得該char的第幾個字元置1。

    5. 在遍歷過程中,如果發現對應的字元位置上已經為1,則代表該值為重複值,可以去除。

  • 主要還是根據記憶體、核數、最大建立執行緒數來動態判斷如何獲取前TopK。

 

相關文章