數據結構和算法-Top K算法


參考:

https://blog.csdn.net/wufaliang003/article/details/82940218

https://blog.csdn.net/boo12355/article/details/11788655

https://blog.csdn.net/luochoudan/article/details/53736752

https://blog.csdn.net/zyq522376829/article/details/47686867

https://blog.csdn.net/u010601183/article/details/56481868

 

 

 

 

 

TOP K算法

現在有一組千萬級別的數,你能不能幫我找出最大的5個?盡量少用空間和時間。

筆者見過關於Top K問題最全的分類總結是在這里(包括海量數據的處理),個人將這些題分成了兩類:一類是容易寫代碼實現的;另一類側重考察思路的。毫無疑問,后一種比較簡單,你只要記住它的應用場景、解決思路,並能在面試的過程中將它順利地表達出來,便能以不變應萬變。前一種,需要手寫代碼,就必須要掌握一定的技巧,常見的解法有兩種,就是前面說過的堆排和快排的變形。

本文主要來看看方便用代碼解決的問題。

堆排解法

用堆排來解決Top K的思路很直接。

前面已經說過,堆排利用的大(小)頂堆所有子節點元素都比父節點小(大)的性質來實現的,這里故技重施:既然一個大頂堆的頂是最大的元素,那我們要找最小的K個元素,是不是可以先建立一個包含K個元素的堆,然后遍歷集合,如果集合的元素比堆頂元素小(說明它目前應該在K個最小之列),那就用該元素來替換堆頂元素,同時維護該堆的性質,那在遍歷結束的時候,堆中包含的K個元素是不是就是我們要找的最小的K個元素?

實現:
在堆排的基礎上,稍作了修改,buildHeap和heapify函數都是一樣的實現,不難理解。

速記口訣:最小的K個用最大堆,最大的K個用最小堆。

public class TopK { public static void main(String[] args) { // TODO Auto-generated method stub int[] a = { 1, 17, 3, 4, 5, 6, 7, 16, 9, 10, 11, 12, 13, 14, 15, 8 }; int[] b = topK(a, 4); for (int i = 0; i < b.length; i++) { System.out.print(b[i] + ", "); } } public static void heapify(int[] array, int index, int length) { int left = index * 2 + 1; int right = index * 2 + 2; int largest = index; if (left < length && array[left] > array[index]) { largest = left; } if (right < length && array[right] > array[largest]) { largest = right; } if (index != largest) { swap(array, largest, index); heapify(array, largest, length); } } public static void swap(int[] array, int a, int b) { int temp = array[a]; array[a] = array[b]; array[b] = temp; } public static void buildHeap(int[] array) { int length = array.length; for (int i = length / 2 - 1; i >= 0; i--) { heapify(array, i, length); } } public static void setTop(int[] array, int top) { array[0] = top; heapify(array, 0, array.length); } public static int[] topK(int[] array, int k) { int[] top = new int[k]; for (int i = 0; i < k; i++) { top[i] = array[i]; } //先建堆,然后依次比較剩余元素與堆頂元素的大小,比堆頂小的, 說明它應該在堆中出現,則用它來替換掉堆頂元素,然后沉降。 buildHeap(top); for (int j = k; j < array.length; j++) { int temp = top[0]; if (array[j] < temp) { setTop(top, array[j]); } } return top; } }



時間復雜度
n*logK

速記:堆排的時間復雜度是n*logn,這里相當於只對前Top K個元素建堆排序,想法不一定對,但一定有助於記憶。

適用場景
實現的過程中,我們先用前K個數建立了一個堆,然后遍歷數組來維護這個堆。這種做法帶來了三個好處:(1)不會改變數據的輸入順序(按順序讀的);(2)不會占用太多的內存空間(事實上,一次只讀入一個數,內存只要求能容納前K個數即可);(3)由於(2),決定了它特別適合處理海量數據。

這三點,也決定了它最優的適用場景。

快排解法

用快排的思想來解Top K問題,必然要運用到”分治”。

與快排相比,兩者唯一的不同是在對”分治”結果的使用上。我們知道,分治函數會返回一個position,在position左邊的數都比第position個數小,在position右邊的數都比第position大。我們不妨不斷調用分治函數,直到它輸出的position = K-1,此時position前面的K個數(0到K-1)就是要找的前K個數。

實現:
“分治”還是原來的那個分治,關鍵是getTopK的邏輯,務必要結合注釋理解透徹,自動動手寫寫。

public class TopK { public static void main(String[] args) { // TODO Auto-generated method stub int[] array = { 9, 3, 1, 10, 5, 7, 6, 2, 8, 0 }; getTopK(array, 4); for (int i = 0; i < array.length; i++) { System.out.print(array[i] + ", "); } } // 分治 public static int partition(int[] array, int low, int high) { if (array != null && low < high) { int flag = array[low]; while (low < high) { while (low < high && array[high] >= flag) { high--; } array[low] = array[high]; while (low < high && array[low] <= flag) { low++; } array[high] = array[low]; } array[low] = flag; return low; } return 0; } public static void getTopK(int[] array, int k) { if (array != null && array.length > 0) { int low = 0; int high = array.length - 1; int index = partition(array, low, high); //不斷調整分治的位置,直到position = k-1 while (index != k - 1) { //大了,往前調整 if (index > k - 1) { high = index - 1; index = partition(array, low, high); } //小了,往后調整 if (index < k - 1) { low = index + 1; index = partition(array, low, high); } } } } }

時間復雜度
n

速記:記住就行,基於partition函數的時間復雜度比較難證明,從來沒考過。

適用場景
對照着堆排的解法來看,partition函數會不斷地交換元素的位置,所以它肯定會改變數據輸入的順序;既然要交換元素的位置,那么所有元素必須要讀到內存空間中,所以它會占用比較大的空間,至少能容納整個數組;數據越多,占用的空間必然越大,海量數據處理起來相對吃力。

但是,它的時間復雜度很低,意味着數據量不大時,效率極高。

 

 

 

 

 

 

 

Top K算法分析

TopK,是問得比較多的幾個問題之一,到底有幾種方法,這些方案里蘊含的優化思路究竟是怎么樣的,今天和大家聊一聊。

問題描述:

從arr[1, n]這n個數中,找出最大的k個數,這就是經典的TopK問題。

栗子:

從arr[1, 12]={5,3,7,1,8,2,9,4,7,2,6,6} 這n=12個數中,找出最大的k=5個。

 

一、排序

排序是最容易想到的方法,將n個數排序之后,取出最大的k個,即為所得。

 

偽代碼:

sort(arr, 1, n);

return arr[1, k];

 

時間復雜度:O(n*lg(n))
 

分析:明明只需要TopK,卻將全局都排序了,這也是這個方法復雜度非常高的原因。那能不能不全局排序,而只局部排序呢?這就引出了第二個優化方法。

 

二、局部排序

不再全局排序,只對最大的k個排序。

冒泡是一個很常見的排序方法,每冒一個泡,找出最大值,冒k個泡,就得到TopK。

 

偽代碼:

for(i=1 to k){

         bubble_find_max(arr,i);

}

return arr[1, k];

 

時間復雜度:O(n*k)

 

分析:冒泡,將全局排序優化為了局部排序,非TopK的元素是不需要排序的,節省了計算資源。不少朋友會想到,需求是TopK,是不是這最大的k個元素也不需要排序呢?這就引出了第三個優化方法。

 

三、堆

思路:只找到TopK,不排序TopK。

先用前k個元素生成一個小頂堆,這個小頂堆用於存儲,當前最大的k個元素。

 

接着,從第k+1個元素開始掃描,和堆頂(堆中最小的元素)比較,如果被掃描的元素大於堆頂,則替換堆頂的元素,並調整堆,以保證堆內的k個元素,總是當前最大的k個元素。

 

直到,掃描完所有n-k個元素,最終堆中的k個元素,就是猥瑣求的TopK。

 

偽代碼:

heap[k] = make_heap(arr[1, k]);

for(i=k+1 to n){

         adjust_heap(heep[k],arr[i]);

}

return heap[k];

 

時間復雜度:O(n*lg(k))

畫外音:n個元素掃一遍,假設運氣很差,每次都入堆調整,調整時間復雜度為堆的高度,即lg(k),故整體時間復雜度是n*lg(k)。

 

分析:堆,將冒泡的TopK排序優化為了TopK不排序,節省了計算資源。堆,是求TopK的經典算法,那還有沒有更快的方案呢?

 

四、隨機選擇

隨機選擇算在是《算法導論》中一個經典的算法,其時間復雜度為O(n),是一個線性復雜度的方法。

 

這個方法並不是所有同學都知道,為了將算法講透,先聊一些前序知識,一個所有程序員都應該爛熟於胸的經典算法:快速排序。

畫外音:

(1)如果有朋友說,“不知道快速排序,也不妨礙我寫業務代碼呀”…額...

(2)除非校招,我在面試過程中從不問快速排序,默認所有工程師都知道;

 

其偽代碼是:

void quick_sort(int[]arr, int low, inthigh){

         if(low== high) return;

         int i = partition(arr, low, high);

         quick_sort(arr, low, i-1);

         quick_sort(arr, i+1, high);

}

 

其核心算法思想是,分治法。

 

分治法(Divide&Conquer),把一個大的問題,轉化為若干個子問題(Divide),每個子問題“都”解決,大的問題便隨之解決(Conquer)。這里的關鍵詞是“都”。從偽代碼里可以看到,快速排序遞歸時,先通過partition把數組分隔為兩個部分,兩個部分“都”要再次遞歸。

 

分治法有一個特例,叫減治法。

 

減治法(Reduce&Conquer),把一個大的問題,轉化為若干個子問題(Reduce),這些子問題中“只”解決一個,大的問題便隨之解決(Conquer)。這里的關鍵詞是“只”。

 

二分查找binary_search,BS,是一個典型的運用減治法思想的算法,其偽代碼是:

int BS(int[]arr, int low, inthigh, int target){

         if(low> high) return -1;

         mid= (low+high)/2;

         if(arr[mid]== target) return mid;

         if(arr[mid]> target)

                   return BS(arr, low, mid-1, target);

         else

                   return BS(arr, mid+1, high, target);

}

 

從偽代碼可以看到,二分查找,一個大的問題,可以用一個mid元素,分成左半區,右半區兩個子問題。而左右兩個子問題,只需要解決其中一個,遞歸一次,就能夠解決二分查找全局的問題。

 

通過分治法與減治法的描述,可以發現,分治法的復雜度一般來說是大於減治法的:

快速排序:O(n*lg(n))

二分查找:O(lg(n))

 

話題收回來,快速排序的核心是:

i = partition(arr, low, high);

 

這個partition是干嘛的呢?

顧名思義,partition會把整體分為兩個部分。

更具體的,會用數組arr中的一個元素(默認是第一個元素t=arr[low])為划分依據,將數據arr[low, high]划分成左右兩個子數組:

