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去重、数据库查询加速等。在实际应用中,可以根据具体需求调整位数组的大小和哈希函数的数量,以达到最佳的性能和准确率。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。