Java位集合之BitMap、BitSet和布隆過濾器示例解析
1 Java位集合
前幾天剛學(xué)習(xí)了Redis中位操作命令,今天順便學(xué)下java中位集合
1.1 Bit-Map
1.1.1 簡(jiǎn)介
Bit-map
的基本思想就是用一個(gè)bit
位來標(biāo)記某個(gè)元素對(duì)應(yīng)的Value
,而Key
即是該元素。由于采用了Bit
為單位來存儲(chǔ)數(shù)據(jù),因此在存儲(chǔ)空間方面,可以大大節(jié)省。(即:節(jié)省存儲(chǔ)空間
)
Bitmap
主要用于快速檢索關(guān)鍵字狀態(tài),通常要求關(guān)鍵字是一個(gè)連續(xù)的序列(或者關(guān)鍵字是一個(gè)連續(xù)序列中的大部分), 最基本的情況,使用1bit
表示一個(gè)關(guān)鍵字的狀態(tài)(可標(biāo)示兩種狀態(tài)),根據(jù)需要也可以使用2bit
(表示4種狀態(tài)),3bit
(表示8種狀態(tài))。
Bitmap
的主要應(yīng)用場(chǎng)合:表示連續(xù)(或接近連續(xù),即大部分會(huì)出現(xiàn))的關(guān)鍵字序列的狀態(tài)(狀態(tài)數(shù)/關(guān)鍵字個(gè)數(shù) 越小越好)。
32位機(jī)器上,對(duì)于一個(gè)整型數(shù),比如int a=1
在內(nèi)存中占32bit
位(一個(gè)字寬
即4Byte
),這是為了方便計(jì)算機(jī)的運(yùn)算。但是對(duì)于某些應(yīng)用場(chǎng)景而言,這屬于一種巨大的浪費(fèi),因?yàn)槲覀兛梢杂脤?duì)應(yīng)的32bit
位對(duì)應(yīng)存儲(chǔ)十進(jìn)制的0-31個(gè)數(shù),而這就是Bit-map
的基本思想。Bit-map
算法利用這種思想處理大量數(shù)據(jù)的排序、查詢以及去重。
假設(shè)有這樣一個(gè)需求:
在20億個(gè)隨機(jī)整數(shù)中找出某個(gè)數(shù)m是否存在其中,并假設(shè)32位操作系統(tǒng),4G內(nèi)存
在Java
中,int占4字節(jié),1字節(jié)=8位(1 byte = 8 bit)
如果每個(gè)數(shù)字用int存儲(chǔ)
,那就是20億個(gè)int,因而占用的空間約為(2000000000*4/1024/1024/1024)≈7.45 G
如果按位存儲(chǔ)
就不一樣了,20億個(gè)數(shù)就是20億位,占用空間約為(2000000000/8/1024/1024/1024)≈0.233 G
Bit-Map
的每一位表示一個(gè)數(shù),0
表示不存在,1
表示存在,這正符合二進(jìn)制,這樣我們可以很容易表示{1,2,4,6}
這幾個(gè)數(shù):
計(jì)算機(jī)內(nèi)存分配的最小單位是字節(jié)
,也就是8位
,那如果要表示{12,13,15}
怎么辦呢,是在另一個(gè)8位上表示了:
這樣的話,好像變成一個(gè)二維數(shù)組了
1個(gè)int占32位
,那么我們只需要申請(qǐng)一個(gè)int
數(shù)組長(zhǎng)度為 int tmp[1+N/32]
即可存儲(chǔ),其中N
表示要存儲(chǔ)的這些數(shù)中的最大值,于是:
tmp[0]:可以表示0~31
tmp[1]:可以表示32~63
tmp[2]:可以表示64~95
。。。
如此一來,給定任意整數(shù)M
,那么M/32
就得到下標(biāo),M%32
就知道它在此下標(biāo)的哪個(gè)位置
1.1.2 添加
這里有個(gè)問題,我們?cè)趺窗岩粋€(gè)數(shù)放進(jìn)去呢?例如,想把5這個(gè)數(shù)字放進(jìn)去,怎么做呢?
首先,5/32=0
,5%32=5
,也是說它應(yīng)該在tmp[0]
的第5
個(gè)位置,那我們把1向左移動(dòng)5位,然后按位或
換成二進(jìn)制就是
這就相當(dāng)于
86 | 32 = 118
86 | (1<<5) = 118
b[0] = b[0] | (1<<5)
也就是說,要想插入一個(gè)數(shù),將1
左移帶代表該數(shù)字的那一位,然后與原數(shù)進(jìn)行按位或操作
化簡(jiǎn)一下,就是 86 + (5/8) | (1<<(5%8))
因此,公式可以概括為:p + (i/8)|(1<<(i%8)) 其中,p表示現(xiàn)在的值,i表示待插入的數(shù)
1.1.3 清除
以上是添加,那如果要清除該怎么做呢?
還是上面的例子,假設(shè)我們要6移除,該怎么做呢?
從圖上看,只需將該數(shù)所在的位置為0
即可
首先把1左移6位,就到達(dá)6這個(gè)數(shù)字所代表的位,然后按位取反,最后與原數(shù)按位與,這樣就把該位置為0了
b[0] = b[0] & (~(1<<6))
b[0] = b[0] & (~(1<<(i%8)))
1.1.4 查找
前面我們也說了,每一位代表一個(gè)數(shù)字,1
表示有(或者說存在),0
表示無(或者說不存在)。通過把該為置為1
或者0
來達(dá)到添加和清除的效果,那么判斷一個(gè)數(shù)存不存在就是判斷該數(shù)所在的位是0
還是1
假設(shè),我們想知道3
在不在,那么只需判斷 b[0] & (1<<3)
如果這個(gè)值是0,則不存在,如果是1,就表示存在
1.2 Bitmap應(yīng)用
大量數(shù)據(jù)的快速排序、查找、去重
1.2.1 快速排序
假設(shè)我們要對(duì)0-7
內(nèi)的5個(gè)元素(4,7,2,5,3)排序(這里假設(shè)這些元素沒有重復(fù)),我們就可以采用Bit-map
的方法來達(dá)到排序的目的。
要表示8個(gè)數(shù),我們就只需要8個(gè)Bit(1Bytes),首先我們開辟1Byte
的空間,將這些空間的所有Bit位都置為0,然后將對(duì)應(yīng)位置為1。
最后,遍歷一遍Bit區(qū)域,將該位是一的位的編號(hào)輸出(2,3,4,5,7),這樣就達(dá)到了排序的目的,時(shí)間復(fù)雜度O(n)。
優(yōu)點(diǎn):
運(yùn)算效率高,不需要進(jìn)行比較和移位;
占用內(nèi)存少,比如N=10000000;只需占用內(nèi)存為N/8=1250000Byte=1.25M
缺點(diǎn):
所有的數(shù)據(jù)不能重復(fù)。即不可對(duì)重復(fù)的數(shù)據(jù)進(jìn)行排序和查找。
只有當(dāng)數(shù)據(jù)比較密集時(shí)才有優(yōu)勢(shì)
1.2.2 快速去重
20億個(gè)整數(shù)中找出不重復(fù)的整數(shù)的個(gè)數(shù),內(nèi)存不足以容納這20億個(gè)整數(shù)。
首先,根據(jù)內(nèi)存空間不足以容納這20億個(gè)整數(shù)
我們可以快速的聯(lián)想到Bit-map
。下邊關(guān)鍵的問題就是怎么設(shè)計(jì)我們的Bit-map
來表示這20億個(gè)數(shù)字的狀態(tài)了。其實(shí)這個(gè)問題很簡(jiǎn)單,一個(gè)數(shù)字的狀態(tài)只有三種,分別為不存在,只有一個(gè),有重復(fù)。因此,我們只需要2bits就可以對(duì)一個(gè)數(shù)字的狀態(tài)進(jìn)行存儲(chǔ)了,假設(shè)我們?cè)O(shè)定一個(gè)數(shù)字不存在為00,存在一次01,存在兩次及其以上為11。那我們大概需要存儲(chǔ)空間2G左右。
接下來的任務(wù)就是把這20億
個(gè)數(shù)字放進(jìn)去(存儲(chǔ)),如果對(duì)應(yīng)的狀態(tài)位為00,則將其變?yōu)?1,表示存在一次;如果對(duì)應(yīng)的狀態(tài)位為01,則將其變?yōu)?1,表示已經(jīng)有一個(gè)了,即出現(xiàn)多次;如果為11,則對(duì)應(yīng)的狀態(tài)位保持不變,仍表示出現(xiàn)多次。
最后,統(tǒng)計(jì)狀態(tài)位為01
的個(gè)數(shù),就得到了不重復(fù)的數(shù)字個(gè)數(shù),時(shí)間復(fù)雜度為O(n)
。
1.2.3 快速查找
這就是我們前面所說的了,int
數(shù)組中的一個(gè)元素是4字節(jié)占32位,那么除以32就知道元素的下標(biāo),對(duì)32求余數(shù)(%32)就知道它在哪一位,如果該位是1,則表示存在。
1.3 BitSet
BitSet
實(shí)現(xiàn)了一個(gè)位向量,它可以根據(jù)需要增長(zhǎng)。每一位都有一個(gè)布爾值。一個(gè)BitSet
的位可以被非負(fù)整數(shù)索引(意思就是每一位都可以表示一個(gè)非負(fù)整數(shù))??梢圆檎?、設(shè)置、清除某一位。通過邏輯運(yùn)算符可以修改另一個(gè)BitSet
的內(nèi)容。默認(rèn)情況下,所有的位都有一個(gè)默認(rèn)值false
。
public class BitSet implements Cloneable, java.io.Serializable { /* * BitSets are packed into arrays of "words." Currently a word is * a long, which consists of 64 bits, requiring 6 address bits. * The choice of word size is determined purely by performance concerns. */ private final static int ADDRESS_BITS_PER_WORD = 6; private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD; private final static int BIT_INDEX_MASK = BITS_PER_WORD - 1; /* Used to shift left or right for a partial word mask */ private static final long WORD_MASK = 0xffffffffffffffffL; /** * @serialField bits long[] * * The bits in this BitSet. The ith bit is stored in bits[i/64] at * bit position i % 64 (where bit position 0 refers to the least * significant bit and 63 refers to the most significant bit). */ private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField("bits", long[].class), }; /** * The internal field corresponding to the serialField "bits". */ private long[] words; /** * The number of words in the logical size of this BitSet. */ private transient int wordsInUse = 0; /** * Given a bit index, return word index containing it. */ private static int wordIndex(int bitIndex) { return bitIndex >> ADDRESS_BITS_PER_WORD; } /** * Creates a new bit set. All bits are initially {@code false}. */ public BitSet() { initWords(BITS_PER_WORD); sizeIsSticky = false; } /** * Creates a bit set whose initial size is large enough to explicitly * represent bits with indices in the range {@code 0} through * {@code nbits-1}. All bits are initially {@code false}. * * @param nbits the initial size of the bit set * @throws NegativeArraySizeException if the specified initial size * is negative */ public BitSet(int nbits) { // nbits can't be negative; size 0 is OK if (nbits < 0) throw new NegativeArraySizeException("nbits < 0: " + nbits); initWords(nbits); sizeIsSticky = true; } private void initWords(int nbits) { words = new long[wordIndex(nbits-1) + 1]; }
用一個(gè)long
數(shù)組來存儲(chǔ),初始長(zhǎng)度64,set值的時(shí)候首先右移6位(相當(dāng)于除以64)計(jì)算在數(shù)組的什么位置,然后更改狀態(tài)位
別的看不懂不要緊,看懂這兩句就夠了:
int wordIndex = wordIndex(bitIndex); words[wordIndex] |= (1L << bitIndex);
1.4 Bloom Filters
1.4.1 簡(jiǎn)介
Bloom filter
是一個(gè)數(shù)據(jù)結(jié)構(gòu),它可以用來判斷某個(gè)元素是否在集合內(nèi),具有運(yùn)行快速,內(nèi)存占用小的特點(diǎn)。
而高效插入和查詢的代價(jià)就是,Bloom Filter
是一個(gè)基于概率的數(shù)據(jù)結(jié)構(gòu):它只能告訴我們一個(gè)元素絕對(duì)不在集合內(nèi)或可能在集合內(nèi)。Bloom filter
的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)是一個(gè) 比特向量(可理解為數(shù)組)。
主要應(yīng)用于大規(guī)模數(shù)據(jù)下不需要精確過濾的場(chǎng)景,如檢查垃圾郵件地址,爬蟲URL
地址去重,解決緩存穿透問題等
如果想判斷一個(gè)元素是不是在一個(gè)集合里,一般想到的是將集合中所有元素保存起來,然后通過比較確定。鏈表、樹、散列表(哈希表)等等數(shù)據(jù)結(jié)構(gòu)都是這種思路,但是隨著集合中元素的增加,需要的存儲(chǔ)空間越來越大;同時(shí)檢索速度也越來越慢,檢索時(shí)間復(fù)雜度分別是O(n)、O(log n)、O(1)。
布隆過濾器
的原理是:當(dāng)一個(gè)元素被加入集合時(shí),通過 K
個(gè)散列函數(shù)將這個(gè)元素映射成一個(gè)位數(shù)組(Bit array
)中的 K 個(gè)點(diǎn),把它們置為 1 。檢索時(shí),只要看看這些點(diǎn)是不是都是1就知道元素是否在集合中;如果這些點(diǎn)有任何一個(gè) 0,則被檢元素一定不在;如果都是1,則被檢元素很可能在(之所以說可能
是誤差的存在)。
1.4.2 BloomFilter 流程
BloomFilter
流程:
- 首先需要
k
個(gè)hash
函數(shù),每個(gè)函數(shù)可以把 key 散列成為 1 的整數(shù); - 初始化時(shí),需要一個(gè)長(zhǎng)度為 n 比特的數(shù)組,每個(gè)比特位初始化為 0;
- 某個(gè)
key
加入集合時(shí),用 k 個(gè)hash
函數(shù)計(jì)算出 k 個(gè)散列值,并把數(shù)組中對(duì)應(yīng)的比特位置為 1; - 判斷某個(gè)
key
是否在集合時(shí),用 k 個(gè)hash
函數(shù)計(jì)算出 k 個(gè)散列值,并查詢數(shù)組中對(duì)應(yīng)的比特位,如果所有的比特位都是1,認(rèn)為在集合中。
1.4.3 應(yīng)用場(chǎng)景
布隆過濾器因?yàn)樗男史浅8撸员粡V泛的使用,比較典型的場(chǎng)景有以下幾個(gè):
網(wǎng)頁爬蟲
: 爬蟲程序可以使用布隆過濾器來過濾掉已經(jīng)爬取過的網(wǎng)頁,避免重復(fù)爬取和浪費(fèi)資源。緩存系統(tǒng)
: 緩存系統(tǒng)可以使用布隆過濾器來判斷一個(gè)查詢是否可能存在于緩存中,從而減少查詢緩存的次數(shù),提高查詢效率。布隆過濾器也經(jīng)常用來解決緩存穿透的問題。分布式系統(tǒng)
: 在分布式系統(tǒng)中,可以使用布隆過濾器來判斷一個(gè)元素是否存在于分布式緩存中,避免在所有節(jié)點(diǎn)上進(jìn)行查詢,減少網(wǎng)絡(luò)負(fù)載。垃圾郵件過濾
: 布隆過濾器可以用于判斷一個(gè)郵件地址是否在垃圾郵件列表中,從而過濾掉垃圾郵件。黑名單過濾
: 布隆過濾器可以用于判斷一個(gè)IP地址或手機(jī)號(hào)碼是否在黑名單中,從而阻止惡意請(qǐng)求。
1.4.4 如何使用
Java中可以使用第三方庫(kù)來實(shí)現(xiàn)布隆過濾器,常見的有Google Guava庫(kù)和Apache Commons庫(kù)以及Redis。
如Guava:
import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; public class BloomFilterExample { public static void main(String[] args) { // 創(chuàng)建布隆過濾器,預(yù)計(jì)插入100個(gè)元素,誤判率為0.01 BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(), 100, 0.01); // 插入元素 bloomFilter.put("Lynn"); bloomFilter.put("666"); bloomFilter.put("八股文"); // 判斷元素是否存在 System.out.println(bloomFilter.mightContain("Lynn")); // true System.out.println(bloomFilter.mightContain("張三")); // false } }
Apache Commons:
import org.apache.commons.lang3.StringUtils; import org.apache.commons.collections4.BloomFilter; import org.apache.commons.collections4.functors.HashFunctionIdentity; public class BloomFilterExample { public static void main(String[] args) { // 創(chuàng)建布隆過濾器,預(yù)計(jì)插入100個(gè)元素,誤判率為0.01 BloomFilter<String> bloomFilter = new BloomFilter<>(HashFunctionIdentity.hashFunction(StringUtils::hashCode), 100, 0.01); // 插入元素 bloomFilter.put("Lynn"); bloomFilter.put("666"); bloomFilter.put("八股文"); // 判斷元素是否存在 System.out.println(bloomFilter.mightContain("Lynn")); // true System.out.println(bloomFilter.mightContain("張三")); // false } }
Redis中可以通過Bloom模塊來使用,使用Redisson
可以:
首先創(chuàng)建一個(gè)RedissonClient
對(duì)象,然后通過該對(duì)象獲取一個(gè)RBloomFilter
對(duì)象,使用tryInit
方法來初始化布隆過濾器,指定了最多能添加的元素?cái)?shù)量為100,誤判率為0.01。
然后,使用add方法將元素"犬小哈"、"666"和"八股文"添加到布隆過濾器中,使用contains方法來檢查元素是否存在于布隆過濾器中。
Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config); RBloomFilter<String> bloomFilter = redisson.getBloomFilter("myfilter"); bloomFilter.tryInit(100, 0.01); bloomFilter.add("Lynn"); bloomFilter.add("666"); bloomFilter.add("八股文"); System.out.println(bloomFilter.contains("Lynn")); System.out.println(bloomFilter.contains("張三")); redisson.shutdown();
或者Jedis也可以:
Jedis jedis = new Jedis("localhost"); jedis.bfCreate("myfilter", 100, 0.01); jedis.bfAdd("myfilter", "Lynn"); jedis.bfAdd("myfilter", "666"); jedis.bfAdd("myfilter", "八股文"); System.out.println(jedis.bfExists("myfilter", "Lynn")); System.out.println(jedis.bfExists("myfilter", "張三")); jedis.close();
總結(jié)
到此這篇關(guān)于Java位集合之BitMap、BitSet和布隆過濾器的文章就介紹到這了,更多相關(guān)Java位集合BitMap、BitSet和布隆過濾器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
mybatis-plus批處理IService的實(shí)現(xiàn)示例
這篇文章主要介紹了mybatis-plus批處理IService的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08提示:Decompiled.class file,bytecode version如何解決
在處理Decompiled.classfile和bytecodeversion問題時(shí),通過修改Maven配置文件,添加阿里云鏡像并去掉默認(rèn)鏡像,解決了下載源的問題,同時(shí),檢查并修改了依賴版本,確保了問題的解決2024-12-12基于Spring-cloud-gateway實(shí)現(xiàn)全局日志記錄的方法
最近項(xiàng)目在線上運(yùn)行出現(xiàn)了一些難以復(fù)現(xiàn)的bug需要定位相應(yīng)api的日志,通過nginx提供的api請(qǐng)求日志難以實(shí)現(xiàn),于是在gateway通過全局過濾器記錄api請(qǐng)求日志,本文給大家介紹基于Spring-cloud-gateway實(shí)現(xiàn)全局日志記錄,感興趣的朋友一起看看吧2023-11-11SpringBoot2 整合Ehcache組件,輕量級(jí)緩存管理的原理解析
這篇文章主要介紹了SpringBoot2 整合Ehcache組件,輕量級(jí)緩存管理,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08高并發(fā)下restTemplate的錯(cuò)誤分析方式
這篇文章主要介紹了高并發(fā)下restTemplate的錯(cuò)誤分析方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10Kotlin語法學(xué)習(xí)-變量定義、函數(shù)擴(kuò)展、Parcelable序列化等簡(jiǎn)單總結(jié)
這篇文章主要介紹了Kotlin語法學(xué)習(xí)-變量定義、函數(shù)擴(kuò)展、Parcelable序列化等簡(jiǎn)單總結(jié)的相關(guān)資料,需要的朋友可以參考下2017-05-05