什麼是位圖?BitMap,你們直譯爲位圖. 個人理解是:位圖是內存中連續的二進制位(bit),能夠用做對大量整形作去重和統計.java
引入一個小栗子來幫助理解一下:程序員
假如咱們要存儲三個int數字 (1,3,5),在java中咱們用一個int數組來存儲,那麼佔用了12個字節.可是咱們申請一個bit數組的話.而且把相應下標的位置爲1,也是能夠表示相同的含義的,好比redis
下標 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
二進制值 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 |
能夠看到,對應於1,3,5爲下標的bit上的值爲1,咱們或者計算機也是能夠get到1,3,5這個信息的.數據庫
那麼這麼作有什麼好處呢?感受更麻煩了鴨,下面這種存儲方式,在申請了bit[8]
的場景下才佔用了一個字節,佔用內存是原來的12分之一,當數據量是海量的時候,好比40億個int,這時候節省的就是10幾個G的內存了.數組
這就引入了位圖的第一個優點,佔用內存小.安全
再想一下,加入咱們如今有一個位圖,保存了用戶今天的簽到數據.下標能夠是用戶的ID.bash
A:服務器
用戶ID | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
二進制值 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 |
這表明了用戶(1,3,5)今天簽到了.數據結構
固然還有昨天的位圖,性能
B:
用戶ID | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
二進制值 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 1 |
這表明了用戶(1,2,3,7)昨天簽到了.
咱們如今想求:
在關係型數據庫中存儲的話,這將是一個比較麻煩的操做,要麼要寫一些表意不明的SQL語句,要麼進行兩次查詢,而後在內存中雙重循環去判斷.
而使用位圖就很簡單了,A & B
, A | B
便可.上面的操做明顯是一個集合的與或
操做,而二進制自然就支持邏輯操做,且衆所周知貓是液體.錯了,衆多周知是計算機進行二進制運算的效率很高.
這就是位圖的第二個優勢: 支持與或運算且效率高.
哇,這麼完美,那麼哪裏能夠買到呢?,那麼有什麼缺點呢?
固然有,位圖不能很方便的支持非運算
,(固然,關係型數據庫支持的也很差).這句話可能有點難理解.繼續舉個例子:
咱們想查詢今天沒有簽到的用戶,直接對位圖進行取非是不能夠的.
對今天簽到的位圖取非獲得的結果以下:
用戶ID | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
二進制值 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 |
這意味着今天(0,2,4,6,7)用戶沒有簽到嗎?不是的,存在沒有7(任意數字)號用戶的狀況,或者他註銷了呢.
這是由於位圖只能表示布爾信息,即true/false
.他在這個位圖中,表示的是XX用戶今天有簽到或者沒有簽到,可是不能額外的表達,xx用戶存在/不存在這個狀態
了.
可是咱們能夠曲線救國,首先搞一個全集用戶的位圖.好比:
全集:
用戶ID | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
二進制值 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0 |
而後用全集的位圖和簽到的位圖作異或操做,相同則爲0,不相同則爲1.
在業務的邏輯爲: 用戶存在和是否簽到兩個bool值,共四種組合.
- 用戶存在,且簽到了. 兩個集合的對應位都爲1,那麼結果就爲0.
- 用戶存在,可是沒簽到. 全集對應位爲1,簽到爲0,因此結果是1.
- 用戶不存在,那麼必然沒可能簽到, 兩個集合的對應位都是0,結果爲0.
因此結果中,爲1的只有一種可能:用戶存在且沒有簽到,正好是咱們所求的結果.
A ^ 全集:
用戶ID | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
二進制值 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
此外,位圖對於稀疏數據的表現不是很好,(固然聰明的大佬們已經基本解決掉了這個問題).原生的位圖來說,若是咱們只有兩個用戶,1號和100000000號用戶,那麼直接存儲int須要8個字節也就是32個bit,而用位圖存儲須要1億個bit.當數據量少,且跨度極大
也就是稀疏的時候,原生的位圖不太適合.
那麼咱們來作一下總結:
位圖是用二進制位來存儲整形數據的一種數據結構,在不少方面都有應用,尤爲是在大數據量的場景下,節省內存及提升運算效率十分實用.
他的優勢有:
缺點有:
上面講了位圖的原理,那麼咱們先來本身手動實現一個!
說明:由於後面還有JDK版本,因此這裏只實現了很簡陋的版本,方便理解位圖的核心原理便可.這個簡陋版本徹底不能夠直接使用,能跑,可是在不少狀況下都會直接報錯.
雖然簡陋,可是必須的仍是要有.
寫了一個僅支持bit數量的構造參數. 由於咱們是用int數組來保存實際的數據,因此對傳入的值右移5
(至關於除以32,由於int是32位的嘛)就是int數組的大小.
支持將某一個位設置爲true/false.
爲了實現set-true
,實際上是有粗暴的符合人類思路的邏輯的,好比當調用set(5,true)
的時候,咱們將int數字轉化爲二進制字符串,獲得000000000000000000000000000000
(應該是32個我沒數),而後將其右邊第六位置爲1,獲得000000000000000000000000100000
,而後再轉回int數字.
這個方法很符合位圖的直接定義,也很好理解,可是對於計算機來講,太麻煩了,並且過程當中須要一個String,佔用太多的內存空間了.
計算機更喜歡使用或運算來解決. 假設現有數字爲3,即000000000000000000000000001000
,這時候咱們調用了set(10,true)
,怎麼辦呢,首先使用左移,將第11位置爲1,而後與原來的值進行或操做.像下面這樣子:
原來值 : 000000000000000000000000001000
1右移10位: 000000000000000000010000000000
或操做的結果: 000000000000000000010000001000 ----> 能夠直接表示 3 和 10 兩個位上都爲1了.
複製代碼
設置某一個位爲false,和上面的流程不太同樣.除去粗暴的辦法以外,還能夠 對1右移x位
的非
取與
.很拗口,下面是示例:
咱們將3上的設爲0.
原來值 : 000000000000000000010000001000 ----> 10和3上爲1,
1右移3位: 000000000000000000000000001000
1右移3位取非後: 111111111111111111111111110111
原來的值與取非後取與: 000000000000000000010000000000 ----> 只有10上爲1了.
複製代碼
獲取某個位上的值.
固然也能夠用粗暴的轉換二進制字符串解決,可是使用與操做
更加快速且計算機友好.
對set方法中的例子來講,設置了3和10以後,若是獲取10上的值,能夠:
當前值: 000000000000000000010000001000
1右移10位: 000000000000000000010000000000
與操做的結果: 000000000000000000010000000000 ---> 只要這個數字不等於0,即說明10上爲1,等於0則爲0.
複製代碼
實際的代碼加註釋以下:
/** * Created by pfliu on 2019/07/02. */
public class BitMapTest {
// 實際使用int數組存儲
private int[] data;
/** * 構造方法,傳入預期的最大index. */
public BitMapTest(int size) {
this.data = new int[size >> 5];
}
/** * get 方法, 傳入要獲取的index, 返回bool值表明該位上爲1/0 */
public boolean get(int bitIdx) {
return (data[bitIdxToWorkIdx(bitIdx)] & (1 << bitIdx)) != 0;
}
/** * 將對應位置的值設置爲傳入的bool值 */
public void set(int idx, boolean v) {
if (v) {
set(idx);
} else {
clear(idx);
}
}
// 將index的值設置爲1
private void set(int idx) {
data[bitIdxToWorkIdx(idx)] |= 1 << idx;
}
// 將index上的值設置爲0
private void clear(int bitIdx) {
data[bitIdxToWorkIdx(bitIdx)] &= ~(1L << bitIdx);
}
// 根據bit的index獲取它存儲的實際int在數組中的index
private int bitIdxToWorkIdx(int bitIdx) {
return bitIdx >> 5;
}
public static void main(String[] args) {
BitMapTest t = new BitMapTest(100);
t.set(10, true);
System.out.println(t.get(9));
System.out.println(t.get(10));
}
}
複製代碼
JDK中對位圖是有實現的,實現類爲BitSet
,其中大體思想和上面實現的簡陋版本相似,只是其內部數據是使用long數組來存儲,此外加了許多的容錯處理.下面看一下源碼.仍是按照方法分類來看.
// long數組,64位的long是2的6次方
private final static int ADDRESS_BITS_PER_WORD = 6;
// 每個word的bit數量
private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD;
// 存儲數據的long數組
private long[] words;
// 上面的數組中使用到了的word的個數
private transient int wordsInUse = 0;
// 數組的大小是否由用戶指定的(註釋裏寫明瞭:若是是true,咱們假設用戶知道他本身在幹什麼)
private transient boolean sizeIsSticky = false;
複製代碼
BitSet提供了兩個公開的構造方法以及四個公開的工廠方法,分別支持從long[]
,LongBuffer
,bytes []
, ByteBuffer
中獲取BitSet實例.
各個方法及其內部調用的方法以下:
// ---------構造方法-------
// 無參的構造方法,初始化數組爲長度爲64個bit(即一個long)以及設置sizeIsSticky爲false.
public BitSet() {
initWords(BITS_PER_WORD);
sizeIsSticky = false;
}
// 根據用戶傳入的bit數量進行初始化,且設置sizeIsSticky爲true.
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];
}
// 根據bit數量獲取long數組的大小,右移6位便可.
private static int wordIndex(int bitIndex) {
return bitIndex >> ADDRESS_BITS_PER_WORD;
}
// ---------工廠方法,返回BitSet實例 -------
// 傳入long數組
public static BitSet valueOf(long[] longs) {
int n;
for (n = longs.length; n > 0 && longs[n - 1] == 0; n--)
;
return new BitSet(Arrays.copyOf(longs, n));
}
// 傳入LongBuffer
public static BitSet valueOf(LongBuffer lb) {
lb = lb.slice();
int n;
for (n = lb.remaining(); n > 0 && lb.get(n - 1) == 0; n--)
;
long[] words = new long[n];
lb.get(words);
return new BitSet(words);
}
// 傳入字節數組
public static BitSet valueOf(byte[] bytes) {
return BitSet.valueOf(ByteBuffer.wrap(bytes));
}
// 傳入ByteBuffer
public static BitSet valueOf(ByteBuffer bb) {
bb = bb.slice().order(ByteOrder.LITTLE_ENDIAN);
int n;
for (n = bb.remaining(); n > 0 && bb.get(n - 1) == 0; n--)
;
long[] words = new long[(n + 7) / 8];
bb.limit(n);
int i = 0;
while (bb.remaining() >= 8)
words[i++] = bb.getLong();
for (int remaining = bb.remaining(), j = 0; j < remaining; j++)
words[i] |= (bb.get() & 0xffL) << (8 * j);
return new BitSet(words);
}
複製代碼
BitSet提供了兩類set方法,
所以BitSet有四個重載的set方法.
// 將某個index的值設置爲true. 使用和上面本身實現的簡陋版本相同的或操做.
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();
}
// 將某個index設置爲傳入的值,注意當傳入值爲false的時候,調用的是clear方法.
public void set(int bitIndex, boolean value) {
if (value)
set(bitIndex);
else
clear(bitIndex);
}
// 將index上bit置爲0
public void clear(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
int wordIndex = wordIndex(bitIndex);
if (wordIndex >= wordsInUse)
return;
words[wordIndex] &= ~(1L << bitIndex);
recalculateWordsInUse();
checkInvariants();
}
// 將from->to之間的全部值設置爲true
public void set(int fromIndex, int toIndex) {
checkRange(fromIndex, toIndex);
if (fromIndex == toIndex)
return;
// Increase capacity if necessary
int startWordIndex = wordIndex(fromIndex);
int endWordIndex = wordIndex(toIndex - 1);
expandTo(endWordIndex);
long firstWordMask = WORD_MASK << fromIndex;
long lastWordMask = WORD_MASK >>> -toIndex;
if (startWordIndex == endWordIndex) {
// Case 1: One word
words[startWordIndex] |= (firstWordMask & lastWordMask);
} else {
// Case 2: Multiple words
// Handle first word
words[startWordIndex] |= firstWordMask;
// Handle intermediate words, if any
for (int i = startWordIndex+1; i < endWordIndex; i++)
words[i] = WORD_MASK;
// Handle last word (restores invariants)
words[endWordIndex] |= lastWordMask;
}
checkInvariants();
}
// 將from->to之間的全部值設置爲傳入的值,當傳入的值爲false的適合,調用的是下面的clear.
public void set(int fromIndex, int toIndex, boolean value) {
if (value)
set(fromIndex, toIndex);
else
clear(fromIndex, toIndex);
}
// 將範圍內的bit置爲0
public void clear(int fromIndex, int toIndex) {
checkRange(fromIndex, toIndex);
if (fromIndex == toIndex)
return;
int startWordIndex = wordIndex(fromIndex);
if (startWordIndex >= wordsInUse)
return;
int endWordIndex = wordIndex(toIndex - 1);
if (endWordIndex >= wordsInUse) {
toIndex = length();
endWordIndex = wordsInUse - 1;
}
long firstWordMask = WORD_MASK << fromIndex;
long lastWordMask = WORD_MASK >>> -toIndex;
if (startWordIndex == endWordIndex) {
// Case 1: One word
words[startWordIndex] &= ~(firstWordMask & lastWordMask);
} else {
// Case 2: Multiple words
// Handle first word
words[startWordIndex] &= ~firstWordMask;
// Handle intermediate words, if any
for (int i = startWordIndex+1; i < endWordIndex; i++)
words[i] = 0;
// Handle last word
words[endWordIndex] &= ~lastWordMask;
}
recalculateWordsInUse();
checkInvariants();
}
複製代碼
這裏有一個須要注意點,那就是當傳入的值爲true/fasle
的時候,處理邏輯是不一樣的.具體的邏輯見上面簡陋版本中的示例.
BitSet提供了一個獲取單個位置bit值的方法,以及一個範圍獲取,返回一個新的BitSet的方法.
// 獲取某個位置的bit值
public boolean get(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
checkInvariants();
int wordIndex = wordIndex(bitIndex);
return (wordIndex < wordsInUse)
&& ((words[wordIndex] & (1L << bitIndex)) != 0);
}
// 返回一個子集,包含傳入範圍內的bit
public BitSet get(int fromIndex, int toIndex) {
checkRange(fromIndex, toIndex);
checkInvariants();
int len = length();
// If no set bits in range return empty bitset
if (len <= fromIndex || fromIndex == toIndex)
return new BitSet(0);
// An optimization
if (toIndex > len)
toIndex = len;
BitSet result = new BitSet(toIndex - fromIndex);
int targetWords = wordIndex(toIndex - fromIndex - 1) + 1;
int sourceIndex = wordIndex(fromIndex);
boolean wordAligned = ((fromIndex & BIT_INDEX_MASK) == 0);
// Process all words but the last word
for (int i = 0; i < targetWords - 1; i++, sourceIndex++)
result.words[i] = wordAligned ? words[sourceIndex] :
(words[sourceIndex] >>> fromIndex) |
(words[sourceIndex+1] << -fromIndex);
// Process the last word
long lastWordMask = WORD_MASK >>> -toIndex;
result.words[targetWords - 1] =
((toIndex-1) & BIT_INDEX_MASK) < (fromIndex & BIT_INDEX_MASK)
? /* straddles source words */
((words[sourceIndex] >>> fromIndex) |
(words[sourceIndex+1] & lastWordMask) << -fromIndex)
:
((words[sourceIndex] & lastWordMask) >>> fromIndex);
// Set wordsInUse correctly
result.wordsInUse = targetWords;
result.recalculateWordsInUse();
result.checkInvariants();
return result;
}
複製代碼
JDK實現的位圖固然是有邏輯操做的,主要支持了與,或,異或,與非
四種操做,因爲代碼不難,這裏就不貼代碼了,簡略的貼一下API.
// 與操做
public void and(BitSet set);
// 或操做
public void or(BitSet set);
// 異或操做
public void xor(BitSet set);
// 與非操做
public void andNot(BitSet set);
複製代碼
到這裏,BitSet的源碼就讀完了,可是有沒有發現一個問題 ? 前面說的稀疏數據
的問題並無獲得解決,別急,下面就來了.
這是google開發的javaEWAH包中的一個類.名字中的EWAH = Enhanced Word-Aligned Hybrid
.而Compressed是指壓縮.
複習一下稀疏數據
的問題,假設咱們在一個位圖中,首先set(1)
,而後set(1億)
會怎樣?
咱們使用JDK中的BitSet來試一下,在運行過程當中打斷點看一下內部的數組是什麼樣子.以下圖:
將其序列化輸出到文件,文件大小以下圖:
能夠看到,咱們爲了保存1和1億這兩個數字,花費了一個一千多萬長度的long數組,序列化後佔用內存將近200m.這是不科學的.
接下來就是EWAHCompressedBitmap
了,名字裏面都帶了壓縮,那麼想必表現不錯.
能夠看到long數組的長度僅僅爲4,且輸出的文件大小爲96byte.
這就很符合預期了.
在EWAHCompressedBitmap中,數據也是使用long數組來保存的,不過對每個long有類別的定義,Literal Word
和Running Length Word
.
Literal Word
: 存儲真正的bit位.
Running Length Word
: 存儲跨度信息.
什麼是跨度信息呢? 舉個例子:
在剛纔使用BitSet存儲1億的時候,截圖中long數組有一千多萬個0,以及以後的一個值.
使用BitSet存儲1和1億(2048爲虛擬值,不想算了):
long | long | long | long | long | long | long | long | long | long | long | long |
---|---|---|---|---|---|---|---|---|---|---|---|
2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ...1千萬個0呢 | 0 | 0 | 2048 |
而在EWAHCompressedBitmap中,則是相似下面這樣:
long | long | long |
---|---|---|
2 | 一千萬個0 | 2048 |
這樣看起來好像沒什麼區別....可是在BitSet中,一千萬個0是真的使用了一千萬個long來存儲的.而在EWAHCompressedBitmap中,這個信息使用一個long來存儲,long的值表示具體有多少個0在這個區間內.
這樣子作,點題了.與名字中的壓縮相對應.將連續的0或者1進行壓縮,以節省空間.
這樣作有沒有什麼反作用呢?有的,當你的每一次插入都在一個Running Length Word
上,也就是每一次插入都涉及到了Running Length Word
的分裂,會降級性能,所以官方建議最好數據的插入從小到大進行.
EWAHCompressedBitmap基本解決了稀疏數據的問題,而當數據很稠密的時候,他的壓縮率沒有那麼好,可是一般也不會差於不壓縮的存儲方式,所以在平常的使用中,仍是建議你們使用這個類,除非你很清楚且能確保本身的數據不會過於稀疏.
在本節,咱們手動實現了一個極其簡陋的位圖,而後閱讀了JDK中位圖實現類BitSet
的源碼,而後分析瞭如何使用EWAHCompressedBitmap
來解決稀疏數據的問題,對於EWAHCompressedBitmap
的源碼具體實現沒有詳細分析,有興趣的朋友能夠本身去查看.
Java語言使用者普遍,所以對於位圖的實現,網上各類版本都有,既有大廠維護的開源版本,也有我的編寫的版本.在使用時也不用徹底侷限於EWAHCompressedBitmap,可使用各類魔改版本,因爲位圖的實現邏輯不是特別複雜,所以在使用前清楚其具體的實現邏輯便可.
Redis是支持位圖的,可是位圖並非一個單獨的數據結構,而是在String類型上定義的一組面向位的操做指令.也就是說,當你使用Redis位圖時,其實底層存儲的是Redis的string類型.所以:
Redis支持的操做以下:
getbit key offset
.setbit key offset value
.bitcount key start end
bitpos key bit(0/1) start end
BitSet
支持的四種同樣,具體的命令以下:BITOP AND destkey srckey1 srckey2 srckey3 ... srckeyN
BITOP OR destkey srckey1 srckey2 srckey3 ... srckeyN
BITOP XOR destkey srckey1 srckey2 srckey3 ... srckeyN
BITOP NOT destkey srckey
複製代碼
其中destkey是結果存儲的key,其他的srckey是參與運算的來源.
應用場景實際上是很考驗人的,不能學以至用,在程序員行業裏基本上就至關於沒有學了吧...
通過本身的摸索以及在網上的瀏覽,大體見到了一些應用場景,粗略的寫出來,方便你們理解而且之後遇到相似的場景能夠想到位圖並應用他!
用戶簽到天天只能一次,搶購活動中只能購買一件,這些需求致使的有一種查詢請求,給定的id作沒作過某事
.並且通常這種需求都沒法接受你去查庫的延遲.固然你查一次庫以後在redis中寫入:key = 2345 , value = 簽到過了
.也是能夠實現的,可是內存佔用太大.
而使用位圖以後,當2345用戶簽到過/搶購過以後,在redis中調用setbit 2019-07-01-簽到 2345 1
便可,以後用戶的每次簽到/搶購請求進來,只須要執行相應的getbit便可拿到是否放行的bool值.
這樣記錄,不只能夠節省空間,以及加快訪問速度以外,還能夠提供一些額外的統計功能,好比調用bitcount
來統計今天簽到總人數等等.統計速度通常是優於關係型數據庫的,能夠用來作實時的接口查詢等.
大數據已經很廣泛了,用戶畫像你們也都在作,這時候須要根據標籤分類用戶,進行存儲.方便後續的推薦等操做.
而用戶及標籤的數據結構設計是一件比較麻煩的事情,且很容易形成查詢性能過低.同時,對多個標籤常常須要進行邏輯操做,好比喜歡電子產品的00後用戶有哪些,女性且愛旅遊的用戶有哪些等等,這在關係型數據庫中都會形成處理的困難.
可使用位圖來進行存儲,每個標籤存儲爲一個位圖(邏輯上,實際上你還能夠按照尾號分開等等操做),在須要的時間進行快速的統計及計算. 如:
用戶 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
愛旅遊 | 1 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 |
能夠清晰的統計出,0,3,6
用戶喜歡旅遊.
用戶 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
00後 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
用戶0,1,6
是00後.
那麼對兩個位圖取與便可獲得愛旅遊的00後用戶爲0,6
.
這個就比較有名了,關於這個的詳細信息能夠查看 布隆過濾器(bloom filter)的原理及在推薦去重中的應用
總之,bitmap能夠高效且節省空間的存儲與用戶ID相關聯的布爾數據.常見的能夠應用其作大量數據的去重以及統計.更多的應用就開發你的想象力吧.
BitSet/EWAHCompressedBitmap源碼
以上皆爲我的所思所得,若有錯誤歡迎評論區指正。
歡迎轉載,煩請署名並保留原文連接。
聯繫郵箱:huyanshi2580@gmail.com
更多學習筆記見我的博客------>呼延十