  • 左半部分,都比t大

  • 右半部分,都比t小

  • 中間位置i是划分元素

以上述TopK的數組為例,先用第一個元素t=arr[low]為划分依據,掃描一遍數組,把數組分成了兩個半區:

  • 左半區比t大

  • 右半區比t小

  • 中間是t

partition返回的是t最終的位置i。

 

很容易知道,partition的時間復雜度是O(n)。

畫外音:把整個數組掃一遍,比t大的放左邊,比t小的放右邊,最后t放在中間N[i]。

 

partition和TopK問題有什么關系呢?

TopK是希望求出arr[1,n]中最大的k個數,那如果找到了第k大的數,做一次partition,不就一次性找到最大的k個數了么?

畫外音:即partition后左半區的k個數。

 

問題變成了arr[1, n]中找到第k大的數。

 

再回過頭來看看第一次partition,划分之后:

i = partition(arr, 1, n);

  • 如果i大於k,則說明arr[i]左邊的元素都大於k,於是只遞歸arr[1, i-1]里第k大的元素即可;

  • 如果i小於k,則說明說明第k大的元素在arr[i]的右邊,於是只遞歸arr[i+1, n]里第k-i大的元素即可;

畫外音:這一段非常重要,多讀幾遍。

 

這就是隨機選擇算法randomized_select,RS,其偽代碼如下:

int RS(arr, low, high, k){

  if(low== high) return arr[low];

  i= partition(arr, low, high);

  temp= i-low; //數組前半部分元素個數

  if(temp>=k)

      return RS(arr, low, i-1, k); //求前半部分第k大

  else

      return RS(arr, i+1, high, k-i); //求后半部分第k-i大

}

 

這是一個典型的減治算法,遞歸內的兩個分支,最終只會執行一個,它的時間復雜度是O(n)。

 

再次強調一下:

  • 分治法,大問題分解為小問題,小問題都要遞歸各個分支,例如:快速排序

  • 減治法,大問題分解為小問題,小問題只要遞歸一個分支,例如:二分查找,隨機選擇

 

通過隨機選擇(randomized_select),找到arr[1, n]中第k大的數,再進行一次partition,就能得到TopK的結果。

 

五、總結

TopK,不難;其思路優化過程,不簡單:

  • 全局排序,O(n*lg(n))

  • 局部排序,只排序TopK個數,O(n*k)

  • 堆,TopK個數也不排序了,O(n*lg(k))

  • 分治法,每個分支“都要”遞歸,例如:快速排序,O(n*lg(n))

  • 減治法,“只要”遞歸一個分支,例如:二分查找O(lg(n)),隨機選擇O(n)

  • TopK的另一個解法:隨機選擇+partition

 

 

 

 

 

 

Top K 算法詳解

應用場景:

        搜索引擎會通過日志文件把用戶每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度為1-255字節。
        假設目前有一千萬個記錄(這些查詢串的重復度比較高,雖然總數是1千萬,但如果除去重復后,不超過3百萬個。一個查詢串的重復度越高,說明查詢它的用戶越多,也就是越熱門。),請你統計最熱門的10個查詢串,要求使用的內存不能超過1G。

必備知識:
什么是哈希表?
        哈希表(Hash table,也叫散列表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。

        也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。

哈希表的做法其實很簡單,就是把Key通過一個固定的算法函數既所謂的哈希函數轉換成一個整型數字,然后就將該數字對數組長度進行取余,取余結果就當作數組的下標,將value存儲在以該數字為下標的數組空間里。
       而當使用哈希表進行查詢的時候,就是再次使用哈希函數將key轉換為對應的數組下標,並定位到該空間獲取value,如此一來,就可以充分利用到數組的定位性能進行數據定位。
問題解析:

要統計最熱門查詢,首先就是要統計每個Query出現的次數,然后根據統計結果,找出Top 10。所以我們可以基於這個思路分兩步來設計該算法。

即,此問題的解決分為以下倆個步驟:

第一步:Query統計              (統計出每個Query出現的次數)
        Query統計有以下倆個方法,可供選擇:
        1、直接排序法                  (經常在日志文件中統計時,使用cat file|format key|sort | uniq -c | sort -nr | head -n 10,就是這種方法)
        首先我們最先想到的的算法就是排序了,首先對這個日志里面的所有Query都進行排序,然后再遍歷排好序的Query,統計每個Query出現的次數了。

但是題目中有明確要求,那就是內存不能超過1G,一千萬條記錄,每條記錄是255Byte,很顯然要占據2.375G內存,這個條件就不滿足要求了。

讓我們回憶一下數據結構課程上的內容,當數據量比較大而且內存無法裝下的時候,我們可以采用外排序的方法來進行排序,這里我們可以采用歸並排序,因為歸並排序有一個比較好的時間復雜度O(NlgN)。

排完序之后我們再對已經有序的Query文件進行遍歷,統計每個Query出現的次數,再次寫入文件中。

綜合分析一下,排序的時間復雜度是O(NlgN),而遍歷的時間復雜度是O(N),因此該算法的總體時間復雜度就是O(N+NlgN)=O(NlgN)。

       2、Hash Table法                (這種方法統計字符串出現的次數非常好)
       在第1個方法中,我們采用了排序的辦法來統計每個Query出現的次數,時間復雜度是NlgN,那么能不能有更好的方法來存儲,而時間復雜度更低呢?

       題目中說明了,雖然有一千萬個Query,但是由於重復度比較高,因此事實上只有300萬的Query,每個Query 255Byte,因此我們可以考慮把他們都放進內存中去,而現在只是需要一個合適的數據結構,在這里,Hash Table絕對是我們優先的選擇,因為Hash Table的查詢速度非常的快,幾乎是O(1)的時間復雜度。

       那么,我們的算法就有了:

               維護一個Key為Query字串,Value為該Query出現次數的HashTable,每次讀取一個Query,如果該字串不在Table中,那么加入該字串,並且將Value值設為1;如果該字串在Table中,那么將該字串的計數加一即可。最終我們在O(N)的時間復雜度內完成了對該海量數據的處理。

                本方法相比算法1:在時間復雜度上提高了一個數量級,為O(N),但不僅僅是時間復雜度上的優化,該方法只需要IO數據文件一次,而算法1的IO次數較多的,因此該算法2比算法1在工程上有更好的可操作性。

     第二步:找出Top 10          (找出出現次數最多的10個)
     算法一:普通排序             (我們只用找出top10,所以全部排序有冗余)
     我想對於排序算法大家都已經不陌生了,這里不在贅述,我們要注意的是排序算法的時間復雜度是NlgN,在本題目中,三百萬條記錄,用1G內存是可以存下的。

     算法二:部分排序         
     題目要求是求出Top 10,因此我們沒有必要對所有的Query都進行排序,我們只需要維護一個10個大小的數組,初始化放入10個Query,按照每個Query的統計次數由大到小排序,然后遍歷這300萬條記錄,每讀一條記錄就和數組最后一個Query對比,如果小於這個Query,那么繼續遍歷,否則,將數組中最后一條數據淘汰(還是要放在合適的位置,保持有序),加入當前的Query。最后當所有的數據都遍歷完畢之后,那么這個數組中的10個Query便是我們要找的Top10了。

      不難分析出,這樣,算法的最壞時間復雜度是N*K, 其中K是指top多少。

       算法三:堆
       在算法二中,我們已經將時間復雜度由NlogN優化到N*K,不得不說這是一個比較大的改進了,可是有沒有更好的辦法呢?

       分析一下,在算法二中,每次比較完成之后,需要的操作復雜度都是K,因為要把元素插入到一個線性表之中,而且采用的是順序比較。這里我們注意一下,該數組是有序的,一次我們每次查找的時候可以采用二分的方法查找,這樣操作的復雜度就降到了logK,可是,隨之而來的問題就是數據移動,因為移動數據次數增多了。不過,這個算法還是比算法二有了改進。

       基於以上的分析,我們想想,有沒有一種既能快速查找,又能快速移動元素的數據結構呢?

       回答是肯定的,那就是堆。
       借助堆結構,我們可以在log量級的時間內查找和調整/移動。因此到這里,我們的算法可以改進為這樣,維護一個K(該題目中是10)大小的小根堆,然后遍歷300萬的Query,分別和根元素進行對比。

思想與上述算法二一致,只是在算法三,我們采用了最小堆這種數據結構代替數組,把查找目標元素的時間復雜度有O(K)降到了O(logK)。
       那么這樣,采用堆數據結構,算法三,最終的時間復雜度就降到了N*logK,和算法二相比,又有了比較大的改進。

總結:

至此,算法就完全結束了,經過上述第一步、先用Hash表統計每個Query出現的次數,O(N);然后第二步、采用堆數據結構找出Top 10,N*O(logK)。所以,我們最終的時間復雜度是:O(N) + N'*O(logK)。(N為1000萬,N’為300萬)。 

 

/

/

 

問題一:

        找出一個無序數組里面前K個最大數
 
算法思想1:

       對數組進行降序全排序,然后返回前K個元素,即是需要的K個最大數。

       排序算法的選擇有很多,考慮數組的無序性,可以考慮選擇快速排序算法,其平均時間復雜度為O(NLogN)。具體代碼實現可以參見相關數據結構與算法書籍。

 

算法思想2(比較好):

         觀察第一種算法,問題只需要找出一個數組里面前K個最大數,而第一種算法對數組進行全排序,不單單找出了前K個最大數,更找出了前N(N為數組大小)個最大數,顯然該算法存在“冗余”,因此基於這樣一個原因,提出了改進的算法二。 

         首先建立一個臨時數組,數組大小為K,從N中讀取K個數,降序全排序(排序算法可以自行選擇,考慮數組的無序性,可以考慮選擇快速排序算法),然后依次讀入其余N - K個數進來和第K名元素比較,大於第K名元素的值則插入到合適位置,數組最后一個元素溢出,反之小於等於第K名元素的值不進行插入操作。只待循環完畢返回臨時數組的K個元素,即是需要的K個最大數。同算法一其平均時間復雜度為O(KLogK + (N - K))。具體代碼實現可以自行完成。

原文:

 

 
問題二:
        有1億個浮點數,請找出其中最大的10000個。
       提示:假設每個浮點數占4個字節,1億個浮點數就要站到相當大的空間,因此不能一次將全部讀入內存進行排序。

       可以發現如果一次讀入那么機器的內存肯定是受不了的,因此我們只有想其他方法解決,解決方式為了高效還是得符合一定的該概率解決,結果並不一定准確,但是應該可以作對大部分的數據。

算法思想1、
       1、我們可以把1億個浮點數利用哈希分為了1000個組(將相同的數字哈希到同一個數組中);

       2、第一次在每個組中找出最大的1W個數,共有1000個;

       3、第二次查詢的時候就是100W個數中再找出最大的1W個數。
       PS:100W個數中再找出最大的1W個數用類似快排的思想搞定。
算法思想2(比較好)、
      1、讀入的頭10000個數,直接創建二叉排序樹。O(1)
      2、對以后每個讀入的數,比較是否比前10000個數中最小的大。(N次比較)如果小的話接着讀下面的數。O(N)
      3、如果大,查找二叉排序樹,找到應當插入的位置。
       4、刪除當前最小的結點。
       5、重復步驟2,直到10億個數全都讀完。
       6、按照中序遍歷輸出當前二叉排序樹中的所有10000個數字。
       基本上算法的時間復雜度是O(N)次比較
       算法的空間復雜度是10000(常數)

       基於上面的想法,可以用最小堆來實現,這樣沒加入一個比10000個樹中最小的數大時的復雜度為log10000.

 

相關類似問題:

1、一個文本文件,大約有一萬行,每行一個詞,要求統計出其中最頻繁出現的前10個詞,請給出思想,給出時間復雜度分析。

     方案1:這題是考慮時間效率。用trie樹(前綴樹)統計每個詞出現的次數,時間復雜度是O(n*le)(le表示單詞的平准長度)。然后是找出出現最頻繁的前10個詞,可以用堆來實現,前面的題中已經講到了,時間復雜度是O(n*lg10)。所以總的時間復雜度,是O(n*le)與O(n*lg10)中較大的哪一個。

 

2、 一個文本文件,找出前10個經常出現的詞,但這次文件比較長,說是上億行或十億行,總之無法一次讀入內存,問最優解。

     方案1:首先根據用hash並求模,將文件分解為多個小文件,對於單個文件利用上題的方法求出每個文件件中10個最常出現的詞。然后再進行歸並處理,找出最終的10個最常出現的詞。

 

3、 100w個數中找出最大的100個數。

    • 方案1:采用局部淘汰法。選取前100個元素,並排序,記為序列L。然后一次掃描剩余的元素x,與排好序的100個元素中最小的元素比,如果比這個最小的要大,那么把這個最小的元素刪除,並把x利用插入排序的思想,插入到序列L中。依次循環,知道掃描了所有的元素。復雜度為O(100w*100)。
    • 方案2:采用快速排序的思想,每次分割之后只考慮比軸大的一部分,知道比軸大的一部分在比100多的時候,采用傳統排序算法排序,取前100個。復雜度為O(100w*100)。
    • 方案3:在前面的題中,我們已經提到了,用一個含100個元素的最小堆完成。復雜度為O(100w*lg100)。

 

 

 

 

 

 

海量數據處理 - 10億個數中找出最大的10000個數(top K問題)

先拿10000個數建堆,然后一次添加剩余元素,如果大於堆頂的數(10000中最小的),將這個數替換堆頂,並調整結構使之仍然是一個最小堆,這樣,遍歷完后,堆中的10000個數就是所需的最大的10000個。建堆時間復雜度是O(mlogm),算法的時間復雜度為O(nmlogm)(n為10億,m為10000)。

        優化的方法:可以把所有10億個數據分組存放,比如分別放在1000個文件中。這樣處理就可以分別在每個文件的10^6個數據中找出最大的10000個數,合並到一起在再找出最終的結果。

        以上就是面試時簡單提到的內容,下面整理一下這方面的問題:

top K問題

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

        針對top K類問題,通常比較好的方案是分治+Trie樹/hash+小頂堆(就是上面提到的最小堆),即先將數據集按照Hash方法分解成多個小數據集,然后使用Trie樹活着Hash統計每個小數據集中的query詞頻,之后用小頂堆求出每個數據集中出現頻率最高的前K個數,最后在所有top K中求出最終的top K。

eg:有1億個浮點數,如果找出期中最大的10000個?

        最容易想到的方法是將數據全部排序,然后在排序后的集合中進行查找,最快的排序算法的時間復雜度一般為O(nlogn),如快速排序。但是在32位的機器上,每個float類型占4個字節,1億個浮點數就要占用400MB的存儲空間,對於一些可用內存小於400M的計算機而言,很顯然是不能一次將全部數據讀入內存進行排序的。其實即使內存能夠滿足要求(我機器內存都是8GB),該方法也並不高效,因為題目的目的是尋找出最大的10000個數即可,而排序卻是將所有的元素都排序了,做了很多的無用功。

        第二種方法為局部淘汰法,該方法與排序方法類似,用一個容器保存前10000個數,然后將剩余的所有數字——與容器內的最小數字相比,如果所有后續的元素都比容器內的10000個數還小,那么容器內這個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個,繼續對大堆快速排序一次分成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(常數)。

實際運行:

        實際上,最優的解決方案應該是最符合實際設計需求的方案,在時間應用中,可能有足夠大的內存,那么直接將數據扔到內存中一次性處理即可,也可能機器有多個核,這樣可以采用多線程處理整個數據集。

       下面針對不容的應用場景,分析了適合相應應用場景的解決方案。

(1)單機+單核+足夠大內存

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

(2)單機+多核+足夠大內存

        這時可以直接在內存總使用Hash方法將數據划分成n個partition,每個partition交給一個線程處理,線程的處理邏輯同(1)類似,最后一個線程將結果歸並。

        該方法存在一個瓶頸會明顯影響效率,即數據傾斜。每個線程的處理速度可能不同,快的線程需要等待慢的線程,最終的處理速度取決於慢的線程。而針對此問題,解決的方法是,將數據划分成c×n個partition(c>1),每個線程處理完當前partition后主動取下一個partition繼續處理,知道所有數據處理完畢,最后由一個線程進行歸並。

(3)單機+單核+受限內存

        這種情況下,需要將原數據文件切割成一個一個小文件,如次啊用hash(x)%M,將原文件中的數據切割成M小文件,如果小文件仍大於內存大小,繼續采用Hash的方法對數據文件進行分割,知道每個小文件小於內存大小,這樣每個文件可放到內存中處理。采用(1)的方法依次處理每個小文件。

(4)多機+受限內存

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

 

        從實際應用的角度考慮,(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)有10000000個記錄,這些查詢串的重復度比較高,如果除去重復后,不超過3000000個。一個查詢串的重復度越高,說明查詢它的用戶越多,也就是越熱門。請統計最熱門的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的內存,即可以只用12.375MB的內存表示所有的8位數電話號碼的內容。

 

 

 

 

 

 

 

怎么在海量數據中找出重復次數最多的一個

1、海量日志數據,提取出某日訪問百度次數最多的那個IP。

  此題,在我之前的一篇文章算法里頭有所提到,當時給出的方案是:IP的數目還是有限的,最多2^32個,所以可以考慮使用hash將ip直接存入內存,然后進行統計。

  再詳細介紹下此方案:首先是這一天,並且是訪問百度的日志中的IP取出來,逐個寫入到一個大文件中。注意到IP是32位的,最多有個2^32個 IP。同樣可以采用映射的方法,比如模1000,把整個大文件映射為1000個小文件,再找出每個小文中出現頻率最大的IP(可以采用hash_map進行頻率統計,然后再找出頻率最大的幾個)及相應的頻率。然后再在這1000個最大的IP中,找出那個頻率最大的IP,即為所求。

  2、搜索引擎會通過日志文件把用戶每次檢索使用的所有檢索串都記錄下來,每個查詢串的長度為1-255字節。

  假設目前有一千萬個記錄(這些查詢串的重復度比較高,雖然總數是1千萬,但如果除去重復后,不超過3百萬個。一個查詢串的重復度越高,說明查詢它的用戶越多,也就是越熱門。),請你統計最熱門的10個查詢串,要求使用的內存不能超過1G。

  典型的Top K算法,還是在這篇文章里頭有所闡述。 文中,給出的最終算法是:第一步、先對這批海量數據預處理,在O(N)的時間內用Hash表完成排序;然后,第二步、借助堆這個數據結構,找出Top K,時間復雜度為N‘logK。 即,借助堆結構,我們可以在log量級的時間內查找和調整/移動。因此,維護一個K(該題目中是10)大小的小根堆,然后遍歷300萬的Query,分別和根元素進行對比所以,我們最終的時間復雜度是:O(N) + N'*O(logK),(N為1000萬,N’為300萬)。ok,更多,詳情,請參考原文。

  或者:采用trie樹,關鍵字域存該查詢串出現的次數,沒有出現為0。最后用10個元素的最小推來對出現頻率進行排序。

  3、有一個1G大小的一個文件,里面每一行是一個詞,詞的大小不超過16字節,內存限制大小是1M。返回頻數最高的100個詞。

  方案:順序讀文件中,對於每個詞x,取hash(x)%5000,然后按照該值存到5000個小文件(記為x0,x1,...x4999)中。這樣每個文件大概是200k左右。

  如果其中的有的文件超過了1M大小,還可以按照類似的方法繼續往下分,直到分解得到的小文件的大小都不超過1M。 對每個小文件,統計每個文件中出現的詞以及相應的頻率(可以采用trie樹/hash_map等),並取出出現頻率最大的100個詞(可以用含100個結點的最小堆),並把100個詞及相應的頻率存入文件,這樣又得到了5000個文件。下一步就是把這5000個文件進行歸並(類似與歸並排序)的過程了。

  4、有10個文件,每個文件1G,每個文件的每一行存放的都是用戶的query,每個文件的query都可能重復。要求你按照query的頻度排序。

  還是典型的TOP K算法,解決方案如下: 方案1: 順序讀取10個文件,按照hash(query)%10的結果將query寫入到另外10個文件(記為)中。這樣新生成的文件每個的大小大約也1G(假設 hash函數是隨機的)。 找一台內存在2G左右的機器,依次對用hash_map(query, query_count)來統計每個query出現的次數。利用快速/堆/歸並排序按照出現次數進行排序。將排序好的query和對應的 query_cout輸出到文件中。這樣得到了10個排好序的文件(記為)。

  對這10個文件進行歸並排序(內排序與外排序相結合)。

  方案2: 一般query的總量是有限的,只是重復的次數比較多而已,可能對於所有的query,一次性就可以加入到內存了。這樣,我們就可以采用trie樹/hash_map等直接來統計每個query出現的次數,然后按出現次數做快速/堆/歸並排序就可以了。

  方案3: 與方案1類似,但在做完hash,分成多個文件后,可以交給多個文件來處理,采用分布式的架構來處理(比如MapReduce),最后再進行合並。

  5、 給定a、b兩個文件,各存放50億個url,每個url各占64字節,內存限制是4G,讓你找出a、b文件共同的url?

  方案1:可以估計每個文件安的大小為5G×64=320G,遠遠大於內存限制的4G。所以不可能將其完全加載到內存中處理。考慮采取分而治之的方法。

  遍歷文件a,對每個url求取hash(url)%1000,然后根據所取得的值將url分別存儲到1000個小文件(記為a0,a1,...,a999)中。這樣每個小文件的大約為300M。

  遍歷文件b,采取和a相同的方式將url分別存儲到1000小文件(記為b0,b1,...,b999)。這樣處理后,所有可能相同的url都在對應的小文件(a0vsb0,a1vsb1,...,a999vsb999)中,不對應的小文件不可能有相同的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(注意會有一定的錯誤率)。

  Bloom filter日后會在本BLOG內詳細闡述。

  6、在2.5億個整數中找出不重復的整數,注,內存不足以容納這2.5億個整數。

  方案1:采用2-Bitmap(每個數分配2bit,00表示不存在,01表示出現一次,10表示多次,11無意義)進行,共需內存內存,還可以接受。然后掃描這2.5億個整數,查看Bitmap中相對應位,如果是00變01,01變10,10保持不變。所描完事后,查看bitmap,把對應位是01的整數輸出即可。

  方案2:也可采用與第1題類似的方法,進行划分小文件的方法。然后在小文件中找出不重復的整數,並排序。然后再進行歸並,注意去除重復的元素。

  7、騰訊面試題:給40億個不重復的unsigned int的整數,沒排過序的,然后再給一個數,如何快速判斷這個數是否在那40億個數當中?

  與上第6題類似,我的第一反應時快速排序+二分查找。以下是其它更好的方法: 方案1:oo,申請512M的內存,一個bit位代表一個unsigned int值。讀入40億個數,設置相應的bit位,讀入要查詢的數,查看相應bit位是否為1,為1表示存在,為0表示不存在。

  dizengrong: 方案2:這個問題在《編程珠璣》里有很好的描述,大家可以參考下面的思路,探討一下:又因為2^32為40億多,所以給定一個數可能在,也可能不在其中;這里我們把40億個數中的每一個用32位的二進制來表示假設這40億個數開始放在一個文件中。

  然后將這40億個數分成兩類: 1.最高位為0 2.最高位為1 並將這兩類分別寫入到兩個文件中,其中一個文件中數的個數<=20億,而另一個>=20億(這相當於折半了);與要查找的數的最高位比較並接着進入相應的文件再查找

再然后把這個文件為又分成兩類: 1.次最高位為0 2.次最高位為1

  並將這兩類分別寫入到兩個文件中,其中一個文件中數的個數<=10億,而另一個>=10億(這相當於折半了); 與要查找的數的次最高位比較並接着進入相應的文件再查找。 ....... 以此類推,就可以找到了,而且時間復雜度為O(logn),方案2完。

  附:這里,再簡單介紹下,位圖方法: 使用位圖法判斷整形數組是否存在重復 判斷集合中存在重復是常見編程任務之一,當集合中數據量比較大時我們通常希望少進行幾次掃描,這時雙重循環法就不可取了。

  位圖法比較適合於這種情況,它的做法是按照集合中最大元素max創建一個長度為max+1的新數組,然后再次掃描原數組,遇到幾就給新數組的第幾位置上1,如遇到5就給新數組的第六個元素置1,這樣下次再遇到5想置位時發現新數組的第六個元素已經是1了,這說明這次的數據肯定和以前的數據存在着重復。這種給新數組初始化時置零其后置一的做法類似於位圖的處理方法故稱位圖法。它的運算次數最壞的情況為2N。如果已知數組的最大值即能事先給新數組定長的話效率還能提高一倍。

  8、怎么在海量數據中找出重復次數最多的一個?

   方案1:先做hash,然后求模映射為小文件,求出每個小文件中重復次數最多的一個,並記錄重復次數。然后找出上一步求出的數據中重復次數最多的一個就是所求(具體參考前面的題)。

  9、上千萬或上億數據(有重復),統計其中出現次數最多的錢N個數據。

  方案1:上千萬或上億的數據,現在的機器的內存應該能存下。所以考慮采用hash_map/搜索二叉樹/紅黑樹等來進行統計次數。然后就是取出前N個出現次數最多的數據了,可以用第2題提到的堆機制完成。

  10、一個文本文件,大約有一萬行,每行一個詞,要求統計出其中最頻繁出現的前10個詞,請給出思想,給出時間復雜度分析。

  方案1:這題是考慮時間效率。用trie樹統計每個詞出現的次數,時間復雜度是O(n*le)(le表示單詞的平准長度)。然后是找出出現最頻繁的前10個詞,可以用堆來實現,前面的題中已經講到了,時間復雜度是O(n*lg10)。所以總的時間復雜度,是O(n*le)與O(n*lg10)中較大的哪一個。

  附、100w個數中找出最大的100個數。

  方案1:在前面的題中,我們已經提到了,用一個含100個元素的最小堆完成。復雜度為O(100w*lg100)。

  方案2:采用快速排序的思想,每次分割之后只考慮比軸大的一部分,知道比軸大的一部分在比100多的時候,采用傳統排序算法排序,取前100個。復雜度為O(100w*100)。

  方案3:采用局部淘汰法。選取前100個元素,並排序,記為序列L。然后一次掃描剩余的元素x,與排好序的100個元素中最小的元素比,如果比這個最小的要大,那么把這個最小的元素刪除,並把x利用插入排序的思想,插入到序列L中。依次循環,知道掃描了所有的元素。復雜度為 O(100w*100)。

第二部分、十個海量數據處理方法大總結

  ok,看了上面這么多的面試題,是否有點頭暈。是的,需要一個總結。接下來,本文將簡單總結下一些處理海量數據問題的常見方法。

  下面的方法全部來自http://hi.baidu.com/yanxionglu/blog/博客,對海量數據的處理方法進行了一個一般性的總結,當然這些方法可能並不能完全覆蓋所有的問題,但是這樣的一些方法也基本可以處理絕大多數遇到的問題。下面的一些問題基本直接來源於公司的面試筆試題目,方法不一定最優,如果你有更好的處理方法,歡迎討論。

  一、Bloom filter

  適用范圍:可以用來實現數據字典,進行數據的判重,或者集合求交集

  基本原理及要點:

  對於原理來說很簡單,位數組+k個獨立hash函數。將hash函數對應的值的位數組置1,查找時如果發現所有hash函數對應位都是1說明存在,很明顯這個過程並不保證查找的結果是100%正確的。同時也不支持刪除一個已經插入的關鍵字,因為該關鍵字對應的位會牽動到其他的關鍵字。所以一個簡單的改進就是 counting Bloom filter,用一個counter數組代替位數組,就可以支持刪除了。

  還有一個比較重要的問題,如何根據輸入元素個數n,確定位數組m的大小及hash函數個數。當hash函數個數k=(ln2)*(m/n)時錯誤率最小。在錯誤率不大於E的情況下,m至少要等於n*lg(1/E)才能表示任意n個元素的集合。但m還應該更大些,因為還要保證bit數組里至少一半為0,則m應該>=nlg(1/E)*lge 大概就是nlg(1/E)1.44倍(lg表示以2為底的對數)。

  舉個例子我們假設錯誤率為0.01,則此時m應大概是n的13倍。這樣k大概是8個。

  注意這里m與n的單位不同,m是bit為單位,而n則是以元素個數為單位(准確的說是不同元素的個數)。通常單個元素的長度都是有很多bit的。所以使用bloom filter內存上通常都是節省的。

  擴展:

  Bloom filter將集合中的元素映射到位數組中,用k(k為哈希函數個數)個映射位是否全1表示元素在不在這個集合中。Counting bloom filter(CBF)將位數組中的每一位擴展為一個counter,從而支持了元素的刪除操作。Spectral Bloom Filter(SBF)將其與集合元素的出現次數關聯。SBF采用counter中的最小值來近似表示元素的出現頻率。

  問題實例:給你A,B兩個文件,各存放50億條URL,每條URL占用64字節,內存限制是4G,讓你找出A,B文件共同的URL。如果是三個乃至n個文件呢?

  根據這個問題我們來計算下內存的占用,4G=2^32大概是40億*8大概是340億,n=50億,如果按出錯率0.01算需要的大概是650 億個bit。現在可用的是340億,相差並不多,這樣可能會使出錯率上升些。另外如果這些urlip是一一對應的,就可以轉換成ip,則大大簡單了。

  二、Hashing

  適用范圍:快速查找,刪除的基本數據結構,通常需要總數據量可以放入內存

  基本原理及要點:

  hash函數選擇,針對字符串,整數,排列,具體相應的hash方法。

  碰撞處理,一種是open hashing,也稱為拉鏈法;另一種就是closed hashing,也稱開地址法,opened addressing。

  擴展:

  d-left hashing中的d是多個的意思,我們先簡化這個問題,看一看2-left hashing。2-left hashing指的是將一個哈希表分成長度相等的兩半,分別叫做T1和T2,給T1和T2分別配備一個哈希函數,h1和h2。在存儲一個新的key時,同時用兩個哈希函數進行計算,得出兩個地址h1[key]和h2[key]。這時需要檢查T1中的h1[key]位置和T2中的h2[key]位置,哪一個位置已經存儲的(有碰撞的)key比較多,然后將新key存儲在負載少的位置。如果兩邊一樣多,比如兩個位置都為空或者都存儲了一個key,就把新key 存儲在左邊的T1子表中,2-left也由此而來。在查找一個key時,必須進行兩次hash,同時查找兩個位置。

  問題實例:

  1).海量日志數據,提取出某日訪問百度次數最多的那個IP。

