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

C++位图学习笔记

位图

在处理海量数据(如 40 亿个整数)时,传统的哈希表、set容器等等会消耗大量内存,这显然是不划算的。如果能用40亿个比特位来表示从0到40亿是否存在(0不存在,1存在),就能节省大量空间,这就涉及到了位图

所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常用于判断某个数据是否存在。

位图原理

我们通常使用一个int数组来实现位图。一个int有 32 个比特位,所以1个int可以表示32个数字的状态。于是可按下面方式进行映射。

  • 第 0 个 int (bits[0]):负责管理数字0 ~ 31
  • 第 1 个 int (bits[1]):负责管理数字32 ~ 63
  • 第 N 个 int (bits[N]):负责管理数字 **N*32 ~ (N+1)*32 - 1**
关键算法:定位与操作

按上述方式,若要在位图中增删数字x,还得解决两个问题:

  1. 它在数组的哪个下标?(确定是第几个int
  2. 它在 int 的第几个比特位?(确定是第几位)

这就用到了简单的数学运算:

数组下标i = x / 32比特位j = x % 32,比如78应被下标2上的数字表示,位于第12个比特位。

确定第几位后,实际中还需要通过位移和位运算实现更改,下面演示在位图中插入3

  1. 定位
    • 数组下标:3 / 32 = 0,所以操作bits[0]
    • 比特位置:3 % 32 = 3,所以操作第 3 位。
  2. 操作
    • 将数字1左移 3 位:0000...00001000
    • bits[0]进行按位或(|)运算。
  3. 结果:位图数组下标0位置处变成了8,用于表示0-31中只有3存在。

操作流程简略图:

插入函数的代码实现

void set(size_t input) { //确认在数组的的哪个位置 size_t pos = input / 32;// //确认在第几个比特位,同时进行或等运算 _table[pos] |= (1 << (input % 32)); }

如果要在位图中删除某数字,定位方法和插入一致,但1左移后要取反,再用取反的结果和原数据做按位与运算,从而保证其它数字的状态不被改变

删除代码的函数实现

void reset(size_t input) { size_t pos = input / 32; _table[pos] &= ~(1 << (input % 32)); }

如果要查询位图中某个数字是否存在,定位方法不变,根据按位与运算得到的结果是否为0判断

检查是否存在的函数实现

bool test(size_t input) { size_t pos = input / 32; size_t temp = _table[pos]; if (temp & (1 << (input % 32))) {//如果不为0说明存在 return true; } return false; }

位图应用:

找出整形数组中仅出现一次的数:

用两个位图A,B表示数据存在状况,遍历数组并做以下操作:

  1. 数据第一次在数组中被找到时,在位图A中插入该数字,位图B不做变动
  2. 第二次出现时,将位图A中删除该数字,在位图B中插入该数字
  3. 第三次及以后出现不做任何改动

最后在位图A中存在的数字表明只出现过一次,下面给出解决方案

//找仅出现一次的数 template< size_t N> class twoBitmap { public: twoBitmap(const vector<size_t>& input) { _input = input; } void set(size_t input){ //判断数据在两个位图中的标记状态,从而进行状态更改 if (!_bm1.test(input) && !_bm2.test(input)) { _bm1.set(input); } else if (_bm1.test(input) && !_bm2.test(input)) { _bm1.reset(input); _bm2.set(input); } else { return; } } //所有答案要输出到一个数组里面 void test() { for (auto i : _input) { if (_bm1.test(i) && !_bm2.test(i)) { cout << i << endl; } } } private: vector<size_t>_input; Bitmap<N> _bm1; Bitmap<N> _bm2; };

寻找两个数组的交集:

将两个数组分别映射到两个位图上,然后遍历这两个位图,如果都为1说明在这两个数组中都存在

布隆过滤器

位图针对整数操作,而布隆过滤器则是以位图为底层的,可将多种类型存入到位图中的一种结构

实现原理:

布隆过滤器需要用户指定哈希函数,通过哈希函数将数据转换成关键码,再将关键码存入位图中即可。

如何能避免两个不同数据关键码相同时引发的误判?(如下图所示)

解决方法,使用三个哈希函数把同一个数据转换成三个关键码,再将关键码在位图中分别标记,在判断是否存在时,如果检测到位图这三个关键码下标存放的都是1,说明该数据存在。下图演示一个非整形如何存放到布隆过滤器中。

注意,这种方法不能完美解决重复的问题,但大幅降低重复的概率(三个哈希函数对不同数据计算出相同的三个关键码概率低)通过增加哈希函数的数量可进一步减小误判的概率。

此外,使用布隆过滤器验证数据的存在,有可能引发误判,但验证数据不存在是完全没有问题的

布隆过滤器的实现:

以string类型为例,给出三个针对string的哈希函数

1. DJB2 Hash //公式 hash = hash * 33 + struct DJB2Hash { size_t operator()(const std::string& str) const { unsigned long hash = 5381; // 初始种子值 for (char c : str) { // hash * 33 可以写成 (hash << 5) + hash,位运算更快 hash = ((hash << 5) + hash) + c; } return hash; } }; // 2. SDBM Hash // 公式 hash = hash * 65599 + c (65599 是一个质数) struct SDBMHash { size_t operator()(const std::string& str) const { unsigned long hash = 0; for (char c : str) { hash = c + (hash << 6) + (hash << 16) - hash; // 等价于 hash = c + hash * 65599 } return hash; } }; // 3. BKDR Hash // 公式 hash = hash * 131 + c struct BKDRHash { size_t operator()(const std::string& str) const { unsigned long long hash = 0; const int seed = 131; for (char c : str) { hash = hash * seed + c; } return (size_t)(hash & 0x7FFFFFFF); // 确保返回非负数(视具体需求而定) } };

下面给出针对string类型的布隆过滤器的不完全实现:

注:布隆过滤器有固定大小,而关键码计算结果可能远大于这个大小,所以每次得到的关键码需要模大小

template<size_t N,class T =string,class hash1=DJB2Hash,class hash2=SDBMHash,class hash3=BKDRHash> class Bloomfilter { public: //思路 //在位图中分别标记相应的关键码 void set(const T& input) { _bloomfilter.set(hash1()(input) % N) ; _bloomfilter.set(hash2()(input) % N) ; _bloomfilter.set(hash3()(input) % N) ; } //在位图中分别删除相应的关键码,实际上这样行不通,见下文 void reset(const T& input) { _bloomfilter.reset(hash1()(input) % N) ; _bloomfilter.reset(hash2()(input) % N) ; _bloomfilter.reset(hash3()(input) % N) ; } //若要检测某个值存在与否,在位图中分别检测相应的关键码,全在则该值存在 bool test(const T& input) { if(_bloomfilter.test(hash1()(input) % N) && _bloomfilter.test(hash2()(input) % N) && _bloomfilter.test(hash3()(input) % N) ) { return true; } return false; } private: Bitmap<N> _bloomfilter; };

注意,上面代码的删除其实并不正确:比如a和b都映射到了位图上同一个位置,删除a的时候导致b的映射也被删除了!真正的删除需要用计数器记录位图每个位置被标记了几次,但计数本身需要占用内存,又和位图减少内存消耗的初衷相悖。这里不做代码演示。

关于布隆过滤器的容量设置

已知布隆过滤器一个数据要在位图上标注三个位置,容量不大的情况下,误判概率高,如下图所示

于是有人研究了位图容量和数据量的大小指定多少较优,一般来说,大小是元素个数的五倍左右误判的概率大幅降低。

http://www.jsqmd.com/news/707047/

相关文章:

  • 【大白话说Java面试题】【Java基础篇】第8题:HashMap在计算元素下标时,为什么要进行二次hash
  • 线性表小回顾
  • Linux 0.11源码深度解析:kernel/chr_drv/tty_io.c —— 终端I/O的控制中枢与行规约引擎
  • Python新手在PyCharm写if总报错?5个坑90%人踩过,看完修复
  • C语言函数全解析
  • AI自主监测宠物健康,陪狗都不用自己来了!涂鸦Hey Tuya打造全屋智能“超级入口”
  • 快速上手:使用Clawdbot将星图平台Qwen3-VL接入飞书,实现智能问答
  • 【Linux从入门到精通】第17篇:日志系统——系统运行的黑匣子
  • 深度解析YOLOv11多光谱目标检测的技术实现与性能优化
  • 第78篇:AI辅助创意与设计工作流——Logo、海报、UI的自动化生成与迭代(操作教程)
  • 万物识别中文镜像部署教程:环境配置与推理测试
  • Python Web框架实战:Flask与Dash构建数据应用
  • OpenClaw本地部署接入飞书机器人并安装Skills(图文并茂超详细)
  • Excel高效使用技巧(一):告别低效!10个必会快捷键与基础操作优化
  • LFM2-VL-1.6B软件测试新范式:自动化生成测试用例与报告
  • AI智能体网页抓取能力实测:六大平台对比与边界测试
  • ACM周报5
  • 词袋模型原理与实践:从文本向量化到工程优化
  • Python网络爬虫实战:从数据采集到反反爬策略
  • 医疗AI安全评估框架:原理、实现与最佳实践
  • 【Linux从入门到精通】第18篇:网络配置基础——IP地址、网关与DNS
  • QML组件之间的通信方案(暴露子组件)
  • 2026山东大学项目实训4月26日
  • 【Applicom】applicom PC Network Interfaces - Version 下载分享
  • Dream-Creator:本地化AI绘画工具的设计、部署与实战指南
  • 【Linux从入门到精通】第19篇:SSH远程管理进阶——不只是输入密码
  • 基于本地LLM的智能桌面宠物开发指南:从架构设计到实践部署
  • 进制只是“数数的规则”,就像我们日常用十进制(逢10进1),计算机底层用二进制(逢2进1)
  • 计算机组成原理教学辅助:用LM Z-Image模拟CPU指令执行
  • 【AI】MCP和SKILLS区别