本篇博文將會介紹幾本的位運算含義、位向量介紹、BitSet實現原理、Java位向量的應用、拓展介紹Bloom Filter等。html
1) 位運算符java
java中位運算操做符主要包括: &: 與 |: 或 ^: 異或
~: 非
前三種能夠和 = 結合使用,好比 &=、|=、^=;可是~是單目運算符,不能和=結合使用。
<<: 左移運算,至關於乘法,低位補0;
>>: 右移運算,至關於除法,有符號移位若高位爲正,則高位補0,若爲負,則高位補1;
java中增長了一種"無符號"右移,>>>,它使用零擴展,不管正負都在高位插入0;
移位操做與等號也能夠組合使用: >>=、<<=
2)位運算簡單應用git
// 1. 得到int型最大值;2147483647的十六進制爲0x7FFFFFFF,其中最高位爲符號位 System.out.println((1 << 31) - 1);// 2147483647, 因爲優先級關係,括號不可省略 System.out.println(~(1 << 31));// 2147483647 // 2. 得到int型最小值 System.out.println(1 << 31); System.out.println(1 << -1); // 3. 判斷一個數n是否是2的冪 System.out.println((n & (n - 1)) == 0); /*若是是2的冪,n必定是100... n-1就是1111.... 因此作與運算結果爲0*/ // 4. 計算2的n次方 n > 0 System.out.println(2<<(n-1)); // 5. 從低位到高位,將n的第m位置爲0 System.out.println(n & ~(0<<(m-1))); /* 將1左移m-1位找到第m位,取反後變成111...0...1111 n再和這個數作與運算*/ // 6. 從低位到高位,取n的第m位 int m = 2; System.out.println((n >> (m-1)) & 1); // 7. 從低位到高位.將n的第m位置爲1 System.out.println(n | (1<<(m-1))); /*將1左移m-1位找到第m位,獲得000...1...000 n在和這個數作或運算*/ // 8. 得到long類型的最大值 System.out.println(((long)1 << 127) - 1); // 9. 乘以2運算 System.out.println(10<<1); // 10. 求兩個整數的平均值 System.out.println((a+b) >> 1); // 11. 除以2運算(負奇數的運算不可用) System.out.println(10>>1); // 12. 判斷一個數的奇偶性,利用的是最後一位 System.out.println((10 & 1) == 1); System.out.println((9 & 1) == 1); // 13. 不用臨時變量交換兩個數(面試常考) a ^= b; b ^= a; a ^= b; // 14. 取絕對值(某些機器上,效率比n>0 ? n:-n 高) int n = -1; System.out.println((n ^ (n >> 31)) - (n >> 31)); /* n>>31 取得n的符號,若n爲正數,n>>31等於0,若n爲負數,n>>31等於-1 若n爲正數 n^0-0數不變,若n爲負數n^-1 須要計算n和-1的補碼,異或後再取補碼, 結果n變號而且絕對值減1,再減去-1就是絕對值 */ // 15. 取兩個數的最大值(某些機器上,效率比a>b ? a:b高) System.out.println(b&((a-b)>>31) | a&(~(a-b)>>31)); // 16. 取兩個數的最小值(某些機器上,效率比a>b ? b:a高) System.out.println(a&((a-b)>>31) | b&(~(a-b)>>31)); // 17. 判斷符號是否相同(true 表示 x和y有相同的符號, false表示x,y有相反的符號。) System.out.println((a ^ b) > 0);
3)應用 - 小遊戲中狀態的判斷,如鬥地主判斷四人是否處於準備狀態github
充分利用一個位有兩種狀態,能夠表明開閉、是否準備好等二狀態場景中,即使是多狀態也能夠用多位來實現,好比在迷宮問題中,能夠用00 01 10 11 來表明四個方向。若是正常的判斷四人是否處於準備狀態,可定義四個變量,可是若是用位運算,則一個byte類型變量的低4位就足夠了。面試
在提升運行速度的同時,也對程序的可讀性形成了影響,上面只是舉例位運算能夠應用在相似的場景中,具體適不適合根據項目背景而定。可使用設計模式來解決,底層用位實現,封裝到上層以後只公開方法。算法
實現代碼:編程
/** * Java 位運算的經常使用方法封裝<br> */ public class BitUtils { /** * 獲取運算數指定位置的值<br> * 例如: 0000 1011 獲取其第 0 位的值爲 1, 第 2 位 的值爲 0<br> * * @param source * 須要運算的數 * @param pos * 指定位置 (0<=pos<=7) * @return 指定位置的值(0 or 1) */ public static byte getBitValue(byte source, int pos) { return (byte) ((source >> pos) & 1); } /** * 將運算數指定位置的值置爲指定值<br> * 例: 0000 1011 須要更新爲 0000 1111, 即第 2 位的值須要置爲 1<br> * * @param source * 須要運算的數 * @param pos * 指定位置 (0<=pos<=7) * @param value * 只能取值爲 0, 或 1, 全部大於0的值做爲1處理, 全部小於0的值做爲0處理 * * @return 運算後的結果數 */ public static byte setBitValue(byte source, int pos, byte value) { byte mask = (byte) (1 << pos); if (value > 0) { source |= mask; } else { source &= (~mask); } return source; } /** * 將運算數指定位置取反值<br> * 例: 0000 1011 指定第 3 位取反, 結果爲 0000 0011; 指定第2位取反, 結果爲 0000 1111<br> * * @param source * * @param pos * 指定位置 (0<=pos<=7) * * @return 運算後的結果數 */ public static byte reverseBitValue(byte source, int pos) { byte mask = (byte) (1 << pos); return (byte) (source ^ mask); } /** * 檢查運算數的指定位置是否爲1<br> * * @param source * 須要運算的數 * @param pos * 指定位置 (0<=pos<=7) * @return true 表示指定位置值爲1, false 表示指定位置值爲 0 */ public static boolean checkBitValue(byte source, int pos) { source = (byte) (source >>> pos); return (source & 1) == 1; } /** * 入口函數作測試<br> * * @param args */ public static void main(String[] args) { // 取十進制 11 (二級制 0000 1011) 爲例子 byte source = 11; // 取第2位值並輸出, 結果應爲 0000 1011 for (byte i = 7; i >= 0; i--) { System.out.printf("%d ", getBitValue(source, i)); } // 將第6位置爲1並輸出 , 結果爲 75 (0100 1011) System.out.println("\n" + setBitValue(source, 6, (byte) 1)); // 將第6位取反並輸出, 結果應爲75(0100 1011) System.out.println(reverseBitValue(source, 6)); // 檢查第6位是否爲1,結果應爲false System.out.println(checkBitValue(source, 6)); // 輸出爲1的位, 結果應爲 0 1 3 for (byte i = 0; i < 8; i++) { if (checkBitValue(source, i)) { System.out.printf("%d ", i); } } } }
位向量,也叫位圖,是一個咱們常常能夠用到的數據結構,在使用小空間來處理大量數據方面有着得天獨厚的優點;位向量的定義就是一串由0.1組成的序列。設計模式
Java中對位向量的實現類時Java.util.BitSet;C++標準庫中也有相應的實現,原理都是同樣的;BitSet源碼也很簡單,很容易看懂,若是讀者在對位向量有必定的瞭解後,能夠經過讀源碼來了解BitSet的具體實現。數組
一個bit上有兩個值,正好能夠用來判斷某些是非狀態的場景,在針對大數據場景下判斷存在性,BitSet是相比其餘數據結構好比HashMap更好的選擇,在Java中,位向量是用一個叫words的long型數組實現的,一個long型變量有64位,能夠保存64個數字;好比咱們有[2,8,6,10,15]這5個數要保存,通常存儲須要 5*4 = 20字節的存儲空間。可是若是咱們使用Java.util.BitSet進行存儲則能夠節省不少的空間只須要一個long型數字就夠了。BitSet只面向數字只面向數字使用,對於string類型的數據,能夠經過hashcode值來使用BitSet。數據結構
因爲,1 << 64, 1<<128, 1<<192 這些數字的結果都爲1,BitSet內部,long[]數組的大小由BitSet接收的最大數字決定,這個數組將數字分段表示[0,63],[64,127],[128,191]...。即long[0]用來存儲[0,63]這個範圍的數字的「存在性」,long[1]用來存儲[64,127],依次輪推,這樣就避免了位運算致使的衝突。原理以下:
|------------|----------|----------|----------|----------| | | 數字範圍 [0,63] [64,127] [128,191] ... | |------------|----------|----------|----------|----------| | | long數組索引 0 1 2 ... | |------------|----------|----------|----------|----------|
Java的BitSet每次申請空間,申請64位,即一個long型變量所佔的位數;
BitSet源碼實現-縮小版:
package java.util; import java.io.*; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.LongBuffer; public class BitSet implements Cloneable, java.io.Serializable { /** 在Java裏面BitSets被打包成一個叫「words」的long型數組,不過words是private的對外不公開, 只公開了操做他們的方法; */ private final static int ADDRESS_BITS_PER_WORD = 6; //2^6=64,程序中出現的 >>6 private final static int BITS_PER_WORD = 64; private final static int BIT_INDEX_MASK = 63; private static final long WORD_MASK = 0xffffffffffffffffL; private long[] words; private transient int wordsInUse = 0; //開了幾個long型數組 public BitSet() { initWords(64); } public BitSet(int nbits) { if (nbits < 0) throw new NegativeArraySizeException("nbits < 0: " + nbits); initWords(nbits); } private void initWords(int nbits) { //初始化多少個long型數組才能存下?除以64(>>6) 而後+1; words = new long[((nbits-1) >> 6) + 1]; } public void set(int bitIndex) { int wordIndex = (bitIndex >> 6); //除以64定位到某個long型變量; words[wordIndex] |= (1L << bitIndex); // Restores invariants } public boolean get(int bitIndex) { int wordIndex = (bitIndex >> 6); return (words[wordIndex] & (1L << bitIndex)) != 0; } public void clear(int bitIndex) { words[wordIndex] &= ~(1L << bitIndex); } public void clear() { while (wordsInUse > 0) words[--wordsInUse] = 0; } public boolean isEmpty() { return wordsInUse == 0; } public int cardinality() { int sum = 0; for (int i = 0; i < wordsInUse; i++) sum += Long.bitCount(words[i]); return sum; } public void and(BitSet set) { if (this == set) return; while (wordsInUse > set.wordsInUse) words[--wordsInUse] = 0; // Perform logical AND on words in common for (int i = 0; i < wordsInUse; i++) words[i] &= set.words[i]; recalculateWordsInUse(); checkInvariants(); } }
1)《編程珠璣》中的排序問題
問題重述:一個最多包含n個正整數的文件,每一個數都小於n,其中n=107,而且沒有重複。最多有1MB內存可用。要求用最快方式將它們排序並按升序輸出。
解決方案就是:把文件一次讀入,出現的數字在位向量對應索引處中標註爲1,讀取完文件以後,將位向量從低位向高位依次將爲1的索引輸出便可。
相關代碼:
package cn.liuning.test; import java.util.BitSet; public class MainTest { /** 使用BitSet進行排序 */ public static void main(String[] args) { int[] data={1,2,5,9,11,21,12,15}; int max = 0; for(int i=0;i<data.length;i++){ if(max < data[i]){ max = data[i]; } } BitSet bm=new BitSet(max+1); System.out.println("The size of bm:"+bm.size()); for(int i=0;i<data.length;i++){ bm.set(data[i], true); } StringBuffer buf=new StringBuffer(); buf.append("["); for(int i=0;i<bm.size();i++){ if(bm.get(i) == true){ buf.append(String.valueOf(i)+" "); } } buf.append("]"); System.out.println(buf.toString()); } } /* 輸出: The size of bm:64 [1 2 5 9 11 12 15 21 ] */
2)使用BitSet作String類型數據的存在性校驗
一種方案:
BitSet bitSet = new BitSet(Integer.MAX_VALUE);//hashcode的值域 //0x7FFFFFFF (int類型的最大值,第一位是符號位,可用Integer.MAX_VALUE代替) String url = "http://baidu.com/a"; int hashcode = url.hashCode() & 0x7FFFFFFF; bitSet.set(hashcode); System.out.println(bitSet.cardinality()); //狀態爲true的個數 System.out.println(bitSet.get(hashcode)); //檢測存在性 bitSet.clear(hashcode); //清除狀態
使用上述算法須要解決Java中hashcode存在衝突的問題。即不一樣的String可能獲得的hashcode是同樣的(即便不重寫hashcode方法)。如何解決?調整hashcode生成算法:咱們能夠對一個String使用多個hashcode算法,生成多個hashcode,而後在同一個BitSet進行屢次「着色」,在判斷存在性時,只有全部的着色位爲true時,才斷定成功。
String url = "http://baidu.com/a"; int hashcode1 = url.hashCode() & 0x7FFFFFFF; bitSet.set(hashcode1); int hashcode2 = (url + "-seed-").hashCode() & 0x7FFFFFFF; bitSet.set(hashcode2); System.out.println(bitSet.get(hashcode1) && bitSet.get(hashcode2)); //也能夠在兩個不一樣的bitSet上進行2次「着色」,這樣衝突性更小。但會消耗雙倍的內存
其實咱們可以看出,這種方式下降了誤判的機率。可是若是BitSet中存儲了較多的數字,那麼互相覆蓋着色,最終數據衝突的可能性會逐漸增長,最終仍然有必定機率的判斷失誤。因此在hashcode算法的個數與實際String的個數之間有一個權衡,咱們建議:
「hashcode算法個數 * String字符串的個數」 < Integer.MAX_VALUE * 0.8;
另外一種解決方案:多個BitSet並行保存
改良1)中的實現方式,咱們仍然使用多個hashcode生成算法,可是每一個算法生成的值在不一樣的BitSet中着色,這樣能夠保持每一個BitSet的稀疏度(下降衝突的概率)。在實際結果上,比1)的誤判率更低,可是它須要額外的佔用更多的內存,畢竟每一個BitSet都須要佔用內存。這種方式,一般是縮小hashcode的值域,避免內存過分消耗。
BitSet bitSet1 = new BitSet(Integer.MAX_VALUE);//127M
BitSet bitSet2 = new BitSet(Integer.MAX_VALUE); String url = "http://baidu.com/a"; int hashcode1 = url.hashCode() & 0x7FFFFFFF; bitSet1.set(hashcode1); int hashcode2 = (url + "-seed-").hashCode() & 0x7FFFFFFF; bitSet2.set(hashcode2);
System.out.println(bitSet1.get(hashcode1) && bitSet2.get(hashcode2));
最後:咱們要考慮是否有必要徹底避免誤判,可能有時候這種誤判也是咱們須要的結果。若是作到100%的正確判斷率,在原理上說BitSet是沒法作的,BitSet可以保證「若是斷定結果爲false,那麼數據必定是不存在;可是若是結果爲true,可能數據存在,也可能不存在(衝突覆蓋)」,即「false == YES,true == Maybe」。有人提出將衝突的數據保存在相似於BTree的額外數據結構中,事實上這種方式增長了設計的複雜度,並且最終仍然沒有良好的解決內存佔用較大的問題。
3)BloomFilter(布隆姆過濾器)
BloomFilter 的設計思想和BitSet有較大的類似性,目的也一致,它的核心思想也是使用多個Hash算法在一個「位圖」結構上着色,最終提升「存在性」判斷的效率。請參見Guava BloomFilter。以下爲代碼樣例:
Charset charset = Charset.forName("utf-8"); BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(charset),2<<21);//指定bloomFilter的容量 String url = "www.baidu.com/a"; bloomFilter.put(url); System.out.println(bloomFilter.mightContain(url));
BloomFilter(布隆姆過濾器)
http://www.programgo.com/article/17112318628/ Hash和Bloom Filter
http://wfwei.github.io/posts/hash-rel/ 類似哈希、完美哈希、Bloom Filter介紹 ***推薦閱讀
http://shift-alt-ctrl.iteye.com/blog/2194519 BitSet使用
http://longshaojian.iteye.com/blog/1946865 java位運算實際應用
http://www.cnblogs.com/wuyuegb2312/p/3136831.html 位向量定義與應用 C++