一文教你学会使用BitSet

开始之前,介绍一下​最近很火的开源技术,低代码。

作为一种软件开发技术逐渐进入了人们的视角里,它利用自身独特的优势占领市场一角——让使用者可以通过可视化的方式,以更少的编码,更快速地构建和交付应用软件,极大程度地降低了软件的开发、配置、部署和培训成本。

应用地址:https://www.jnpfsoft.com

开发语言:Java/.net

这是一个基于 Java Boot/.Net Core 构建的简单、跨平台快速开发框架。前后端封装了上千个常用类,方便扩展;采用微服务、前后端分离架构,集成了代码生成器,支持前后端业务代码生成,满足快速开发;框架集成了表单、报表、图表、大屏等各种常用的 Demo 方便直接使用;后端框架支持 Vue2、Vue3,平台即可私有化部署,也支持 K8S 部署。

1. BitSet 原理

java.util.BitSet 是一个高效的位数组,用于管理和操作二进制标志位。它不仅在空间上非常节省,而且在处理一系列布尔值时非常高效。下面详细介绍 BitSet 的实现原理和使用方法。

1.1. 数据结构

BitSet 的内部数据结构基于一个 long 数组,每个 long 值包含64个二进制位。BitSet 通过位操作来管理和操作这些位,从而实现高效的空间和时间性能。

public class BitSet implements Cloneable, java.io.Serializable {
    private long[] words;
    private transient int wordsInUse;
    private transient boolean sizeIsSticky;
    
    // ... 其他方法和字段
}
核心字段
  • words:存储实际位数据的 long 数组。
  • wordsInUse:当前使用的 long 元素数量。
  • sizeIsSticky:跟踪 BitSet 的大小是否在序列化后保持不变。

1.2. 主要方法

设置位
public void set(int bitIndex) {
    if (bitIndex < 0)
        throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);

    int wordIndex = wordIndex(bitIndex);
    expandTo(wordIndex);

    words[wordIndex] |= (1L << bitIndex);
}

private static int wordIndex(int bitIndex) {
    return bitIndex >> 6; // 等效于 bitIndex / 64
}

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) {
        int request = Math.max(2 * words.length, wordsRequired);
        words = Arrays.copyOf(words, request);
    }
}

这个方法通过位操作将指定索引的位设置为1。

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

private void recalculateWordsInUse() {
    int i;
    for (i = wordsInUse - 1; i >= 0; i--)
        if (words[i] != 0)
            break;

    wordsInUse = i + 1;
}

这个方法通过位操作将指定索引的位清除(即设置为0)。

获取位
public boolean get(int bitIndex) {
    if (bitIndex < 0)
        throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);

    int wordIndex = wordIndex(bitIndex);
    return (wordIndex < wordsInUse) && ((words[wordIndex] & (1L << bitIndex)) != 0);
}

这个方法通过位操作获取指定索引的位的值。

并集(OR)
public void or(BitSet set) {
    if (this == set)
        return;

    int wordsInCommon = Math.min(wordsInUse, set.wordsInUse);

    if (wordsInUse < set.wordsInUse) {
        ensureCapacity(set.wordsInUse);
        wordsInUse = set.wordsInUse;
    }

    for (int i = 0; i < wordsInCommon; i++)
        words[i] |= set.words[i];

    if (wordsInUse > wordsInCommon)
        System.arraycopy(set.words, wordsInCommon,
                         words, wordsInCommon,
                         wordsInUse - wordsInCommon);

    recalculateWordsInUse();
}
交集(AND)
public void and(BitSet set) {
    if (this == set)
        return;

    int wordsInCommon = Math.min(wordsInUse, set.wordsInUse);

    for (int i = 0; i < wordsInCommon; i++)
        words[i] &= set.words[i];

    for (int i = wordsInCommon; i < wordsInUse; i++)
        words[i] = 0;

    recalculateWordsInUse();
}

1.3. 使用示例

下面是一些常见的使用示例,展示如何在实际开发中使用 BitSet。

创建和设置位
import java.util.BitSet;

public class BitSetExample {
    public static void main(String[] args) {
        BitSet bitSet = new BitSet();

        // 设置位
        bitSet.set(0);
        bitSet.set(1);
        bitSet.set(2);

        // 输出 BitSet
        System.out.println(bitSet); // {0, 1, 2}
    }
}
清除位
import java.util.BitSet;

