BitMap是一種很經常使用的數據結構,它的思想的和原理是不少算法的基礎,固然,而且在索引,數據壓縮,海量數據處理等方面有普遍應用。java
BitMap 是一種很經常使用的數據結構,它的思想和原理是不少算法的基礎,好比Bloom Filter 。算法
BitMap 的基本原理就是用一個 bit 位來存放某種狀態(若是理解不了,看完下文再回頭來看便可),適用於擁有大規模數據,但數據狀態又不是不少的狀況。一般是用來判斷某個數據存不存在的。數組
它最大的一個特色就是對內存的佔用極小,因此常常在大數據中被優化使用。數據結構
爲何說佔用內存小呢?其實從名字就能夠看出端倪,直譯過來叫位圖,但不是圖形學裏面的位圖哦,關鍵單詞是Bit。好比經過某種方法用一個 bit 來表示一個 int,這樣的話內存足足壓縮至 1/32(1 int = 4 byte = 32 bit,PS:理論計算而已,實操時並不會有 1/32 這麼誇張,下文會解釋),因此原先須要8G內存的數據,如今只須要256M,豈不樂哉?固然了,其中算法的一些概念在下文會詳解。函數
所謂的 BitMap 就是用一個 Bit 位來標記某個元素對應的 Value, 而 Key 便是該元素。因爲採用了 Bit 爲單位來存儲數據,所以在存儲空間方面,能夠大大節省。性能
好比有個 int 數組 [2,6,1,7,3],內含5個元素,存儲的空間大小爲 5 * 32 = 160 bit,取的時候,使用元素的下標來獲取對應位上的元素。大數據
可是若是換種思路,把元素的值做爲下標,每一個下標位使用 bit 來標記,有值則爲1,不然爲0,此時咱們只須要在內存上開闢一個連續的二進制位空間,長度爲8(由於上面數據最大的元素是7,可是須要考慮下標起點爲0),則能夠表示成:優化
說明:初始化一個長度是8的 BitMap,初始值均爲0,而後將[2,6,1,7,3]填入對應的下標處,上圖中藍色域,即將這幾個下標處的值設置爲1,因此表示爲:1 1 0 0 1 1 1 0。此時佔用的內存空間爲 8 bit,而原來是 160 bit(順便解釋下上文提到的 1/32,由於咱們開闢的是連續的內容空間,因此會有冗餘)。spa
① 案例一:仍是上文的數組,需求是查詢元素6是否在數組中。 原先咱們須要遍歷整個數組,時間複雜度爲 O(n); 而如今咱們只須要查驗下標爲6的字節是0仍是1便可,若是是1,則表明存在,時間複雜度直接降爲 O(1)。 因此,**最直接的應用場景即是:**數據的查重。設計
② 案例二:有兩個數組,判斷這兩個數組中的重複元素。 原先的最淺顯的作法是雙層for循環進行判斷比較。 而如今,只須要將轉換完成的兩個BirMap進行與運算便可,如:11001110B & 10100000B = 10000000B,全部得出結果,只有元素 7 重複。 固然,最直接的應用場景是: 每一個客戶都有不一樣的標籤,當須要查找同時符合標籤a和標籤b的客戶的時候,只須要將標籤a和標籤b的客戶查出來進行如上的與運算便可。
① 實際使用的時候,並不會向上面同樣很隨意地將長度設置爲8,通常會設置爲32(int型)或64(long型),理由見下文 BitSet 源碼便可。
② 除了上文提到的與運算,固然了,邏輯或和邏輯異或操做都是OK的。
③ 每一個Bit位只能是0或1,因此只能表明true or false,當咱們要進行少許統計的時候,可使用2-BitMap,即每一個位上可使用 00、0一、十、11來分別表示數量爲 0、一、2,此時的 11 通常無心義。
對於 BitMap 這種經典的數據結構,在 Java 語言裏面,其實已經有對應實現的數據結構類 java.util.BitSet 了(***@since ***JDK1.0),而 BitSet 的底層原理,其實就是用 long 類型的數組來存儲元素,因此回過頭來看上文提到的爲何實際使用的時候,長度通常會是有規則的,由於此處使用的是long類型的數組,而 1 long = 64 bit,因此數據大小會是64的整數倍。
/** * The internal field corresponding to the serialField "bits". */
private long[] words;
複製代碼
至於 Java 中的 BitSet 爲何使用 long 數組而不使用 int 數組,我以爲應該是出於 Java 語言的性能考慮的,由於在進行邏輯與等一系列位運算的時候,是須要將兩個數組中的元素一一進行位運算的,而使用 long 的一個好處是數組的長度減小了,從而遍歷的次數也就減小了。
總之就是和場景有關係,抽象概念上就有點相似 Java 中字符串的匹配算法(indexOf)使用的是 BF(暴力檢索)算法同樣,爲何不用更優解呢?還不是由於更優解在少許數據的狀況下反而是拖後腿的那一位。
有參構造的參數表明的是元素的長度,不是數組的大小,好比傳參1和64,數組的長度均爲1,整個size均爲64,可是傳參65的時候,數組長度爲2,size爲128,由於數組是long類型,而一個long能夠存儲64個bit元素。
該函數只在兩個構造方法中調用,做用是初始化數組,而數組的長度則會經過 workIndex(nbits-1) + 1 來獲取。
這個方法很重要, 它是用來獲取某個數在 words 數組中的索引的,採用的算法是將這個數右移6位,why?由於 bitIndex >> 6 == bitIndex / (2^6) == bitIndex /64,而long就是64個字節。
又是一個很重要的方法,做用是動態擴容,由於在初始化的時候,咱們並不知道未來會須要存儲多大的數據。
size 方法很好理解,返回的其實就是數組的空間大小,即數組長度*64。 而 length 方法,看源碼其實有點晦(qu)澀(qiao),簡言之,返回的是 BitSet 的「邏輯大小」,即BitSet 中最高設置位的索引加 1 。
舉個栗子,一個 BitSet 中存儲了兩個元素,10和50,那麼,此時這個 BitMap 的:size = 64;length = 51。
其他的 set、get等方法暫不贅述,總之一句話,想要深入理解 BitSet 的源碼,對於二進制的計算須要有必定的掌握水準。不得不認可,BitSet 的源碼,不少細節的設計太精妙了。
如要論述拓展,要麼就是論述場景的高層次應用,要麼就是論述此算法的不足之處,此處各提一個點:
① 不足:數據稀疏問題,好比三個元素(1,100,10000000),則須要初始化的長度爲 10000000,很不合理,此時可使用 Roaring BitMap 算法來解決,而 Java 程序可使用goolge的 **EWAHCompressedBitmap **來解決。
② 拓展:數據碰撞問題,好比上文提到的爬蟲應用場景是將URL進行哈希運算,而後將hash值存入BitMap之中,可是不得不面臨一個尷尬的狀況,那就是哈希碰撞,而布隆算法(Bloom Filter)就能夠解決這個問題,爲何是拓展呢?由於它是以 BitMap 爲基礎的排重算法。