首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Java Bitmap 去重:原理、代码实现与应用

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

原创
作者头像
Yeats_Liao
修改于 2025-09-02 16:39:09
修改于 2025-09-02 16:39:09
6300
举报

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 个数字:

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

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

代码语言:java
复制
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方法

代码语言:java
复制
public void add(int value) {
    int byteIndex = value / 8;
    int bitIndex = value % 8;
    // 使用位运算设置相应的位为 1
    bitmap[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方法

代码语言:java
复制
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 降到只需要几十字节。
  2. 内存对齐优化 CPU 访问对齐的内存更快。虽然 Java 的 sun.misc.Unsafe 不推荐使用,但在性能要求极高的场景下可以考虑。

4.2 算法操作优化

  1. 批量操作优化 如果要添加 100-200 这个范围的所有数字,逐个添加需要循环 101 次。批量操作可以直接设置对应的字节:
代码语言:java
复制
   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 个掩码,直接查表:
代码语言:java
复制
   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. 并发安全处理 多个线程同时修改同一个字节会出问题。加锁是最简单的解决方案:
代码语言:java
复制
   public synchronized boolean contains(int value) {
       //...
   }
   public synchronized void add(int value) {
       //...
   }

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

  1. 并行处理 处理 1 亿条数据时,可以分成 8 份给 8 个线程处理,最后把 8 个 Bitmap 用或运算合并起来。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 1. Bitmap 原理概述
  • 2. Bitmap 去重算法实现
    • 2.1 数据结构设计
    • 2.2 判断元素是否存在 - contains方法
    • 2.3 添加元素 - add方法
    • 2.4 测试代码 - main方法
  • 3. Bitmap 去重的应用场景
  • 4. 优化Bitmap去重算法性能的方法
    • 4.1 内存管理优化
    • 4.2 算法操作优化
    • 4.3 多线程优化
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档