演算法---BitMap

白露非霜發表於2020-05-05

問題:

假設有3億個整數(範圍0-2億),如何判斷某一個樹是否存在。侷限條件一臺機器,記憶體500m

 

常規的思路:我們可以將資料存到一個集合中,然後判斷某個數是否存在;或者用一個等長的陣列來表示,每個數對應的索引位置,存在就標記為1,不存在0。當然如果裝置條件允許,上面的這方案是可行的。

但是現在我們記憶體只有500mInt型別4個位元組。

單單是這3億個數都已經:300000000*4/1024/1024 差不多1144m了。顯然已經遠超過記憶體限制了。

顯然在這種條件下面,我們想要將這些書完整的儲存下來已經是不現實的。只有另尋他經。

上面的第二種方案中提供一個很好思路,用標記來標註。我們只有尋求記憶體佔用更小的資料型別來標記了,1int=4byte,1byte = 8bit

如果我們用bit來打標記。就不可以很好的縮小記憶體佔用了嘛。1int=4byte,如果用bit那麼記憶體佔用就會縮小32倍,1144/32大概36m就可以了。

也就是說其實我們的一個int位置,可以表示32個資料存在與否

Int 1的二進位制完整表示:0000 0000 0000 0000 0000 0000 0000 0001,它是有32位,只不過平時前面的0都是沒有顯示的

我們宣告一個int型別的標記陣列:flag[2/32 +1],陣列長度2/32 +1,就可以了,為什麼是2/32,不是3億呢,因為這3億個數的範圍就是0-2億,所以我們的標記陣列最大隻需要對應2億即可。有的數,就將其標記為1。就是說陣列的沒一個元素其實包含了32個數存在與否。

Flag[0] --->0-31

Flag[1] --->32-63

Flag[2] --->64-95

Flag[3] --->96-127

.......

我們怎麼來操作這個int型別裡面的32位呢,這就要用到我們的位運算:

<<:左移

>>:右移

&:同位上的兩個數都是1則位1,否則為0

|:同為上的兩個數只要有一個為1 則為1,否則為0

 

我們怎麼找到某個數的標記在陣列的索引及其值的多少位呢?

假設3這個數

Index = 3 / 32 = 0

Location = 3 % 32 = 3

3的標記應該在flag[0] 的第三個位置,將其置為1

flag[0]  |=  1 << Location (位或是不會影響其他位置1的標記)

原本的flag[0]  0000 0000 0000 0000 0000 0000 0000 0011    原本已經有了01

     |    0000 0000 0000 0000 0000 0000 0000 1000    1 << Location 之後的

得到:           0000 0000 0000 0000 0000 0000 0000 1011    現在有0,1,3

這樣就可以所有的資料標記給儲存下來了。

那麼判斷的時候怎麼判斷呢。

同理先找到某個數精確位置

還是3

Index = 3 / 32 = 0

Location = 3 % 32 = 3

現在我們的flag[0] -> 0000 0000 0000 0000 0000 0000 0000 1011

Exist = flag[0] & (1 << Location) != 0  &:相同位上的兩個數都是1則位1,否則為0

  0000 0000 0000 0000 0000 0000 0000 1011

&  0000 0000 0000 0000 0000 0000 0000 1000

    0000 0000 0000 0000 0000 0000 0000 1000

&運算之後結果不為0說明該數存在。

 

大致的思路就是這個樣子,下面用程式碼實現上述邏輯

 

package com.nijunyang.algorithm.math;

/**
 * Description:
 * Created by nijunyang on 2020/5/5 22:33
 */
public class BitMap {
    /**
     * 範圍中最大的那個數
     */
    private int max;

    private int[] flag;

    public BitMap(int max) {
        this.max = max;
        flag = new int[max >> 5 + 1]; //除以32也可以表示為>>5
    }

    /**
     * 新增資料標記
     * @param val
     */
    public void put(int val) {        //往bitmap裡面新增數字

        int index = val / 32;        // 計算陣列索引位置
        int location = val % 32;    // 計算在32位int中的位置
        flag[index] |= 1 << location;   //標記位改成1
    }

    /**
     * 判斷是否存在
     * @param val
     * @return
     */
    public boolean exist(int val) {
        int index = val / 32;
        int location = val % 32;

        int result = flag[index] & (1 << location);
        return result != 0;
    }

    /**
     * 移除標記
     * @param val
     * @return
     */
    public void remove(int val) {
        int index = val / 32;
        int location = val % 32;

        System.out.println(Integer.toBinaryString(flag[index]));
        flag[index] = flag[index] &~ (1 << location); //~取反1變0 0變1
        System.out.println(Integer.toBinaryString(flag[index]));
    }

    public static void main(String[] args) {
        BitMap bitMap = new BitMap(200_000_000);
        bitMap.put(128);
        bitMap.put(129);
        System.out.println(bitMap.exist(127));
        System.out.println(bitMap.exist(128));
        bitMap.remove(128);
        System.out.println(bitMap.exist(128));

    }
}

 

 

雖然說用bitMap的這種思想可以解決上面的這個問題,但是它還是有缺點的,我們上面用的數字,如果是其他型別可能就需要hash之後得到hashcode再進行這個操作,對於hash衝突是無法解決的。因為標記只有01。資料量少的時候相對於普通的hash集合操作並沒有優勢。它對於那種資料量很大,且資料相對密集的,因為陣列的長度是和最大的資料值有關,而不是和集合容量有關。

布隆過濾器就使用bitMap的思想。不過它同時會使用集中hash演算法來計算hashcode,來儘量解決hash衝突。Redis快取設計與效能優化 中有簡單的介紹。

JDK中也有一個BitSet類。

 

相關文章