public class BitSetExample {
    public static void main(String[] args) {
        BitSet bitSet = new BitSet();

        // 设置位
        bitSet.set(0);
        bitSet.set(1);
        bitSet.set(2);

        // 清除位
        bitSet.clear(1);

        // 输出 BitSet
        System.out.println(bitSet); // {0, 2}
    }
}
**获取位
import java.util.BitSet;

public class BitSetExample {
    public static void main(String[] args) {
        BitSet bitSet = new BitSet();

        // 设置位
        bitSet.set(0);
        bitSet.set(1);

        // 获取位
        System.out.println(bitSet.get(0)); // true
        System.out.println(bitSet.get(2)); // false
    }
}
集合操作
import java.util.BitSet;

public class BitSetExample {
    public static void main(String[] args) {
        BitSet bitSet1 = new BitSet();
        BitSet bitSet2 = new BitSet();

        // 设置位
        bitSet1.set(0);
        bitSet1.set(1);
        bitSet2.set(1);
        bitSet2.set(2);

        // 并集
        BitSet orSet = (BitSet) bitSet1.clone();
        orSet.or(bitSet2);
        System.out.println("OR: " + orSet); // {0, 1, 2}

        // 交集
        BitSet andSet = (BitSet) bitSet1.clone();
        andSet.and(bitSet2);
        System.out.println("AND: " + andSet); // {1}

        // 差集
        BitSet andNotSet = (BitSet) bitSet1.clone();
        andNotSet.andNot(bitSet2);
        System.out.println("AND NOT: " + andNotSet); // {0}
    }
}

java.util.BitSet 是一个功能强大且高效的位数组实现,适用于各种需要位操作和布尔标志的大规模应用场景。其内部基于 long[] 数组,并通过一系列位操作方法来实现集合操作和布尔运算。理解 BitSet 的实现原理和使用方法,有助于在实际开发中充分利用 BitSet 的优势,实现高效的数据查询和处理。

2. 应用场景概述

下面简单介绍BitSet的应用场景,位图和布隆过滤器后面单独说。推荐用BitSet的场景,在于当数据量特别大的时候,内存和性能要求高,用位运算有时更合适。

2.1. 布尔标志存储

BitSet 非常适合存储大量布尔标志。每个标志只需要一个位来表示,因此比使用 boolean[] 节省大量空间。

示例:

// 初始化一个BitSet用于存储布尔标志
BitSet flags = new BitSet();
flags.set(0); // 设置第0位为true
flags.set(1, false); // 设置第1位为false
flags.set(2); // 设置第2位为true

// 检查标志
System.out.println("Flag 0: " + flags.get(0)); // true
System.out.println("Flag 1: " + flags.get(1)); // false
System.out.println("Flag 2: " + flags.get(2)); // true

2.2. 集合操作

BitSet 可以高效地执行集合操作,如并集、交集和差集。它的位操作在处理这些集合操作时非常快速。

示例:

BitSet set1 = new BitSet();
BitSet set2 = new BitSet();

set1.set(0);
set1.set(1);
set2.set(1);
set2.set(2);

// 并集
BitSet union = (BitSet) set1.clone();
union.or(set2);
System.out.println("Union: " + union); // {0, 1, 2}

// 交集
BitSet intersection = (BitSet) set1.clone();
intersection.and(set2);
System.out.println("Intersection: " + intersection); // {1}

// 差集
BitSet difference = (BitSet) set1.clone();
difference.andNot(set2);
System.out.println("Difference: " + difference); // {0}

2.3. 图算法

在图论中,BitSet 可用于表示图中的节点集合,进行快速的邻接检查和子图操作。

示例:

import java.util.BitSet;

public class GraphExample {
    public static void main(String[] args) {
        int numNodes = 5;
        BitSet[] adjacencyMatrix = new BitSet[numNodes];
        
        // 初始化邻接矩阵
        for (int i = 0; i < numNodes; i++) {
            adjacencyMatrix[i] = new BitSet(numNodes);
        }
        
        // 图的边
        adjacencyMatrix[0].set(1);
        adjacencyMatrix[1].set(0);
        adjacencyMatrix[1].set(2);
        adjacencyMatrix[2].set(1);
        adjacencyMatrix[2].set(3);
        adjacencyMatrix[3].set(2);
        adjacencyMatrix[3].set(4);
        adjacencyMatrix[4].set(3);
        
        // 打印邻接矩阵
        for (int i = 0; i < numNodes; i++) {
            for (int j = 0; j < numNodes; j++) {
                System.out.print(adjacencyMatrix[i].get(j) ? "1 " : "0 ");
            }
            System.out.println();
        }
    }
}

