位图去重需满足数据可映射、范围已知、无负数/越界三条件,否则vector或uint64_t[]会静默出错;vector返回代理对象,不支持取地址、引用传递及集合操作,不适合通用去重模块。

位图去重不是“换个容器就能省内存”,而是必须满足数据可映射、范围已知、无负数/越界这三点,否则 std::vector<bool></bool> 或手写 uint64_t[] 都会静默出错或崩溃。
为什么 std::vector 不适合直接用于去重逻辑
std::vector<bool></bool> 是特化容器,返回的是代理对象(proxy),不是真实 bool&。这意味着:
- 不能取地址:
&bitmap[i]编译失败 - 不能传给需要
bool&的算法(比如某些自定义计数器或原子操作封装) - 跨函数传递引用时,临时代理可能被析构,后续读值为未定义行为
- 不提供
find、count等集合语义接口,你得自己遍历+判断
它适合做“一次性标记+简单查询”,但不适合封装成通用去重模块。真要省空间又保安全,别图省事——自己管存储。
手写 uint64_t[] 位图的正确初始化与索引计算
用 uint64_t* 手动管理位图,核心是两点:对齐分配 + 精确位偏移。常见错误是直接 new uint64_t[n],没考虑 SIMD 指令或缓存行对齐要求。
- 用
aligned_alloc(64, size)或posix_memalign分配 64 字节对齐内存,避免 AVX 指令异常 - 字索引 =
index / 64,位偏移 =index % 64(不是% 32,除非你用uint32_t) - 置位操作必须用无符号左移:
data[word] |= (1ULL ;用 <code>1 在 <code>bit >= 32时 UB - 越界检查不能只靠
index ,还要确保 <code>word ,尤其在多线程下
示例初始化:
size_t num_bits = 10'000'000; size_t num_words = (num_bits + 63) / 64; uint64_t* bits = static_cast<uint64_t>(aligned_alloc(64, num_words * sizeof(uint64_t))); memset(bits, 0, num_words * sizeof(uint64_t)); </uint64_t>
去重时如何安全映射原始数据到位图索引
映射不是数学题,是约束检查。假设你要处理用户 ID,常见陷阱:
- ID 为负数?必须提前过滤:
if (id - ID 超过预设上限?比如最大允许
10^7,但输入含10000001,直接算索引会越界 - ID 稀疏分布?比如只出现 [0, 100) 和 [9999900, 10000000) 两段,中间全空——位图仍要分配 1000 万 bit,但这是设计取舍,不是 bug
- 别用哈希后取模:
hash(id) % size会引入冲突,位图必须是**确定性、无冲突**映射
正确做法只有两种:
- 值即索引(最常用):确认所有
id∈ [0, N),直接用id当索引 - 归一化偏移:如 ID 全在 [1000, 10000000),则索引 =
id - 1000,容量 =10000000 - 1000 + 1
遍历已去重结果的高效方式
位图不存值,只存状态。想拿到所有“出现过的数”,不能写 for (int i = 0; i ——这会扫完整个稀疏范围,O(N) 时间完全浪费。
- 如果活跃数据少(比如十亿位图里只有一百万 1),优先用
_tzcnt_u64(x86)或__builtin_ctzll(GCC/Clang)逐字扫描,跳过全零字 - 每个非零
uint64_t用位扫描指令提取 set bit 位置,再加回字偏移:pos = word_idx * 64 + __builtin_ctzll(word) - 避免在循环里反复调用
test(i),那会重复算除法和取模——先定位到字,再在字内查位 - 若需排序输出,位图天然有序,扫一遍就是升序;无需额外排序
真正难的从来不是怎么写 set(),而是怎么让业务数据乖乖落在那个“能用位图”的范围内。越界检查、负数过滤、范围预估,这些前置动作漏掉一个,后面全白干。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











