zoukankan      html  css  js  c++  java
  • BitSet

    BitSet

    一、BitSet简介

    ​ 类实现了一个按需增长的位向量。位 set 的每个组件都有一个boolean值。用非负的整数将BitSet的位编入索引。可以对每个编入索引的位进行测试、设置或者清除。通过逻辑与、逻辑或和逻辑异或操作,可以使用一个BitSet修改另一个BitSet的内容。

    ​ 默认情况下,set 中所有位的初始值都是false。

    ​ 每个位 set 都有一个当前大小,也就是该位 set 当前所用空间的位数。注意,这个大小与位 set 的实现有关,所以它可能随实现的不同而更改。位 set 的长度与位 set 的逻辑长度有关,并且是与实现无关而定义的。

    ​ 除非另行说明,否则将 null 参数传递给BitSet中的任何方法都将导致NullPointerException。

    ​ 在没有外部同步的情况下,多个线程操作一个BitSet是不安全的

    二、基本原理

    内部维护一个long数组words, 每一个bit位值为0或1即false和true, 初始只有一个long,所以BitSet最小的size是64,当随着存储的元素越来越多,BitSet内部会动态扩充(每次扩容为原来的两倍),最终内部是由N个long来存储;

    用1位来表示一个数据是否出现过,0为没有出现过,1表示出现过。使用用的时候既可根据某一个是否为0表示,此数是否出现过。一个1G的空间,有8*1024*1024*1024=8.58*10^9bit,也就是可以表示85亿个不同的数。

    三、Java中BitSet的实现

    BitSet位于java.util这个包, 以下基于jdk8对BitSet实现进行分析;

    3.1 部分属性

    • ADDRESS_BITS_PER_WORD: 值为6, 一个long类型占8个byte, 有64bit, 而 2^6 = 64;
    • BITS_PER_WORD: 值为1 << ADDRESS_BITS_PER_WORD = 64;
    • BIT_INDEX_MASK: 索引掩码,
    • words: BitSet内部维护的long类型数组, BitSet的核心;
    • wordsInUse: 记录BitSet中words使用了的长度;
    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;
    
       /**
        * 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;
    }
    

    3.2 常用方法

    BitSet主要分析的方法:

    • 构造方法
    • 反转某一位: flip
    • 设置某一指定位: set
    • 获取某一位: get
    • 清空bit位: clear
    • 获取size, length: size, length
    • BitSet存储的数量: cardinality
    • 下一个bit位: nextSetBit

    1. 构造方法

    • 无参构造, 默认words长度为1
    public BitSet() {
        initWords(BITS_PER_WORD); // 初始化words
        sizeIsSticky = false;
    }
    
    • 有参构造, 根据nbits计算出words长度
    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;
    }
    
    • 初始化words;
    // 根据nbits大小初始化 words的长度
    private void initWords(int nbits) {
        words = new long[wordIndex(nbits-1) + 1];
    }
    // 将bitIndex右移64位, 计算出bitIndex在words中的索引;
    private static int wordIndex(int bitIndex) {
        return bitIndex >> ADDRESS_BITS_PER_WORD;
    }
    

    2. flip反转某一位

    public void flip(int bitIndex) {
        if (bitIndex < 0)
            throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
    
        int wordIndex = wordIndex(bitIndex);
        expandTo(wordIndex);
    
        words[wordIndex] ^= (1L << bitIndex);
    
        recalculateWordsInUse();
        checkInvariants();
    }
    

    3. 一个检查扩容;两个检查函数

    • 检查扩容
    // wordsInUse表示BitSet中使用的
    private void expandTo(int wordIndex) {
        int wordsRequired = wordIndex+1;
        if (wordsInUse < wordsRequired) {
            ensureCapacity(wordsRequired);
            wordsInUse = wordsRequired;
        }
    }
    private void ensureCapacity(int wordsRequired) {
        if (words.length < wordsRequired) {
            // Allocate larger of doubled size or required size
            int request = Math.max(2 * words.length, wordsRequired);
            words = Arrays.copyOf(words, request);
            sizeIsSticky = false;
        }
    }
    
    • 更新wordsInUse的值, wordsInUse的值可以被用来计算length;
    private void recalculateWordsInUse() {
        // Traverse the bitset until a used word is found
        int i;
        for (i = wordsInUse-1; i >= 0; i--)
            if (words[i] != 0)
                break;
    
        wordsInUse = i+1; // The new logical size
    }
    
    • checkInvariants 可以看出是检查内部状态,尤其是wordsInUse是否合法的函数。
    private void checkInvariants() {
        assert(wordsInUse == 0 || words[wordsInUse - 1] != 0);
        assert(wordsInUse >= 0 && wordsInUse <= words.length);
        assert(wordsInUse == words.length || words[wordsInUse] == 0);
    }
    

    4. set

    注意: 按位左移 若移动的位数超过被移动数类型的bit数, 则会对移位数取模, 再左移; 例如 11L<<65 等价于 11L << 1

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

    5. get

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

    6. 清空BitSet

    • 清空所有的bit位,即全部置0。通过循环方式来以此以此置0。
    public void clear() {  
        while (wordsInUse > 0)  
            words[--wordsInUse] = 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();
    }
    

    第一行是参数检查,如果bitIndex小于0,则抛参数非法异常。后面执行的是bitset中操作中经典的两步:a. 找到对应的long b. 操作对应的位。
    a. 找到对应的long。 这行语句是 int wordIndex = wordIndex(bitIndex);
    b. 操作对应的位。常见的位操作是通过与特定的mask进行逻辑运算来实现的。因此,首先获取 mask(掩码)。
    对于 clear某一位来说,它需要的掩码是指定位为0,其余位为1,然后与对应的long进行&运算。
    ~(1L << bitIndex); 即获取mask
    words[wordIndex] &= ; 执行相应的运算。
    注意:这里的参数检查,对负数index跑出异常,对超出大小的index,不做任何操作,直接返回。

    • 清空指定范围的那些bits
     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();
    }
    

    方法是将这个范围分成三块,startword; interval words; stopword。
    其中startword,只要将从start位到该word结束位全部置0;interval words则是这些long的所有bits全部置0;而stopword这是从起始位置到指定的结束位全部置0。
    而特殊情形则是没有startword和stopword是同一个long。
    具体的实现,参照代码,是分别作出两个mask,对startword和stopword进行操作。

    7. size, length

    • 获取size, size即为words数组中bit位的数量
    public int size() {
        return words.length * BITS_PER_WORD; // BITS_PER_WORD = 64
    }
    
    • length, 值等于使用了的最高的bit位索引 + 1;
    public int length() {
        if (wordsInUse == 0)
            return 0;
    
        return BITS_PER_WORD * (wordsInUse - 1) +
            (BITS_PER_WORD - Long.numberOfLeadingZeros(words[wordsInUse - 1]));
    }
    

    8. cardinality, BitSet中存储的数据个数

    • Long.bitCount(words[i]): 计算出一个long类型的二进制中1的个数;
    public int cardinality() {
        int sum = 0;
        for (int i = 0; i < wordsInUse; i++)
            sum += Long.bitCount(words[i]);
        return sum;
    }
    

    9. nextSetBit

    /**
     * fromIndex后面为bit为值为1的bit索引
     * @param  fromIndex [description]
     * @return           [description]
     */
    public int nextSetBit(int fromIndex) {
        if (fromIndex < 0)
            throw new IndexOutOfBoundsException("fromIndex < 0: " + fromIndex);
    
        checkInvariants();
    
        int u = wordIndex(fromIndex);
        if (u >= wordsInUse)
            return -1;
    
        long word = words[u] & (WORD_MASK << fromIndex);
    
        while (true) {
            if (word != 0)
                return (u * BITS_PER_WORD) + Long.numberOfTrailingZeros(word);
            if (++u == wordsInUse)
                return -1;
            word = words[u];
        }
    

    四、Java中Bitset的使用

    由于BitSet中可以实现1G的内存存储8亿个不同的数据, 因此可以用来处理大量数据的情况;

    使用场景:

    • 对海量数据进行一些统计工作的时候,比如日志分析、用户数统计

    如统计40亿个数据中没有出现的数据,将40亿个不同数据进行排序等。
    现在有1千万个随机数,随机数的范围在1到1亿之间。现在要求写出一种算法,将1到1亿之间没有在随机数中的数求出来

    • mq中, 防止消息被重复消费;
    package com.ricky.java.test;
     
    import java.util.BitSet;
     
    public class BitSetTest {
     
    	/**
    	 * @param args
    	 */
    	public static void main(String[] args) {
    		// 字符串去除重复出现的字符
    		containChars("abcdfab");
    		// 对数组数据进行排序, 去重;
    		sortArray(new int[] { 423, 700, 9999, 2323, 356, 6400, 1,2,3,2,2,2,2 });
    	}
    	
    	/**
    	 * 字符串去除重复出现的字符
    	 * @param str [description]
    	 */
        public static void containChars(String str) {
            BitSet used = new BitSet();
            for (int i = 0; i < str.length(); i++)
                used.set(str.charAt(i)); // set bit for char
     
            StringBuilder sb = new StringBuilder();
            sb.append("[");
            int size = used.size();
            System.out.println(size);
            for (int i = 0; i < size; i++) {
                if (used.get(i)) {
                    sb.append((char) i);
                }
            }
            sb.append("]");
            System.out.println(sb.toString());
        }
        
        /**
         * 对数组去重排序
         */
        public static void sortArray(int[] array) {
            
            BitSet bitSet = new BitSet(2 << 13);
            // 虽然可以自动扩容,但尽量在构造时指定估算大小,默认为64
            System.out.println("BitSet size: " + bitSet.size());
     
            for (int i = 0; i < array.length; i++) {
                bitSet.set(array[i]);
            }
            //剔除重复数字后的元素个数
            int bitLen=bitSet.cardinality();    
     
            //进行排序,即把bit为true的元素复制到另一个数组
            int[] orderedArray = new int[bitLen];
            int k = 0;
            for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) {
                orderedArray[k++] = i;
            }
     
            System.out.println("After ordering: ");
            for (int i = 0; i < bitLen; i++) {
                System.out.print(orderedArray[i] + "	");
            }
             
            System.out.println("iterate over the true bits in a BitSet");
            //或直接迭代BitSet中bit为true的元素iterate over the true bits in a BitSet
            for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) {
                System.out.print(i+"	");
            }
        }
    }
    
  • 相关阅读:
    C++ Primer高速入门之六:数组和指针
    C++ Primer高速入门之六:数组和指针
    C++ Primer高速入门之六:数组和指针
    大学生毕业卖蔬菜,成就财富梦想
    美女毕业去养牛,创造自主牛肉品牌
    情侣合开夫妻店,爱情和努力让他们生活走向光明
    从3万元创业资金到年销售3亿元,看他是如何做到的?
    “海归”创办服装公司,全国竟拥有2000多家网点?
    90后卖地瓜,仅仅一个月就可以赚2万元
    老头创业弄养殖,每亩收入3万元,水蛭为啥这么值钱?
  • 原文地址:https://www.cnblogs.com/jxkun/p/9479132.html
Copyright © 2011-2022 走看看