  IP的數目還是有限的,最多2^32個,所以可以考慮使用hash將ip直接存入內存,然后進行統計。

  三、bit-map

  適用范圍:可進行數據的快速查找,判重,刪除,一般來說數據范圍是int的10倍以下

  基本原理及要點:使用bit數組來表示某些元素是否存在,比如8位電話號碼

  擴展:bloom filter可以看做是對bit-map的擴展

  問題實例:

  1)已知某個文件內包含一些電話號碼,每個號碼為8位數字,統計不同號碼的個數。

  8位最多99 999 999,大概需要99m個bit,大概10幾m字節的內存即可。

  2)2.5億個整數中找出不重復的整數的個數,內存空間不足以容納這2.5億個整數。

  將bit-map擴展一下,用2bit表示一個數即可,0表示未出現,1表示出現一次,2表示出現2次及以上。或者我們不用2bit來進行表示,我們用兩個bit-map即可模擬實現這個2bit-map。

  四、堆

  適用范圍:海量數據前n大,並且n比較小,堆可以放入內存

  基本原理及要點:最大堆求前n小,最小堆求前n大。方法,比如求前n小,我們比較當前元素與最大堆里的最大元素,如果它小於最大元素,則應該替換那個最大元素。這樣最后得到的n個元素就是最小的n個。適合大數據量,求前n小,n的大小比較小的情況,這樣可以掃描一遍即可得到所有的前n元素,效率很高。

