当前位置: 首页 > news >正文

Java Bitmap 去重:原理、代码实现与应用

1. Bitmap 原理概述

Bitmap 是基于位的数据结构,每一位代表一个元素是否存在。

对于一个范围在 0 到 N - 1 的整数集合,如果使用普通的布尔数组来表示每个整数是否存在,需要占用 N 个字节的内存空间(假设布尔值占用 1 个字节)。

而使用 Bitmap,只需要 N / 8 个字节(因为 1 个字节有 8 位),大大节省了内存。

比如要存储 0 到 999999 的整数,普通布尔数组需要 1MB 内存,而 Bitmap 只需要 125KB,节省了 87.5% 的内存空间。

2. Bitmap 去重算法实现

2.1 数据结构设计

用字节数组来实现 Bitmap,每个字节包含 8 位,可以表示 8 个数字:

public class BitmapDeduplication {// 定义 Bitmap 的大小,这里假设处理的数据范围在 0 到 999999private static final int BITMAP_SIZE = 1000000;// 字节数组用于存储 Bitmap 数据private byte[] bitmap = new byte[BITMAP_SIZE / 8];

2.2 判断元素是否存在 - contains方法

public boolean contains(int value) {// 计算元素对应的字节索引int byteIndex = value / 8;// 计算元素在字节中的位索引int bitIndex = value % 8;// 通过位运算检查该位是否被设置return (bitmap[byteIndex] & (1 << bitIndex))!= 0;
}

这个方法的逻辑很简单:先算出数字在哪个字节(除以8),再算出在字节的哪一位(取余8),最后用位运算检查这一位是不是1。

用生活例子来理解:

把Bitmap想象成一排开关,每个开关代表一个数字:

  • 开关亮着(1)= 数字存在
  • 开关关着(0)= 数字不存在

具体步骤(以数字19为例):

  1. 找到开关位置

    • 19 ÷ 8 = 2 余 3
    • 意思是:在第2组开关的第3个位置
  2. 制作检查工具

    • 1 << 3 制作一个"探测器" 00001000
    • 这个探测器只能检查第3个位置
  3. 检查开关状态

   开关组状态:  01011010  (这是bitmap[2]的当前值,表示8个数字的存在状态)探测器:      00001000  (专门检查第3位的工具)检查结果:    00001000  (不为0,说明第3个开关是亮的,数字19存在)

解释:开关组状态就是内存中实际存储的字节值,每一位代表一个数字是否存在。

简单理解:就像用手电筒照特定位置,如果那个位置有光(1),手电筒就能照到;如果没光(0),就照不到。

2.3 添加元素 - add方法

public void add(int value) {int byteIndex = value / 8;int bitIndex = value % 8;// 使用位运算设置相应的位为 1bitmap[byteIndex] |= (1 << bitIndex);
}

添加元素就是把对应的位设置为1。先找到位置,然后用按位或运算把那一位变成1,其他位保持不变。

用生活例子来理解:

添加数字就是"打开开关":

  1. 找到开关位置

    • 数字19在第2组开关的第3个位置
  2. 制作开关工具

    • 1 << 3 制作一个"开关器" 00001000
    • 这个工具专门用来打开第3个位置的开关
  3. 打开开关

   原来状态:    01010010  (bitmap[2]的原始值,第3位是0,表示数字19不存在)开关工具:    00001000  (专门打开第3个位置的掩码)操作结果:    01011010  (第3个开关被打开了,数字19现在存在)

解释:原来状态是操作前bitmap中的实际数据,通过位运算修改后变成新的状态。

简单理解:就像按电灯开关,不管原来是开是关,按了之后肯定是开的。其他开关不受影响。

为什么这样设计?

  • 一个字节8位,可以表示8个数字的存在状态
  • 比用8个布尔变量节省7倍内存
  • 位运算速度极快,比逐个检查快很多

2.4 测试代码 - main方法

public static void main(String[] args) {BitmapDeduplication deduplicator = new BitmapDeduplication();int[] data = {1, 2, 3, 4, 2, 5, 1, 6};for (int num : data) {if (!deduplicator.contains(num)) {deduplicator.add(num);System.out.println("Unique element: " + num);}}
}

运行这段代码,输出结果是:1, 2, 3, 4, 5, 6。重复的数字被自动过滤掉了。

3. Bitmap 去重的应用场景

  1. 大规模数据处理:处理几千万条日志数据时,用 Bitmap 去重比传统 HashSet 快几倍,内存占用也少得多。
  2. 数据库优化:电商网站统计男女用户数量,用 Bitmap 索引比普通索引快10倍以上。
  3. 资源管理:操作系统用 Bitmap 管理磁盘块的使用状态,一个位代表一个磁盘块是否被占用。

4. 优化Bitmap去重算法性能的方法

4.1 内存管理优化