3. 位图索引

位图索引(Bitmap Index)是数据存储和查询中的一种高效策略,尤其适用于高基数(high-cardinality)属性的场景。java.util.BitSet 是 Java 中实现位图索引的一个工具,它允许快速地表示和操作大量布尔值。

其实在StarRocks的文章中,就有专门说过位图索引的应用。应用场景的关键在于“高基数”属性。

它不仅常用于各种OLAP数据库,倘若我们的业务系统需要基于内存构建数据仓储,用于数据快速匹配,同样可以用 BitSet 构建查询索引。

场景描述

我们有一个用户表,每个用户有以下属性:

  • 性别(Gender,'M' 或 'F')
  • 年龄段(Age Group,如20-29,30-39,40-49等)
  • 兴趣爱好(Interest,如 'Sports', 'Music', 'Reading' 等)
    我们希望能够快速查询出特定属性组合的用户,例如:

查找所有性别为男且年龄在30-39岁之间并且喜欢音乐的用户。

代码实现

我们将使用 BitSet 来表示每个属性的集合,并通过位运算来实现复杂的查询。

import java.util.*;

public class BitSetUserIndex {
    private Map<String, BitSet> genderIndex = new HashMap<>();
    private Map<String, BitSet> ageGroupIndex = new HashMap<>();
    private Map<String, BitSet> interestIndex = new HashMap<>();
    private int userCount = 0;

    public int addUser(String gender, String ageGroup, Set<String> interests) {
        int userId = userCount++;
        addAttribute(genderIndex, gender, userId);
        addAttribute(ageGroupIndex, ageGroup, userId);
        for (String interest : interests) {
            addAttribute(interestIndex, interest, userId);
        }
        return userId;
    }

    private void addAttribute(Map<String, BitSet> index, String attribute, int userId) {
        BitSet bitSet = index.computeIfAbsent(attribute, k -> new BitSet());
        bitSet.set(userId);
    }

    public Set<Integer> queryUsers(String gender, String ageGroup, Set<String> interests) {
        BitSet result = (BitSet) genderIndex.getOrDefault(gender, new BitSet()).clone();
        result.and(ageGroupIndex.getOrDefault(ageGroup, new BitSet()));
        for (String interest : interests) {
            result.and(interestIndex.getOrDefault(interest, new BitSet()));
        }
        return bitSetToSet(result);
    }

    private Set<Integer> bitSetToSet(BitSet bitSet) {
        Set<Integer> set = new HashSet<>();
        for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) {
            set.add(i);
        }
        return set;
    }

    public static void main(String[] args) {
        BitSetUserIndex userIndex = new BitSetUserIndex();
        
        // 添加用户
        userIndex.addUser("M", "20-29", Set.of("Sports", "Music"));
        userIndex.addUser("F", "30-39", Set.of("Reading", "Music"));
        userIndex.addUser("M", "30-39", Set.of("Sports", "Reading"));
        userIndex.addUser("F", "20-29", Set.of("Music"));

        // 查询
        Set<Integer> users = userIndex.queryUsers("M", "30-39", Set.of("Sports"));
        System.out.println("Users matching criteria (M, 30-39, Sports): " + users);

        users = userIndex.queryUsers("F", "20-29", Set.of("Music"));
        System.out.println("Users matching criteria (F, 20-29, Music): " + users);

        users = userIndex.queryUsers("M", "30-39", Set.of("Music"));
        System.out.println("Users matching criteria (M, 30-39, Music): " + users);
    }
}
解释
  • 数据结构初始化:

    • genderIndex、ageGroupIndex 和 interestIndex 分别存储性别、年龄段和兴趣的位图索引。
    • userCount 用于生成唯一的用户ID。
  • 添加用户:

    • addUser 方法为每个用户生成唯一ID,并将其属性添加到对应的 BitSet 中。
    • addAttribute 方法用于添加单个属性到对应的 BitSet 中。
  • 查询用户:

    • queryUsers 方法接收查询条件(性别、年龄段、兴趣),通过位运算(AND)来组合多个 BitSet,实现复杂查询。
    • bitSetToSet 方法将 BitSet 转换为 Set<Integer>,以便返回用户ID集合。
输出示例
Users matching criteria (M, 30-39, Sports): [2]
Users matching criteria (F, 20-29, Music): [3]
Users matching criteria (M, 30-39, Music): []
总结

