大數據量獲取TopK的幾種方案


一:介紹

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

二:局部淘汰法 -- 借助“冒泡排序”獲取TopK

思路:

可以避免對所有數據進行排序,只排序部分

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

時間復雜度空間復雜度

時間復雜度:排序一輪是O(N),則K次排序總時間復雜度為:O(KN)

空間復雜度:O(K),用來存放獲得的topK,也可以O(1)遍歷原數組的最后K個元素即可。ps:冒泡排序請參考: https://blog.csdn.net/CSDN___LYY/article/details/81478583

代碼比較簡單就不貼了,只要會寫冒泡就ok了

三:局部淘汰法 -- 借助數據結構"堆"獲取TopK

思路:

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

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

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

然后循環從K下標位置遍歷數據,只要元素大於堆頂,我們就將堆頂賦值為該元素,然后重新調整為小頂堆

循環完畢后,K個元素的堆數組就是我們所需要的TopK

為什么使用小頂堆呢?

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

時間復雜度與空間復雜度

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

空間復雜度:O(K),只需要新建一個K大小的數組用來存儲topK即可

適用環境

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

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

代碼實現

使用的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

思路:

比如有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

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

第二種方法中我們可以采用多核的優勢,創建多個線程,分別去操作不同的數據。

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

適用環境

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

時間復雜度與空間復雜度

時間復雜度:一份獲取前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。如何進行大數據量的去重操作呢,簡單的說一下:

采用bitmap來進行去重。

一個char類型的數據為一個字節也就是8個字符,而每個字符都是用0\1標識,我們初始化所有字符為0。

我們申請N/8+1容量的char數組,總共有N+8個字符。

對數據進行遍歷,對每個元素S進行S/8操作獲得char數組中的下標位置,S%8操作獲得該char的第幾個字符置1。

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

主要還是根據內存、核數、最大創建線程數來動態判斷如何獲取前TopK。

原文鏈接:https://liyangyang.blog.csdn.net/article/details/82909081


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM