Java位向量的實現原理與巧妙應用

Java位向量的實現原理與巧妙應用

一、博文介紹

  本篇博文將會介紹幾本的位運算含義、位向量介紹、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);
View Code

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);
            }
        }

    }
}
View Code

 

三、Java位向量介紹-BitSet

   位向量,也叫位圖,是一個咱們常常能夠用到的數據結構,在使用小空間來處理大量數據方面有着得天獨厚的優點;位向量的定義就是一串由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();
    }

}
View Code

 

四、BitSet的應用

  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 ]
*/
View Code

  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++

  http://blog.luoyuanhang.com/2016/05/15/I-位向量的實現與應用8/

相關文章
相關標籤/搜索