  1. 动态调整Bitmap大小
    如果数据范围是 100-200,没必要为 0-999999 分配内存。可以根据实际数据范围来分配:
   public BitmapDeduplication(int minValue, int maxValue) {BITMAP_SIZE = maxValue - minValue + 1;bitmap = new byte[BITMAP_SIZE / 8];}

这样内存使用量从固定的 125KB 降到只需要几十字节。

  1. 内存对齐优化
    CPU 访问对齐的内存更快。虽然 Java 的 sun.misc.Unsafe 不推荐使用,但在性能要求极高的场景下可以考虑。

4.2 算法操作优化

  1. 批量操作优化
    如果要添加 100-200 这个范围的所有数字,逐个添加需要循环 101 次。批量操作可以直接设置对应的字节:
   public void addRange(int start, int end) {int startByteIndex = start / 8;int endByteIndex = end / 8;int startBitIndex = start % 8;int endBitIndex = end % 8;if (startByteIndex == endByteIndex) {// 在同一个字节内byte mask = (byte) ((1 << (endBitIndex - startBitIndex + 1)) - 1 << startBitIndex);bitmap[startByteIndex] |= mask;} else {// 跨越多个字节byte startMask = (byte) ((1 << (8 - startBitIndex)) - 1 << startBitIndex);bitmap[startByteIndex] |= startMask;for (int i = startByteIndex + 1; i < endByteIndex; i++) {bitmap[i] = (byte) 0xff;}byte endMask = (byte) ((1 << (endBitIndex + 1)) - 1);bitmap[endByteIndex] |= endMask;}}

批量操作的性能提升非常明显,特别是处理连续数据时。

  1. 位运算优化
    每次都计算 1 << bitIndex 很浪费。预先算好 8 个掩码,直接查表:
   private static final byte[] BIT_MASKS = new byte[8];static {for (int i = 0; i < 8; i++) {BIT_MASKS[i] = (byte) (1 << i);}}public boolean contains(int value) {int byteIndex = value / 8;int bitIndex = value % 8;return (bitmap[byteIndex] & BIT_MASKS[bitIndex])!= 0;}

这个小优化在高频调用时能提升 10-20% 的性能。

4.3 多线程优化

  1. 并发安全处理
    多个线程同时修改同一个字节会出问题。加锁是最简单的解决方案:
   public synchronized boolean contains(int value) {//...}public synchronized void add(int value) {//...}

更高级的做法是用 AtomicIntegerArray,性能比 synchronized 好一些。

  1. 并行处理
    处理 1 亿条数据时,可以分成 8 份给 8 个线程处理,最后把 8 个 Bitmap 用或运算合并起来。
http://www.dtcms.com/a/363831.html

相关文章:

  • 广东省省考备考(第九十二天9.2)——言语(刷题巩固第一节课)
  • 从全栈开发到微服务架构:一次真实的Java全栈面试经历
  • 子进程、父进程
  • 高效数据传输的秘密武器:Protobuf
  • Linux系统:进程信号的处理
  • TKDE-2022《Low-Rank Linear Embedding for Robust Clustering》
  • 【机器学习深度学习】向量模型与重排序模型:RAG 的双引擎解析
  • 利用 Java 爬虫获取淘宝商品 SKU 详细信息实战指南
  • keycloak中对接oidc协议时设置prompt=login
  • 机器学习回顾——决策树详解
  • SOL中转转账教程
  • Android Binder 驱动 - Media 服务启动流程
  • TiDB v8.5.3 单机集群部署指南
  • rocketmq启动与测试
  • 数据结构--跳表(Skip List)
  • playwright+python UI自动化测试中实现图片颜色和像素对比
  • 便携式显示器怎么选?:6大关键指标全解析
  • 【三班网】初三大事件
  • ELK 统一日志分析系统部署与实践指南(上)
  • 【C++上岸】C++常见面试题目--数据结构篇(第十七期)
  • Oracle 数据库与操作系统兼容性指南
  • LeetCode 31. 下一个排列
  • 机器人抓取中的力学相关概念解释
  • Crawl4AI:为LLM而生的下一代网页爬虫框架
  • 【机器学习入门】5.2 回归的起源——从身高遗传到线性模型的百年演变
  • 学习笔记 | 如何将MaxKB应用对外发布为MCP服务?
  • 嵌入式学习 51单片机基础
  • 数控机床相邻轨迹最大过渡速度计算方法介绍
  • 25 万/秒写入 + 70% 硬件节省,TDengine 在首自信工业时序数据平台中的落地
  • 别再误会了!Redis 6.0 的多线程,和你想象的完全不一样