簡介
bitmap在很多海量數(shù)據(jù)處理的情況下會用到。一些典型的情況包括數(shù)據(jù)過濾坡椒,數(shù)據(jù)位設(shè)置和統(tǒng)計等扰路。 它的引入和應(yīng)用通常是考慮到海量數(shù)據(jù)的情況下,用普通的數(shù)組會超出數(shù)據(jù)保存的范圍肠牲。使用這種位圖的方式雖然不能在根本上解決海量數(shù)據(jù)處理的問題幼衰,但是在一定的數(shù)據(jù)范圍內(nèi),它是一種有效的方法缀雳。bitmap在java的類庫里有一個對應(yīng)的實現(xiàn):BitSet渡嚣。我們會對bitmap的引入做一個介紹,然后詳細分析一個bitvector的精妙實現(xiàn),并在后面和java中的BitSet實現(xiàn)做一個對比识椰。在本文中對bitmap, bitvector不做區(qū)分绝葡,他們表達的是同一個意思。
bitmap的引出
假設(shè)我們有一個很大的數(shù)據(jù)集合腹鹉,比如說是一組數(shù)字藏畅,它是保存在一個很大的文件中。它總體的個數(shù)為400個億功咒。里面有大量重復(fù)的數(shù)據(jù)愉阎,如果去除重復(fù)的元素之后,大概的數(shù)據(jù)有40個億力奋。那么榜旦,假定我們有一臺內(nèi)存為2GB的機器。我們該如何來消除其中重復(fù)的元素呢景殷?再進一步考慮溅呢,如果我們消除了重復(fù)的元素之后,怎么統(tǒng)計里面元素的個數(shù)并將消重后的元素保存到另外的一個結(jié)果文件里呢猿挚?
我們先來做一個大致的估計咐旧。假定數(shù)字的范圍都是從0到Integer.MAX_VALUE。如果我們開一個數(shù)組來保存的話绩蜻,是否可行呢铣墨?一個int數(shù)字4個字節(jié),要保存0到Integer.MAX_VALUE個數(shù)字辜羊,那么就需要2的31次方個踏兜,也就是說4G個元素词顾。這么一相乘八秃,除非有8GB的內(nèi)存,否則根本就保存不下來這么多數(shù)據(jù)肉盹。
bitmap分析和應(yīng)用
現(xiàn)在昔驱,如果我們換一種方式,用bitmap試試呢上忍?bitmap它本質(zhì)上也是一個數(shù)組骤肛,只是用數(shù)組中間對應(yīng)的位來表示一個對應(yīng)的數(shù)字。假設(shè)我們用byte數(shù)組窍蓝。比如說數(shù)字1則對應(yīng)數(shù)組第1個元素的第一位腋颠。數(shù)字9則超出了第一個元素的8位范圍,它對應(yīng)第二個元素的第一位吓笙。這樣依次類推淑玫,我們可以將這40億個元素映射到這個byte數(shù)組里。一個數(shù)字對應(yīng)到數(shù)組中位的關(guān)系如下圖所示:
在上圖中,假設(shè)i是數(shù)組中的一個字節(jié)絮蒿,那么它將對應(yīng)有下面的8個位尊搬。假設(shè)i是第一個字節(jié),那么數(shù)字1就對應(yīng)到第1位土涝,后面的元素依次類推佛寿。
通過這一番討論,我們也可以很容易得到數(shù)字和保存在數(shù)組中元素具體位之間的關(guān)系但壮。假設(shè)有一個數(shù)字i冀泻,它對應(yīng)保存的元素位置為: i / 8。假設(shè)數(shù)組為a蜡饵,那么則為a[i/8]腔长。那么它對應(yīng)到a[i/8]中間的哪個位呢?它對應(yīng)這個元素中的第i % 8這一位验残。
有了這些討論捞附,我們再來看bitmap的一個具體實現(xiàn)。
bitmap的一個實現(xiàn)
針對前面討論的部分您没,bitmap主要的功能包括有一下幾個方面鸟召。
1. 置位(set):將某一位置為1.
2. 清除位(clear),清除某一位氨鹏,將其置為0.
3. 讀取位(get)欧募,讀取某一位的數(shù)據(jù),看結(jié)果是1還是0.
4. 容器所能容納的位個數(shù)(size)仆抵,相當于返回容器的長度跟继。
5. 被置位的元素個數(shù)(count),返回所有被置為1的位的個數(shù)镣丑。我們就一個個來分析:
首先舔糖,我們要定義一個byte數(shù)組,來保存這些數(shù)據(jù)莺匠。另外金吗,我們也需要元素來保存里面所有位的個數(shù)和被置位的元素個數(shù)。因此趣竣,我們有如下的定義:
Java代碼
private byte[] bits;
private int size;
private int count = -1;
現(xiàn)在摇庙,假設(shè)我們要構(gòu)造一個BitVector,我們就需要指定它的長度遥缕。它的一個構(gòu)造函數(shù)可以構(gòu)造成如下:
Java代碼
public BitVector(int n) {
size = n;
bits = new byte[(size >> 3) + 1];
}
這里卫袒,指定的參數(shù)n表示有多少個數(shù)字,相當于要置多少個位单匣。由于我們要用byte來保存夕凝,所以能保存這么多數(shù)字的byte個數(shù)為n / 8 + 1烤蜕。這種長度用移位的方式來表示則為(size >> 3) + 1。右移3位相當于除以8.
set
前面已經(jīng)提到過迹冤,set某個位的元素讽营,需要找到元素所在的byte,然后再設(shè)置byte對應(yīng)的位泡徙。而n / 8得到的就是對應(yīng)byte的索引橱鹏,而n % 8得到的是對應(yīng)byte中的位。這部分的代碼實現(xiàn)如下:
Java代碼
public final void set(int bit) {
bits[bit >> 3] |= 1 << (bit & 7);
count = -1;
}
和我前面討論的類似堪藐,這里不過是利用移位的方式實現(xiàn)同樣的效果莉兰。前面bit >> 3相當于bit / 8。而bit & 7則相當于bit % 8礁竞。為什么bit & 7會相當于這個效果呢糖荒?在前面有一篇分析HashMap實現(xiàn)的文章里也討論過這種手法。因為這里一個byte是8位模捂,而8對應(yīng)的二進制表示形式為1000捶朵,那么比它小1的7的二進制形式為0111。在將bit和7進行與運算的時候狂男,所有大于第3位的高位都被置為0综看,之保留最低的3位。這樣岖食,最低的3位數(shù)字最小是0红碑,最大是7.就相當于對數(shù)字8求模的運算效果。
clear
和前面的set方法相反泡垃,這里是需要將特定的位置為0析珊。
Java代碼
public final void clear(int bit) {
bits[bit >> 3] &= ~(1 << (bit & 7));
count = -1;
}
get
get這部分的代碼主要是判斷這一位是否被置為1。我們將這個byte和對應(yīng)位為1的數(shù)字求與運算蔑穴,如果結(jié)果不是0捌年,則表示它被置為1.
Java代碼
public final boolean get(int bit) {
return (bits[bit >> 3] & (1 << (bit & 7))) != 0;
}
count
count方法的實現(xiàn)是一個比較精妙的手法洪碳。按照我們原來的理解畸裳,如果要計算里面所有被置為1的位的個數(shù)到忽,我們需要遍歷每個byte菇用,然后求每個byte里面1的個數(shù)龄坪。一種想當然的辦法就是每次和數(shù)字1移位的數(shù)字進行與運算焙格,如果結(jié)果為0表示該位沒有被置為1瓮床,否則表示該位有被置位芜茵。這種辦法沒問題叙量,不過對于每個字節(jié),都要這么走一輪的話九串,相當于前面運算量的8倍绞佩。如果我們可以優(yōu)化一下的話寺鸥,對于大數(shù)據(jù)來說還是有一定價值的。下面是另一種高效方法的實現(xiàn)品山,采用空間換時間的辦法:
Java代碼
public final int count() {
// if the vector has been modified
if (count == -1) {
int c = 0;
int end = bits.length;
for (int i = 0; i < end; i++)
c += BYTE_COUNTS[bits[i] & 0xFF]; // sum bits per byte
count = c;
}
return count;
}
private static final byte[] BYTE_COUNTS = { // table of bits/byte
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8
};
這里建立了一個BYTE_COUNTS的數(shù)組胆建。里面記錄了對應(yīng)一個數(shù)字1的個數(shù)。我們在bit[i] && 0xff運算之后得到的是一個8位的數(shù)字肘交,范圍從0到255.那么笆载,問題就歸結(jié)到找到對應(yīng)數(shù)字的二進制表示里1的個數(shù)。比如說數(shù)字0有0個1涯呻, 1有1個1凉驻, 2有1個1,3有2個1...复罐。在一個byte里面涝登,最多有256種,如果我們將這256個數(shù)字對應(yīng)的1個數(shù)都事先編碼保存好的話效诅,后面求這個數(shù)字對應(yīng)的1個數(shù)只要直接取就可以了胀滚。
和BitSet的比較
前面我們討論的bitmap的實現(xiàn)實際上是摘自開源軟件lucene的代碼片段。它采用byte數(shù)組來做為內(nèi)部數(shù)據(jù)保存的方式乱投。各種置位的操作和運算都采用二進制移位等運算方式來實現(xiàn)盡可能的高效率蛛淋。在java內(nèi)部的類庫里,實際上也有一個類似的實現(xiàn)篡腌。那就是BitSet褐荷。
BitSet的內(nèi)部實現(xiàn)和BitVector的實現(xiàn)稍微有點不一樣,它內(nèi)部是采用long[]數(shù)組來保存元素嘹悼。這樣叛甫,每次的置位和清位操作方式就有差別。比如說置位杨伙,原來是對要置的數(shù)字除以8其监,現(xiàn)在則是除以64,相當于>> 6這中移位6次的操作限匣。
另外抖苦,在BigSet里并沒有實現(xiàn)求所有被置為1的元素的個數(shù),如果要求他們的話米死,因為要在64位的數(shù)字范圍內(nèi)來找锌历,不可能再用前面數(shù)字列表的方法來加快其統(tǒng)計速度,只能一位一位的運算和比較統(tǒng)計了峦筒。這是這種實現(xiàn)一個不足的地方究西。
BitSet的內(nèi)部代碼實現(xiàn)還有一個比較有意思的地方,我們先看這一段代碼:
Java代碼
/**
* Sets the bit at the specified index to {@code true}.
*
* @param bitIndex a bit index
* @throws IndexOutOfBoundsException if the specified index is negative
* @since JDK1.0
*/
public void set(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
int wordIndex = wordIndex(bitIndex);
expandTo(wordIndex);
words[wordIndex] |= (1L << bitIndex); // Restores invariants
checkInvariants();
}
/**
* Sets the bit at the specified index to the specified value.
*
* @param bitIndex a bit index
* @param value a boolean value to set
* @throws IndexOutOfBoundsException if the specified index is negative
* @since 1.4
*/
public void set(int bitIndex, boolean value) {
if (value)
set(bitIndex);
else
clear(bitIndex);
}
這是java里對應(yīng)的置位實現(xiàn)方法物喷。按照我們的理解卤材,它應(yīng)該是找到對應(yīng)的long元素遮斥,然后再將對64取模后對應(yīng)的位設(shè)置為1.可是這代碼里的設(shè)置部分卻如下: words[wordIndex] |= (1L << bitIndex); // Restores invariants. 這里用到了移位,但是沒有對64求模扇丛。為什么呢术吗?這樣不會出錯嗎?在我們的理解里帆精,如果對數(shù)字向左移位藐翎,如果超出了數(shù)字的表示范圍,潛意識里就會認為那些部分被忽略掉了实幕。這樣想的話吝镣,那么這么一通移位下來不就得到個0了嗎?我們后面針對這一點繼續(xù)分析昆庇。
一個有意思的地方
這個問題的答案并不復(fù)雜末贾。如果我們?nèi)ゲ炜磿系亩x,仔細看才發(fā)現(xiàn)整吆。<< >>等這樣的移位運算拱撵,實際上是循環(huán)移位效果的。也就是說表蝙,如果我一個數(shù)字向左移位到溢出了拴测,它不是被忽略掉,而是后續(xù)會在低位繼續(xù)補進府蛇。比如說我們看下面一個最簡單的代碼:
Java代碼
class test
{
public static void main(String[] args)
{
for(int i = 0; i < 100; i++)
System.out.println(1 << i);
}
}
如果我們執(zhí)行上面這一段代碼集索,會發(fā)現(xiàn)實際的結(jié)果是當溢出之后又開始重新從頭來顯示,部分的輸出結(jié)果如下所示:
Java代碼
1
2
4
8
16
32
64
128
256
512
1024
2048
4096
8192
16384
32768
65536
131072
262144
524288
1048576
2097152
4194304
8388608
16777216
33554432
67108864
134217728
268435456
536870912
1073741824
-2147483648
1
2
4
8
現(xiàn)在汇跨,我們也就理解了為什么前面直接用一個左移位的運算來表示务荆。因為這是循環(huán)的移位,相當于已經(jīng)實現(xiàn)了求模的運算效果了穷遂。老實說函匕,這種方式可行,不過個人覺得不太直觀蚪黑,還是用一個類似于求模運算的方式來表示好一些盅惜。
總結(jié)
bitmap通過充分利用數(shù)組里面每一位的置位來表示數(shù)據(jù)的存在與否。比如說某一位設(shè)置為1忌穿,表示數(shù)據(jù)存在抒寂,否則表示不存在。通過充分利用數(shù)據(jù)的空間伴网,它比直接利用一個數(shù)組蓬推,然后數(shù)組里面的每一個元素來表示一個數(shù)組的空間利用率高。比如說有一個同等長度的int數(shù)組澡腾,原來一個int元素用來表示一個數(shù)據(jù)沸伏,現(xiàn)在利用int元素的每一位,它可以表示32個元素动分。所以說毅糟,在一定程度上,某些數(shù)據(jù)映射澜公、過濾等問題通過bitmap它可以處理的范圍更大姆另。當然,bitmap也受到計算機本身數(shù)據(jù)表示范圍的限制坟乾,在超出一定的范圍之后迹辐,我們還是需要考慮結(jié)合數(shù)據(jù)劃分等手段。另外甚侣,在考慮這些數(shù)據(jù)結(jié)構(gòu)的詳細實現(xiàn)時明吩,有很多細節(jié)的東西也會加深我們的認識,也許很多就是我們平時忽略的地方殷费。
BitSet簡介
類實現(xiàn)了一個按需增長的位向量印荔。位 set 的每個組件都有一個boolean值。用非負的整數(shù)將BitSet的位編入索引详羡∪月桑可以對每個編入索引的位進行測試、設(shè)置或者清除实柠。通過邏輯與水泉、邏輯或和邏輯異或操作,可以使用一個BitSet修改另一個BitSet的內(nèi)容窒盐。
默認情況下茶行,set 中所有位的初始值都是false。
每個位 set 都有一個當前大小登钥,也就是該位 set 當前所用空間的位數(shù)畔师。注意,這個大小與位 set 的實現(xiàn)有關(guān)牧牢,所以它可能隨實現(xiàn)的不同而更改看锉。位 set 的長度與位 set 的邏輯長度有關(guān),并且是與實現(xiàn)無關(guān)而定義的塔鳍。
除非另行說明伯铣,否則將 null 參數(shù)傳遞給BitSet中的任何方法都將導(dǎo)致NullPointerException。
在沒有外部同步的情況下轮纫,多個線程操作一個BitSet是不安全的
基本原理
BitSet是位操作的對象腔寡,值只有0或1即false和true,內(nèi)部維護了一個long數(shù)組掌唾,初始只有一個long放前,所以BitSet最小的size是64忿磅,當隨著存儲的元素越來越多,BitSet內(nèi)部會動態(tài)擴充凭语,最終內(nèi)部是由N個long來存儲,這些針對操作都是透明的似扔。
用1位來表示一個數(shù)據(jù)是否出現(xiàn)過吨些,0為沒有出現(xiàn)過,1表示出現(xiàn)過炒辉。使用用的時候既可根據(jù)某一個是否為0表示豪墅,此數(shù)是否出現(xiàn)過。
一個1G的空間黔寇,有 8102410241024=8.5810^9bit偶器,也就是可以表示85億個不同的數(shù)
使用場景
常見的應(yīng)用是那些需要對海量數(shù)據(jù)進行一些統(tǒng)計工作的時候,比如日志分析啡氢、用戶數(shù)統(tǒng)計等等
如統(tǒng)計40億個數(shù)據(jù)中沒有出現(xiàn)的數(shù)據(jù)状囱,將40億個不同數(shù)據(jù)進行排序等。
現(xiàn)在有1千萬個隨機數(shù)倘是,隨機數(shù)的范圍在1到1億之間⊥ぜ希現(xiàn)在要求寫出一種算法,將1到1億之間沒有在隨機數(shù)中的數(shù)求出來
package util;
import java.util.Arrays;
import java.util.BitSet;
public class BitSetDemo {
/**
* 求一個字符串包含的char
*
*/
public static void containChars(String str) {
BitSet used = new BitSet();
for (int i = 0; i < str.length(); i++)
used.set(str.charAt(i)); // set bit for char
StringBuilder sb = new StringBuilder();
sb.append("[");
int size = used.size();
System.out.println(size);
for (int i = 0; i < size; i++) {
if (used.get(i)) {
sb.append((char) i);
}
}
sb.append("]");
System.out.println(sb.toString());
}
/**
* 求素數(shù) 有無限個搀崭。一個大于1的自然數(shù)叨粘,如果除了1和它本身外,不能被其他自然數(shù)整除(除0以外)的數(shù)稱之為素數(shù)(質(zhì)數(shù)) 否則稱為合數(shù)
*/
public static void computePrime() {
BitSet sieve = new BitSet(1024);
int size = sieve.size();
for (int i = 2; i < size; i++)
sieve.set(i);
int finalBit = (int) Math.sqrt(sieve.size());
for (int i = 2; i < finalBit; i++)
if (sieve.get(i))
for (int j = 2 * i; j < size; j += i)
sieve.clear(j);
int counter = 0;
for (int i = 1; i < size; i++) {
if (sieve.get(i)) {
System.out.printf("%5d", i);
if (++counter % 15 == 0)
System.out.println();
}
}
System.out.println();
}
/**
* 進行數(shù)字排序
*/
public static void sortArray() {
int[] array = new int[] { 423, 700, 9999, 2323, 356, 6400, 1,2,3,2,2,2,2 };
BitSet bitSet = new BitSet(2 << 13);
// 雖然可以自動擴容瘤睹,但盡量在構(gòu)造時指定估算大小,默認為64
System.out.println("BitSet size: " + bitSet.size());
for (int i = 0; i < array.length; i++) {
bitSet.set(array[i]);
}
//剔除重復(fù)數(shù)字后的元素個數(shù)
int bitLen=bitSet.cardinality();
//進行排序升敲,即把bit為true的元素復(fù)制到另一個數(shù)組
int[] orderedArray = new int[bitLen];
int k = 0;
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) {
orderedArray[k++] = i;
}
System.out.println("After ordering: ");
for (int i = 0; i < bitLen; i++) {
System.out.print(orderedArray[i] + "\t");
}
System.out.println("iterate over the true bits in a BitSet");
//或直接迭代BitSet中bit為true的元素iterate over the true bits in a BitSet
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) {
System.out.print(i+"\t");
}
System.out.println("---------------------------");
}
/**
* 將BitSet對象轉(zhuǎn)化為ByteArray
* @param bitSet
* @return
*/
public static byte[] bitSet2ByteArray(BitSet bitSet) {
byte[] bytes = new byte[bitSet.size() / 8];
for (int i = 0; i < bitSet.size(); i++) {
int index = i / 8;
int offset = 7 - i % 8;
bytes[index] |= (bitSet.get(i) ? 1 : 0) << offset;
}
return bytes;
}
/**
* 將ByteArray對象轉(zhuǎn)化為BitSet
* @param bytes
* @return
*/
public static BitSet byteArray2BitSet(byte[] bytes) {
BitSet bitSet = new BitSet(bytes.length * 8);
int index = 0;
for (int i = 0; i < bytes.length; i++) {
for (int j = 7; j >= 0; j--) {
bitSet.set(index++, (bytes[i] & (1 << j)) >> j == 1 ? true
: false);
}
}
return bitSet;
}
/**
* 簡單使用示例
*/
public static void simpleExample() {
String names[] = { "Java", "Source", "and", "Support" };
BitSet bits = new BitSet();
for (int i = 0, n = names.length; i < n; i++) {
if ((names[i].length() % 2) == 0) {
bits.set(i);
}
}
System.out.println(bits);
System.out.println("Size : " + bits.size());
System.out.println("Length: " + bits.length());
for (int i = 0, n = names.length; i < n; i++) {
if (!bits.get(i)) {
System.out.println(names[i] + " is odd");
}
}
BitSet bites = new BitSet();
bites.set(0);
bites.set(1);
bites.set(2);
bites.set(3);
bites.andNot(bits);
System.out.println(bites);
}
public static void main(String args[]) {
//BitSet使用示例
BitSetDemo.containChars("How do you do? 你好呀");
BitSetDemo.computePrime();
BitSetDemo.sortArray();
BitSetDemo.simpleExample();
//BitSet與Byte數(shù)組互轉(zhuǎn)示例
BitSet bitSet = new BitSet();
bitSet.set(3, true);
bitSet.set(98, true);
System.out.println(bitSet.size()+","+bitSet.cardinality());
//將BitSet對象轉(zhuǎn)成byte數(shù)組
byte[] bytes = BitSetDemo.bitSet2ByteArray(bitSet);
System.out.println(Arrays.toString(bytes));
//在將byte數(shù)組轉(zhuǎn)回來
bitSet = BitSetDemo.byteArray2BitSet(bytes);
System.out.println(bitSet.size()+","+bitSet.cardinality());
System.out.println(bitSet.get(3));
System.out.println(bitSet.get(98));
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) {
System.out.print(i+"\t");
}
【本文轉(zhuǎn)自: 對大量數(shù)據(jù)進行排序】