BitSet去重核心是“标存在”而非存数据,仅用1 bit标记非负整数是否出现,空间达理论下限;但必须同时满足:数据可映射为非负整数、值域可控、分布稠密,否则易浪费资源或崩溃。

BitSet 做海量数据去重,核心不是“存数据”,而是“标存在”——用 1 个 bit 表示 1 个非负整数是否出现过。它不存值本身,只记“有或没有”,所以空间直接压到理论下限:1 bit/元素。40 亿个可能取值(如 QQ 号),只需 512 MB 内存;1000 万个 ID,仅占约 1.25 MB。但这个优势只在条件匹配时成立,用错场景反而浪费资源。
适用前提:三个硬约束缺一不可
BitSet 不是通用集合替代品,必须同时满足:
- 数据类型可映射为非负整数:原始数据要是 int、long 范围内的非负整数;字符串需经一致性哈希 + 取模(注意冲突校验),浮点数需先离散化(如 ×10 取整)再偏移归零;负数一律加偏移量(如温度 −50~50 → 映射为 0~100)
- 值域范围可控:最大值 N 决定内存用量(≈ N / 8 字节)。N = 1 亿 → ~12.5 MB;N = 100 亿 → ~1.25 GB。超过 2³¹(约 21.47 亿)会抛 IndexOutOfBoundsException(Java)或越界未定义行为(C++)
- 分布相对稠密:若 1 亿范围内只出现几百个离散大整数(如 ID 是 10⁹、2×10⁹、3×10⁹),位图中几乎全是 0,空间严重浪费,此时 RoaringBitmap 或布隆过滤器更合适
安全使用:边界防护与操作顺序不能错
常见崩溃和逻辑错误多源于疏忽检查:
-
永远先 test() / get(),再 set():C++ 中
seen.test(x)比seen[x]安全,后者不检查越界;Java 中bitSet.get(x)对超限索引返回 false 而不报错,容易掩盖负数误传(如 -1 自动转成极大正数) -
输入必须过滤:循环中加
if (x = MAX_N) continue;,否则 C++std::vector或 JavaBitSet都可能静默失败或行为异常 -
避免重复计数陷阱:不要写
bitSet.set(x); count++,而应if (!bitSet.get(x)) { bitSet.set(x); count++; }
突破限制:分桶、协同与手动优化
现实数据常超出单 BitSet 承载能力,可用组合策略拓展边界:
- 高位分桶 + ConcurrentHashMap:把 0~10¹⁰ 拆成 1000 个桶(每桶覆盖 10⁷),用高位哈希定位桶,低位作桶内索引;桶按需加载,避免一次性分配巨量内存
- BitSet + 布隆过滤器两级筛选:先用极小内存的布隆过滤器快速拦截 95% 重复项;仅当布隆返回“可能存在”时,才查 BitSet 精确判定——降低 BitSet 访问频次,提升吞吐
-
C++ 手写 uint64_t[] 替代 std::bitset:运行时才知道规模?用
std::unique_ptr分配对齐内存,手动算字偏移与位掩码,比std::bitset(编译期定长)或std::vector(代理对象缺陷)更可控、更快
避坑提醒:什么情况坚决别用
以下场景强行套用 BitSet 会适得其反:
- 原始数据是 UUID、长文本、任意长度字符串——哈希后易碰撞,且值域爆炸,内存失控
- ID 高度稀疏(如只出现 100 个跨度在 0~Long.MAX_VALUE 的整数)——分配几 GB 内存却几乎全空
- 需要线程安全且高并发写入——
BitSet无内置同步,synchronized全局锁吞吐暴跌;应改用分段 BitSet + 细粒度锁,或ConcurrentHashMap - 需遍历所有去重结果——位图不存值,遍历本质是扫描 + 解码(逐位查 1),比 HashSet 迭代慢得多;如需输出全部唯一值,建议边处理边收集到
ArrayList










