10億個數中找出最大的10000個數(top K問題)

這個問題仍是創建最小堆比較好一些。java

        先拿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次這樣的比較。dom

        第四種方法是Hash法。若是這1億個書裏面有不少重複的數,先經過Hash法,把這1億個數字去重複,這樣若是重複率很高的話,會減小很大的內存用量,從而縮小運算空間,而後經過分治法或最小堆法查找最大的10000個數。socket

        第五種方法採用最小堆。首先讀入前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位數電話號碼的內容。

 

算法一:冒泡排序法

  千里之行,始於足下。咱們先不說最好,甚至不說好。咱們只問,如何「從10000個整數中找出最大的10個」?我最早想到的是用冒泡排序的辦法:咱們從頭至尾走10趟,天然會把最大的10個數找到。方法簡單,就再也不這裏寫代碼了。這個算法的複雜度是10N(N=10000)。

算法二:

  有沒有更好一點的算法呢?固然。維持一個長度爲10的降序數組,每個從數組拿到的數字都與這個降序數組的最小值比較。若是小於最小值,就捨棄;若是大於最小值,就把它插入到降序數組中的合適位置,捨棄原來的最小值。這樣,遍歷一遍就能夠找到最大的10個數。由於須要在降序數組中插入一個數,對於遍歷的每一個數可能都須要這樣,因此其複雜爲5N。

  僞代碼以下:

  A[N],a[m](分別爲原始數組和降序數組,其中N=10000,m=10)

  a = A[0 ... 9](將數組A的前10個數賦給數組a)

  sort a(將組數a降序排序)

  for i in A[ 10 ... N](從10到N遍歷數組A)

    if A[i] > a[9] then (若是當前值比降序數組中的最小值大)

      刪除a[9]

      將A[i]插入a的合適位置,使a保持降序

    end if

  end for

  輸出數組a

  其實算法二還有一個優勢,就是當數組很大時,能夠將數據分段讀入內存處理,而這樣作並不影響結果。

 

一、首先一點,對於海量數據處理,思路基本上是肯定的,必須分塊處理,而後再合併起來。

二、對於每一塊必須找出10個最大的數,由於第一塊中10個最大數中的最小的,可能比第二塊中10最大數中的最大的還要大。

三、分塊處理,再合併。也就是Google MapReduce 的基本思想。Google有不少的服務器,每一個服務器又有不少的CPU,所以,100億個數分紅100塊,每一個服務器處理一塊,1億個數分紅100塊,每一個CPU處理一塊。而後再從下往上合併。注意:分塊的時候,要保證塊與塊之間獨立,沒有依賴關係,不然不能徹底並行處理,線程之間要互斥。另一點,分塊處理過程當中,不要有反作用,也就是不要修改原數據,不然下次計算結果就不同了。

四、上面講了,對於海量數據,使用多個服務器,多個CPU能夠並行,顯著提升效率。對於單個服務器,單個CPU有沒有意義呢?

  也有很大的意義。若是不分塊,至關於對100億個數字遍歷,做比較。這中間存在大量的沒有必要的比較。能夠舉個例子說明,全校高一有100個班,我想找出全校前10名的同窗,很傻的辦法就是,把高一100個班的同窗成績都取出來,做比較,這個比較數據量太大了。應該很容易想到,班裏的第11名,不多是全校的前10名。也就是說,不是班裏的前10名,就不多是全校的前10名。所以,只須要把每一個班裏的前10取出來,做比較就好了,這樣比較的數據量就大大地減小了。

以前看到一個面試題,1000個亂序正整數中找出10個最小值,要求高效快速,不能使用API。

              這學期學了些排序的方法,什麼快速排序啊,冒泡啊,歸併排序啊之類的。看到這個題的時候,第一反應是快速排序,由於它的名字叫快速嘛,應該夠快吧。而後又想到這個題目不是要你完整的排序,只須要求出最小的10個就夠了,那麼冒泡法呢?冒泡法只冒前面10個數?

             發現這樣子仍是要遍歷比較1000*10次。

             而後,利用相似於數據結構裏的棧的特性?把這1000個數的前10個放到一個數組種,排好序,而後,剩下的990個數字,每一個都與這10個數的最大數比較,大於則不考慮,小於就把最大值拿出來,把這個數放進去,這樣的話,冒泡排序的比較次數就變成了990次,好像高效不少哦?

             將題目裏的1000和10設定爲由用戶輸入的n和m,產生了個人下面這個程序(沒有考慮m>n的狀況)

 

[java] import java.util.Random;  import java.util.Scanner;        public class Main {        private static int n;      private static int m;      private static int []num;      private static int []min;      static Scanner in = new Scanner(System.in);            public static void main(String[] args) {          int con = 1;          while(con == 1) {              getAns();              System.out.println("\nContinue?(0:No; 1:Yes)");              con = in.nextInt();          }      }            public static void getAns() {          System.out.println("Enter n and m :");          n = in.nextInt();          m = in.nextInt();          num = new int[n];             min = new int[m];                    //隨機生成n個整數並讓前m個數默認爲是這n個數中最小的m個數           Random rand = new Random();          for(int i = 0; i < n; i++) {              num[i] = rand.nextInt((int) Math.pow(2, 10));              if(i < m) {                  min[i] = num[i];              }                        }          //冒泡排序,對前m個數順序排列           sort1();                    //求的這最小的m個數           getMin();                    System.out.println("m個最小值分別爲 :");          for(int i = 0; i < m; i++) {              System.out.print(min[i] + "\t");              if((i+1) % 10 == 0) {                  System.out.println();              }          }      }            public static void sort1() {          int temp;          int k;          for(int i = 0; i < m; i++) {              temp = i;              for(int j = i+1; j < m; j++) {                  if(min[temp] > min[j]) {                      temp = j;                  }              }              if(temp != i) {                  k = min[temp];                  min[temp] = min[i];                  min[i] = k;              }          }      }            public static void getMin() {          int temp;          for(int i = m; i < n; i++) {              if(num[i] < min[m-1]) {                  temp = num[i];                  num[i] = min[m-1];                  min[m-1] = temp;                  //更換了數據以後,高效率的從新讓數據順序排列                   sort2();              }          }      }            //比最大者小的數放在最大值的位置,此時要想辦法將這個數放在合適的位置       public static void sort2() {          int x = m - 1;          int temp;          while(x > 0 && min[x] < min[x-1]) {              temp = min[x];              min[x] = min[x-1];              min[x-1] = temp;              x--;          }      }    }

相關文章
相關標籤/搜索