  擴展:雙堆,一個最大堆與一個最小堆結合,可以用來維護中位數。

  問題實例:

  1)100w個數中找最大的前100個數。

  用一個100個元素大小的最小堆即可。

  五、雙層桶划分----其實本質上就是【分而治之】的思想,重在分的技巧上!

  適用范圍:第k大,中位數,不重復或重復的數字

  基本原理及要點:因為元素范圍很大,不能利用直接尋址表,所以通過多次划分,逐步確定范圍,然后最后在一個可以接受的范圍內進行。可以通過多次縮小,雙層只是一個例子。

  擴展:

  問題實例:

  1).2.5億個整數中找出不重復的整數的個數,內存空間不足以容納這2.5億個整數。

  有點像鴿巢原理,整數個數為2^32,也就是,我們可以將這2^32個數,划分為2^8個區域(比如用單個文件代表一個區域),然后將數據分離到不同的區域,然后不同的區域在利用bitmap就可以直接解決了。也就是說只要有足夠的磁盤空間,就可以很方便的解決。

  2).5億個int找它們的中位數。

  這個例子比上面那個更明顯。首先我們將int划分為2^16個區域,然后讀取數據統計落到各個區域里的數的個數,之后我們根據統計結果就可以判斷中位數落到那個區域,同時知道這個區域中的第幾大數剛好是中位數。然后第二次掃描我們只統計落在這個區域中的那些數就可以了。

  實際上,如果不是int是int64,我們可以經過3次這樣的划分即可降低到可以接受的程度。即可以先將int64分成2^24個區域,然后確定區域的第幾大數,在將該區域分成2^20個子區域,然后確定是子區域的第幾大數,然后子區域里的數的個數只有2^20,就可以直接利用direct addr table進行統計了。

  六、數據庫索引

  適用范圍:大數據量的增刪改查

  基本原理及要點:利用數據的設計實現方法,對海量數據的增刪改查進行處理。

  七、倒排索引(Inverted index)

  適用范圍:搜索引擎,關鍵字查詢

  基本原理及要點:為何叫倒排索引?一種索引方法,被用來存儲在全文搜索下某個單詞在一個文檔或者一組文檔中的存儲位置的映射。

 以英文為例,下面是要被索引的文本: T0 = "it is what it is" T1 = "what is it" T2 = "it is a banana"

我們就能得到下面的反向文件索引:

"a": {2} "banana": {2} "is": {0, 1, 2} "it": {0, 1, 2} "what": {0, 1}

 檢索的條件"what","is"和"it"將對應集合的交集。

  正向索引開發出來用來存儲每個文檔的單詞的列表。正向索引的查詢往往滿足每個文檔有序頻繁的全文查詢和每個單詞在校驗文檔中的驗證這樣的查詢。在正向索引中,文檔占據了中心的位置,每個文檔指向了一個它所包含的索引項的序列。也就是說文檔指向了它包含的那些單詞,而反向索引則是單詞指向了包含它的文檔,很容易看到這個反向的關系。

  擴展:

  問題實例:文檔檢索系統,查詢那些文件包含了某單詞,比如常見的學術論文的關鍵字搜索。

  八、外排序

  適用范圍:大數據的排序,去重

  基本原理及要點:外排序的歸並方法,置換選擇敗者樹原理,最優歸並樹

  擴展:

  問題實例:

  1).有一個1G大小的一個文件,里面每一行是一個詞,詞的大小不超過16個字節,內存限制大小是1M。返回頻數最高的100個詞。

  這個數據具有很明顯的特點,詞的大小為16個字節,但是內存只有1m做hash有些不夠,所以可以用來排序。內存可以當輸入緩沖區使用。

  九、trie樹

  適用范圍:數據量大,重復多,但是數據種類小可以放入內存

  基本原理及要點:實現方式,節點孩子的表示方式

  擴展:壓縮實現。

  問題實例:

  1).有10個文件,每個文件1G,每個文件的每一行都存放的是用戶的query,每個文件的query都可能重復。要你按照query的頻度排序。

  2).1000萬字符串,其中有些是相同的(重復),需要把重復的全部去掉,保留沒有重復的字符串。請問怎么設計和實現?

  3).尋找熱門查詢:查詢串的重復度比較高,雖然總數是1千萬,但如果除去重復后,不超過3百萬個,每個不超過255字節。

  十、分布式處理 mapreduce

  適用范圍:數據量大,但是數據種類小可以放入內存

  基本原理及要點:將數據交給不同的機器去處理,數據划分,結果歸約。

  擴展:

  問題實例:

  1).The canonical example application of MapReduce is a process to count the appearances ofeach different word in a set of documents:

  2).海量數據分布在100台電腦中,想個辦法高效統計出這批數據的TOP10。

  3).一共有N個機器,每個機器上有N個數。每個機器最多存O(N)個數並對它們操作。如何找到N^2個數的中數(median)?

  經典問題分析

  上千萬or億數據(有重復),統計其中出現次數最多的前N個數據,分兩種情況:可一次讀入內存,不可一次讀入。

  可用思路:trie樹+堆,數據庫索引,划分子集分別統計,hash,分布式計算,近似統計,外排序

  所謂的是否能一次讀入內存,實際上應該指去除重復后的數據量。如果去重后數據可以放入內存,我們可以為數據建立字典,比如通過 map,hashmap,trie,然后直接進行統計即可。當然在更新每條數據的出現次數的時候,我們可以利用一個堆來維護出現次數最多的前N個數據,當然這樣導致維護次數增加,不如完全統計后在求前N大效率高。

  如果數據無法放入內存。一方面我們可以考慮上面的字典方法能否被改進以適應這種情形,可以做的改變就是將字典存放到硬盤上,而不是內存,這可以參考數據庫的存儲方法。

  當然還有更好的方法,就是可以采用分布式計算,基本上就是map-reduce過程,首先可以根據數據值或者把數據hash(md5)后的值,將數據按照范圍划分到不同的機子,最好可以讓數據划分后可以一次讀入內存,這樣不同的機子負責處理各種的數值范圍,實際上就是map。得到結果后,各個機子只需拿出各自的出現次數最多的前N個數據,然后匯總,選出所有的數據中出現次數最多的前N個數據,這實際上就是reduce過程。

  實際上可能想直接將數據均分到不同的機子上進行處理,這樣是無法得到正確的解的。因為一個數據可能被均分到不同的機子上,而另一個則可能完全聚集到一個機子上,同時還可能存在具有相同數目的數據。比如我們要找出現次數最多的前100個,我們將1000萬的數據分布到10台機器上,找到每台出現次數最多的前 100個,歸並之后這樣不能保證找到真正的第100個,因為比如出現次數最多的第100個可能有1萬個,但是它被分到了10台機子,這樣在每台上只有1千個,假設這些機子排名在1000個之前的那些都是單獨分布在一台機子上的,比如有1001個,這樣本來具有1萬個的這個就會被淘汰,即使我們讓每台機子選出出現次數最多的1000個再歸並,仍然會出錯,因為可能存在大量個數為1001個的發生聚集。因此不能將數據隨便均分到不同機子上,而是要根據hash 后的值將它們映射到不同的機子上處理,讓不同的機器處理一個數值范圍。

    而外排序的方法會消耗大量的IO,效率不會很高。而上面的分布式方法,也可以用於單機版本,也就是將總的數據根據值的范圍,划分成多個不同的子文件,然后逐個處理。處理完畢之后再對這些單詞的及其出現頻率進行一個歸並。實際上就可以利用一個外排序的歸並過程。

    另外還可以考慮近似計算,也就是我們可以通過結合自然語言屬性,只將那些真正實際中出現最多的那些詞作為一個字典,使得這個規模可以放入內存。 

 


免責聲明!

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



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