BitSet原理与实践

10 篇文章 3 订阅

1、BitSet原理

JAVA中,一个long型数字占用64位空间,根据上述“位图”的概念,那么一个long型数字就可以保存64个数字的“存在性”状态(无碰撞冲突时)。比如50个数字{0,1,10,…63},判定“15”是否存在,那么我们通常会首先将这些数字使用数组或者hashmap保存,然后再去判定,那么保存这些这些数据需要占用64 * 64位;如果使用位图,那么一个long型数字即可。(如果换成50个字符串,那么其节约的空间可能更大)。

  1. BitSet只面向数字比较,比如set(int a,boolean value)方法,将数字a在bitSet中设定为true或者false;此后可以通过get(int a)方法检测结果。对于string类型的数据,如果像使用BitSet,那么可以将其hashcode值映射在bitset中

  2. 首先我们需要知道:1,1<<64,1<<128,1<<192…等,这些数字的计算结果是相等的(位运算);这也是一个long数字,只能表达连续的(或者无冲突的)64个数字的状态,即如果把数字1在long中用位表示,那么数字64将无法通过同一个long数字中位表示--冲突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         ...   |  
|------------|----------|----------|----------|----------|  
  1. BitSet内部,是一个long[]数组,数组的大小由bitSet接收的最大数字决定。bitSet内部的long[]数组是基于向量的,即随着set的最大数字而动态扩展。数组的最大长度计算:举例最大数字如果是64,(64-1)/64+1=1最大数字如果是65,(65-1)/64+1=2
(maxValue - 1) >> 6  + 1  
  1. BitSet中set方法伪代码:
public void set(int number) {  
    int index = number >> 6;//找到number需要映射的数组的index。  
    if(index + 1 > length) {  
        ensureCapacity(index + 1);//重新扩展long[]数组  
    }   
    long[index] |= (1L << number);//冲突解决  
}  

5)计算BitSet所占内存的大小
(最大数字/64+1)*8字节,其中“最大数字/64+1”是计算出BizSet需要多少了long,一个long占8字节。

2、使用BitSet:本例中使用bitSet做String字符串的存在性校验。

//hashcode的值域  
BitSet bitSet = new BitSet(Integer.MAX_VALUE);
  
//0x7FFFFFFF  
String url = "http://baidu.com/a";  
int hashcode = url.hashCode() & 0x7FFFFFFF;  
bitSet.set(hashcode);  
  
//着色位的个数
System.out.println(bitSet.cardinality());  
//检测存在性  
//清除位数据  
System.out.println(bitSet.get(hashcode));
bitSet.clear(hashcode);

3、BitSet与Hashcode冲突

因为BitSet API只能接收int型的数字,即只能判定int数字是否在bitSet中存在。所以,对于String类型通常使用它的hashcode,但这有一种隐患,java中hashcode存在冲突问题,即不同的String可能得到的hashcode是一样的(即使不重写hashcode方法),如果我们不能很好的解决这个问题,那么就会出现“数据抖动”—不同的hashcode算法、运行环境、bitSet容量,会导致判断的结果有所不同。比如A、B连个字符串,它们的hashcode一样,如果A在BitSet中“着色”(值为true),那么检测B是否在BitSet存在时,也会得到true。

这个问题该如何解决或者缓解呢?

1)调整hashcode生成算法:我们可以对一个String使用多个hashcode算法生成多个hashcode,然后在同一个BitSet进行多次“着色”,在判断存在性时,只有所有的着色位为true时,才判定成功。如字符串的int = hashcode % bitset的count

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中存储了较多的数字,那么互相覆盖着色,最终数据冲突的可能性会逐渐增加,最终仍然有一定概率的判断失误。所以在hashcode算法的个数与实际String的个数之间有一个权衡,我们建议: “hashcode算法个数 * String字符串的个数” < Integer.MAX_VALUE * 0.8。

2) 多个BitSet并行保存

改良1)中的实现方式,我们仍然使用多个hashcode生成算法,但是每个算法生成的值在不同的BitSet中着色这样可以保持每个BitSet的稀疏度(降低冲突的几率)。在实际结果上,比1)的误判率更低但是它需要额外的占用更多的内存,毕竟每个BitSet都需要占用内存。这种方式,通常是缩小hashcode的值域,避免内存过度消耗,如字符串的int = hashcode % bitset的count。

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

