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


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

1、博文介紹

  本篇博文將會介紹幾本的位運算含義、位向量介紹、BitSet實現原理、Java位向量的應用、拓展介紹Bloom Filter等。

 

2、位運算介紹

  1) 位運算符

java中位運算操作符主要包括:   &: 與   |: 或   ^: 異或
  ~: 非
  前三種可以和 = 結合使用,比如 &=、|=、^=;但是~是單目運算符,不能和=結合使用。
  <<: 左移運算,相當於乘法,低位補0;
  >>: 右移運算,相當於除法,有符號移位若高位為正,則高位補0,若為負,則高位補1;
    java中增加了一種"無符號"
右移,>>>,它使用零擴展,無論正負都在高位插入0;
  移位操作與等號也可以組合使用: >>=、<<=

2)位運算簡單應用

// 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)應用 - 小游戲中狀態的判斷,如斗地主判斷四人是否處於准備狀態

  充分利用一個位有兩種狀態,可以代表開閉、是否准備好等二狀態場景中,即便是多狀態也可以用多位來實現,比如在迷宮問題中,可以用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

 

3、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

 

4、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)); 

 

5、延伸閱讀和參考資料

  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/

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM