20 億個數(shù)字在 4G 內(nèi)存中如何去重排序:快來試一試 BitMap

有一道流傳廣泛的面試題:
給你一臺 4G 內(nèi)存的機器翘地,一組 20 億個無序正整數(shù)浑玛,如何快速地判斷一個正整數(shù) N 是否在這組數(shù)字中甲锡?或者如何快速地對這組數(shù)據(jù)排重后排序?
讓我們先算算 20 億個整數(shù)會占用多大的內(nèi)存空間中燥,Java 的 int 類型占用 4 個字節(jié)蝙眶,那么 20 億 * 4 再換算成 G 大約是 7.5G,大于題目中 4G 內(nèi)存的限制褪那,無法一次性地放到內(nèi)存中幽纷;
這時候有些伙伴會說:“把數(shù)據(jù)放到磁盤上,然后分批將數(shù)據(jù)讀取到內(nèi)存中就行查詢”博敬,但是這種方法會導致多次磁盤 IO友浸,而且只能解決第一個查找的問題,排序就沒有辦法做到了偏窝。

BitMap 的概念

BitMap 能夠很好地解決這個問題收恢;它是用一個 Bit 位來標記某個元素對應的 Value, 而 Key 即是該元素祭往,比如我們初始化一個類型為 bit伦意、長度為 8 的數(shù)組,數(shù)組下標 0-7硼补,數(shù)組中的內(nèi)容 1 表示存在驮肉,0 表示不存在,那么:


下標 5 對應的位置是 1已骇,表示 5

00000001 下標為 0 的位置离钝,對應值是1,那么表示 0褪储;同理:
00000010 表示 1卵渴;
00000100 表示 2;
00001000 表示 3鲤竹;
...
10000000 表示 7浪读;

如果一組數(shù)據(jù) {2,3,4,7} 放到同一個數(shù)組中的話,就是 10011100:


下標 2,3,4,7 對應的位置是 1,表示了這 4 個數(shù)字

如果按照 int 數(shù)組存儲碘橘,{2,3,4,7} 需要 4 * 4 * 8 個 bit 才能存儲的數(shù)據(jù)互订,但是現(xiàn)在 BitMap 只需要 8 個 bit 就可以存儲,很大地節(jié)省了存儲空間蛹屿,并且排重后的排序也變的非常簡單了屁奏;如果用 byte 實現(xiàn)的話,只需要 1 個 byte 就可以(1 byte = 8 bits)错负。

如果增加了一個數(shù)字 10 呢坟瓢,那么 1 個 byte 就不夠了:


image.png

數(shù)據(jù)結(jié)構(gòu)及初始化

我們可以得知,BitMap 的容量大小取決于最大的那個數(shù)值犹撒,比如要存儲 {2,3,4,7,10}:

  • 如果用 bit 數(shù)組實現(xiàn)(假如有的話)折联,那么需要 10 + 1 個長度;
  • 如果是用 byte 數(shù)組實現(xiàn)识颊,那么需要 10/8 + 1 個長度诚镰;
  • 如果是用 int 數(shù)組實現(xiàn),那么就需要 10/32 + 1 個長度(1 個 int 等于 4 個 bytes祥款,等于 32 個 bits)清笨;

明白了這點之后,一個簡單的 BitMap 數(shù)據(jù)結(jié)構(gòu)也就可以確定了:

public class BitMap {
    //數(shù)據(jù)
    private byte[] bits; 
    //最大值
    private int max_value;
    //容量
    private int capacity;
    
    /**
     * 初始化
     * @param capacity
     */
    public BitMap(int max_value){
        this.max_value = max_value;
        //1bit存儲8個數(shù)據(jù)刃跛,存儲最大值為 max_value 的數(shù)組需要 max_value/8+1 個 byte抠艾,除以8就是右移3位
        this.capacity = (max_value >> 3 ) + 1;
        bits = new byte[capacity];
    }
}

添加數(shù)據(jù)

添加數(shù)據(jù),需要快速地定位到這個元素要存到整個數(shù)組中的哪個位置桨昙,這里有兩個概念:

  • 索引號 index:數(shù)據(jù)保存在整個數(shù)組的哪個下標中检号;
  • 位置號 position:數(shù)據(jù)在這個下標元素的哪個位置;

比如 10 保存在 index = 1蛙酪,position = 2(從 0 開始) 這個位置中齐苛,經(jīng)推算可得:

index = N / 8
position = N % 8
添加數(shù)據(jù)

知道了 10 保存的位置之后,怎么把對應位置的數(shù)據(jù)更改成 1 呢桂塞?可以用“位或”運算凹蜂。將 10 添加到 BitMap 中的完整步驟如下:

  • 計算 index = 10/8 = 1 ;
  • 計算 position = 10%8 = 2 藐俺;
  • 將 byte[1] 的數(shù)據(jù)與 0000100 做“位或”運算炊甲,其中 0000100 是通過對 1 左移 2 得到。