3) 是否有必要完全避免误判?

如果做到100%的正确判断率,在原理上说BitSet是无法做的,BitSet能够保证“如果判定结果为false,那么数据一定是不存在;但是如果结果为true,可能数据存在,也可能不存在(冲突覆盖)”。有人提出将冲突的数据保存在类似于BTree的额外数据结构中,事实上这种方式增加了设计的复杂度,而且最终仍然没有良好的解决内存占用较大的问题。

4、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、应用场景

内存占用率比较高,由于gc stop the world导致rt较长,通过分析发现某个对象的string集合所占内存较大、且string可以映射为一个固定的下标,那么可以用BitSet代替string集合,降低内存占用。封装通用的类:根据String集合构建BitSet、根据BitSet计算String集合。

public class CompressInfo<T> {
    /**
     * 原始对象集合
     */
    private List<T> rawList;
    /**
     * 原始对象和集合下标的关系
     */
    private Map<String, Integer> rawIndexMap;
    /**
     * 原始对象和集合下标的关系
     */
    private Function<T, String> getKeyFunction;

    /**
     * 构造函数
     *
     * @param getKeyFunction 获取key的方法
     */
    public CompressInfo(Function<T, String> getKeyFunction) {
        this.rawList = Lists.newArrayList();
        this.rawIndexMap = new HashMap<>();
        this.getKeyFunction = getKeyFunction;
    }

    /**
     * 添加原生数据
     *
     * @param data 原生数据
     */
    public void add(T data) {
        rawList.add(data);
        rawIndexMap.put(getKey(data), rawList.size() - 1);
    }

    /**
     * 将对象数组 转成 bitset
     *
     * @param dataList 对象集合
     * @return bitset:对象在全部对象中的下标的压缩信息
     */
    public BitSet buildBitSet(List<T> dataList) {
        if (CollectionUtils.isEmpty(dataList)) {
            BitSet bitSet = new BitSet();
            return bitSet;
        }

        BitSet bitSet = new BitSet(rawList.size());
        for (T data : dataList) {
            Integer index = rawIndexMap.get(getKey(data));
            if (Objects.nonNull(index)) {
                bitSet.set(index);
            } else {
                log.warn("buildBitSet 异常, key={} 在全量原生数据map中没有找到", getKey(data));
            }
        }
        return bitSet;
    }

    /**
     * 将bitset 转成 对象集合
     *
     * @param bitSet bitset:对象在全部对象中的下标的压缩信息
     * @return 对象集合
     */
    public List<T> getDataList(BitSet bitSet) {
        return bitSet.stream().mapToObj(e -> rawList.get(e)).collect(Collectors.toList());
    }

    private String getKey(T data) {
        return getKeyFunction.apply(data);
    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
`std::bitset` 是 C++ 标准库中提供的一个类模板,用于表示固定大小的二进制序列。它的原理是使用一个整数类型(通常是无符号整数)来存储二进制数据,并提供了一组成员函数和操作符来对二进制数据进行各种操作。 `std::bitset` 类模板的基本原理如下: 1. 内部存储:`std::bitset` 使用一个整数类型的变量来存储二进制数据。这个整数变量的大小由模板参数指定,例如 `std::bitset<N>` 中的 `N` 指定了位数。通常情况下,整数类型是无符号整数,如 `unsigned long long`。 2. 位级操作:`std::bitset` 提供了一组成员函数和操作符,用于对二进制数据进行位级操作。这些操作包括按位与(`&`)、按位或(`|`)、按位异或(`^`)、按位取反(`~`)等。通过这些操作,你可以对 `std::bitset` 中的二进制数据进行位级运算。 3. 数值转换:`std::bitset` 支持将二进制数据转换为整数类型,以及将整数类型转换为二进制数据。你可以使用 `to_ulong()` 成员函数将 `std::bitset` 转换为无符号整数,或使用 `to_ullong()` 将其转换为无符号长长整数。相反,你可以使用 `std::bitset` 的构造函数将整数类型转换为 `std::bitset`。 4. 迭代器支持:`std::bitset` 支持迭代器,使你能够对 `std::bitset` 的每个位进行迭代访问。 通过这些原理和功能,`std::bitset` 提供了一种便捷的方式来处理和操作固定大小的二进制数据。 希望这能解答你的问题!如果还有其他疑问,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值