通过使用 BitSet,我们可以高效地管理和查询具有多个属性的用户数据。位图索引不仅在空间上节省,而且在处理复杂查询时非常高效。在实际应用中,这种方法可以用于广告定向、推荐系统、社交网络分析等多个领域。理解和应用位图索引技术,可以显著提升系统的查询性能和响应速度。

4. 布隆过滤器

布隆过滤器(Bloom Filter)是一种用于检测元素是否存在于集合中的概率性数据结构。布隆过滤器使用多个哈希函数和一个位数组(通常使用 BitSet 实现)来表示集合。它的优势在于极高的空间效率和查询效率,缺点是它允许一定概率的假阳性(即可能会误判某个元素存在于集合中,但实际上不存在),但不存在假阴性(即不会误判某个元素不存在于集合中,实际上存在)。

布隆过滤器的基本原理
  • 初始化:

    • 创建一个长度为 m 的位数组(通常使用 BitSet)。
    • 选择 k 个独立的哈希函数。
  • 添加元素:

    • 对每个要添加的元素,用 k 个哈希函数计算其哈希值,得到 k 个位置。
    • 将位数组中这 k 个位置设置为1。
  • 查询元素:

    • 对要查询的元素,用 k 个哈希函数计算其哈希值,得到 k 个位置。
    • 检查位数组中这 k 个位置是否均为1。如果是,则该元素可能存在于集合中;如果有任意一个位置为0,则该元素一定不在集合中。
代码示例

以下是一个简单的布隆过滤器实现,使用 BitSet 作为底层位数组:

import java.util.BitSet;
import java.util.Random;

public class BloomFilter {
    private BitSet bitSet;
    private int size;
    private int[] hashSeeds;

    public BloomFilter(int size, int hashCount) {
        this.size = size;
        this.bitSet = new BitSet(size);
        this.hashSeeds = new int[hashCount];
        Random random = new Random();
        for (int i = 0; i < hashCount; i++) {
            hashSeeds[i] = random.nextInt();
        }
    }

    public void add(String element) {
        for (int seed : hashSeeds) {
            int hash = hash(element, seed);
            bitSet.set(Math.abs(hash % size));
        }
    }

    public boolean mightContain(String element) {
        for (int seed : hashSeeds) {
            int hash = hash(element, seed);
            if (!bitSet.get(Math.abs(hash % size))) {
                return false;
            }
        }
        return true;
    }

    private int hash(String element, int seed) {
        int hash = 0;
        for (char c : element.toCharArray()) {
            hash = hash * seed + c;
        }
        return hash;
    }

    public static void main(String[] args) {
        BloomFilter bloomFilter = new BloomFilter(1024, 3);
        bloomFilter.add("hello");
        bloomFilter.add("world");

        System.out.println("Might contain 'hello': " + bloomFilter.mightContain("hello")); // true
        System.out.println("Might contain 'world': " + bloomFilter.mightContain("world")); // true
        System.out.println("Might contain 'java': " + bloomFilter.mightContain("java"));   // false (probably)
    }
}
解释
  • 初始化:

    • bitSet:用于存储位数组。
    • size:位数组的大小。
    • hashSeeds:存储 k 个不同的哈希种子,用于生成 k 个独立的哈希函数。
  • 添加元素:

    • add(String element) 方法接收一个待添加的元素。
    • 对该元素使用 k 个哈希函数计算其哈希值,并将对应的位数组位置设置为1。
  • 查询元素:

    • mightContain(String element) 方法接收一个待查询的元素。
    • 对该元素使用 k 个哈希函数计算其哈希值,检查对应的位数组位置是否均为1。如果是,则返回 true,表示该元素可能存在;否则返回 false,表示该元素一定不存在。
  • 哈希函数:

    • hash(String element, int seed) 方法实现一个简单的哈希函数,将字符串的每个字符与种子值组合进行哈希计算。
输出示例
Might contain 'hello': true
Might contain 'world': true
Might contain 'java': false
总结

通过使用 BitSet 实现布隆过滤器,可以在很高效地存储和查询大型数据集时节省空间。尽管布隆过滤器允许一定概率的假阳性,但它在许多应用场景中仍然非常有用,例如垃圾邮件过滤、网页爬虫中的URL去重、数据库查询加速等。在实际应用中,可以根据具体需求调整位数组的大小和哈希函数的数量,以达到最佳的性能和准确率。

  • 30
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值