完整的代碼如下:

public void add(int num){
    //數(shù)據(jù)保存在整個數(shù)組的哪個下標中
    int index = num / 8;
    //數(shù)據(jù)在這個下標元素的哪個位置
    int position = num % 8;
    
    bits[index] |= 1<<position;
}

判斷數(shù)字是否存在

知道了如何判斷數(shù)字的索引號和位置號之后欲芹,判斷數(shù)字是否存在也就容易了,直接使用“位與”運算吟吝,代碼如下:

public boolean contains(int num){
  if(num > max_value){
    return false;
  }
  //數(shù)據(jù)保存在整個數(shù)組的哪個下標中
  int index = num / 8;
  //數(shù)據(jù)在這個下標元素的哪個位置
  int position = num % 8;
  return (bits[index] & 1<<position) != 0;
}

測試

讓我們做一下測試吧:

public class BitMapTest {
  public static void main(String[] agrs){
    BitMap bm = new BitMap(100);
    
    bm.add(1);
    bm.add(12);
    bm.add(14);
    bm.add(51);
    bm.add(71);
    bm.add(100);
    
    System.out.println("12:" + (bm.contains(12)?"存在":"不存在"));
    System.out.println("13:" + (bm.contains(13)?"存在":"不存在"));
    System.out.println("51:" + (bm.contains(51)?"存在":"不存在"));
    System.out.println("66:" + (bm.contains(66)?"存在":"不存在"));
    System.out.println("100:" + (bm.contains(100)?"存在":"不存在"));
  }
}

運行結(jié)果:

12:存在
13:不存在
51:存在
66:不存在
100:存在

從結(jié)果可以看到菱父,判斷的都很準確,當然這只是一個最簡單的BitMap實現(xiàn),它還存在著很多問題浙宜,比如我們必須知道數(shù)據(jù)中最大的那個數(shù)字是多少官辽,這個可以采用動態(tài)擴容的方式解決;
在 JDK 中粟瞬,已經(jīng)有對應實現(xiàn)的數(shù)據(jù)結(jié)構(gòu)類 java.util.BitSet同仆,我們可以不用強擼 BitMap,直接使用 BitSet 就好了裙品,或者使用谷歌封裝的 EWAHCompressedBitmap俗批。

優(yōu)缺點

優(yōu)點:

  • 占用內(nèi)存空間低,可以極大地節(jié)約空間市怎;
  • 運算效率高岁忘,查找、去重都不需要遍歷全部數(shù)據(jù)区匠;

缺點:

  • 所有的數(shù)據(jù)不能重復干像,相當于直接就是排重過的;
  • 如果數(shù)據(jù)只有兩個:1 和 10000000驰弄,使用 BitMap 得不償失麻汰,只有當數(shù)據(jù)比較密集時才有優(yōu)勢。

本章節(jié)介紹了 BitMap 的概念和基本實現(xiàn)戚篙,后續(xù)會介紹 BitMap 在實際開發(fā)中的應用五鲫。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市已球,隨后出現(xiàn)的幾起案子臣镣,更是在濱河造成了極大的恐慌,老刑警劉巖智亮,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忆某,死亡現(xiàn)場離奇詭異,居然都是意外死亡阔蛉,警方通過查閱死者的電腦和手機弃舒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來状原,“玉大人聋呢,你說我怎么就攤上這事〉咔” “怎么了削锰?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長毕莱。 經(jīng)常有香客問我器贩,道長颅夺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任蛹稍,我火速辦了婚禮吧黄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘唆姐。我一直安慰自己拗慨,他們只是感情好,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布奉芦。 她就那樣靜靜地躺著赵抢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪仗阅。 梳的紋絲不亂的頭發(fā)上昌讲,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音减噪,去河邊找鬼短绸。 笑死,一個胖子當著我的面吹牛筹裕,可吹牛的內(nèi)容都是我干的醋闭。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼朝卒,長吁一口氣:“原來是場噩夢啊……” “哼证逻!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起抗斤,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤囚企,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后瑞眼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體龙宏,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年伤疙,在試婚紗的時候發(fā)現(xiàn)自己被綠了银酗。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡徒像,死狀恐怖黍特,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情锯蛀,我是刑警寧澤灭衷,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站旁涤,受9級特大地震影響今布,放射性物質(zhì)發(fā)生泄漏经备。R本人自食惡果不足惜拭抬,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一部默、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧造虎,春花似錦傅蹂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至氓轰,卻和暖如春婚夫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背署鸡。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工案糙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人靴庆。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓时捌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親炉抒。 傳聞我的和親對象是個殘疾皇子奢讨,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355