C++---位图
一、基本原理
位图(Bitmap)是一种高效的数据结构,主要用于快速判断某个元素是否存在(通常是整数),其核心原理是通过二进制位(bit) 来表示数据的状态,具有空间效率极高的特点。
核心思想用一个二进制位(0 或 1)来标记一个整数的存在状态:
- 位值为
1
表示该整数存在 - 位值为
0
表示该整数不存在
- 位值为
空间映射假设要表示范围为
0~N
的整数集合,位图需要的空间仅为N/8
字节(或N/32
个 32 位整数)。例如:- 表示
0~1023
的整数,只需1024 bits = 128 bytes
(仅 128 字节)。 - 映射关系:整数
x
对应二进制位的位置为x
,通过x / 32
定位到数组下标,x % 32
定位到具体位。
- 表示
操作逻辑通过位运算实现高效的增删查:
- 设置存在(set):
_bs[i] |= (1 << j)
(将第j
位设为 1) - 判断存在(test):
return _bs[i] & (1 << j)
(非零则存在) - 清除存在(reset):
_bs[i] &= ~(1 << j)
(将第j
位设为 0)
- 设置存在(set):
二、相关接口
1.set
功能:将指定整数 x
对应的二进制位标记为 1
(表示 x
存在)。
实现:1 << j
表示将数字 1 向左移动 j 位,得到一个只有第 j 位为 1、其他位都为 0 的二进制数
- 只会将
_bs[i]
中第 j 位设置为 1(如果原本是 0 的话) - 其他位的值会保持不变
void set(size_t x)
{size_t i = x / 32;size_t j = x % 32;_bs[i] |= (1 << j);
}
2.reset
功能:将指定整数 x
对应的二进制位标记为 0
(表示 x
不存在)。
实现:1 << j
生成一个只有第 j
位为 1
、其他位为 0
的二进制数
·~(1 << j)
对上面的结果进行按位取反,得到一个只有第 j
位为 0
、其他位全为 1
的二进制数
·_bs[i] & (~(1 << j))
进行按位与运算,再通过 &=
赋值给 _bs[i]
。
void reset(size_t x)
{size_t i = x / 32;size_t j = x % 32;_bs[i] &= (~(1 << j));
}
3.test
功能:将指定整数 x
对应的二进制位标记为 0
(表示 x
不存在)。
实现:通过 _bs[i] & (1 << j)
运算,若结果非零则返回 true
,否则返回 false
。
bool test(size_t x)
{size_t i = x / 32;size_t j = x % 32;return _bs[i] & (1 << j);
}
4.clear
功能:将位图中所有位重置为 0
,即清空所有元素。
实现:遍历底层数组,将每个元素赋值为 0
。
void clear()
{for (int i = 0; i < _bs.size(); i++){_bs[i] = 0;}
}
5.count
功能:计算位图中值为 1
的位的总数(即存在的元素总数)。
实现:遍历数组,对每个元素统计二进制中 1
的个数(可借助快速位计数算法,如 Brian Kernighan 算法)。
6.isEmpty
功能:判断位图中是否没有任何元素(所有位均为 0
)。
实现:遍历数组,若所有元素均为 0
则返回 true
。
三、优缺点
1.优点
- 空间效率极高(比哈希表节省成百上千倍空间)
- 操作速度快(基于位运算,CPU 支持高效处理)
2.缺点
- 仅适用于整数类型
- 不适用于范围极大的场景(如
0~10^18
会占用过多空间
四、例题
给定100亿个整数,设计算法找到只出现一次的整数
一个文件有100亿个整数,1G内存,设计算法找到出现次数不超过两次的所有整数
思路:
1.状态编码设计
用两个位图 _bs1
和 _bs2
中对应位置的两个二进制位,共同表示一个整数的出现次数:
_bs1
的位为高位,_bs2
的位为低位,组合成 2 位状态:00
(_bs1=0
且_bs2=0
):该整数出现 0 次01
(_bs1=0
且_bs2=1
):该整数出现 1 次10
(_bs1=1
且_bs2=0
):该整数出现 2 次11
(_bs1=1
且_bs2=1
):该整数出现 3 次及以上(不再区分具体次数)
2.技术更新逻辑
每次遇到一个整数 x
时,根据当前状态更新为下一个状态:
- 从
00
(未出现)→01
(出现 1 次):仅设置_bs2
的位为 1 - 从
01
(出现 1 次)→10
(出现 2 次):设置_bs1
的位为 1,同时清除_bs2
的位 - 从
10
(出现 2 次)→11
(出现 3 次及以上):设置_bs2
的位为 1(此时_bs1
已为 1) - 达到
11
后不再变化(无论再出现多少次都保持该状态)
3.结果查询
通过读取两个位图对应位置的状态,返回该整数的出现次数类型:
- 返回 1 时,即为 “只出现一次的整数”,正是问题需要的结果
五、代码展示
#pragma once
#include<vector>
namespace Z
{template<size_t N>class bitset{public:bitset(){_bs.resize(N / 32 + 1);}//x映射的位标记成1void set(size_t x){size_t i = x / 32;size_t j = x % 32;_bs[i] |= (1 << j);}//x映射的位标记成0void reset(size_t x){size_t i = x / 32;size_t j = x % 32;_bs[i] &= (~(1 << j));}//判断是一还是零bool test(size_t x){size_t i = x / 32;size_t j = x % 32;return _bs[i] & (1 << j);}void clear(){for (int i = 0; i < _bs.size(); i++){_bs[i] = 0;}}private:std::vector<int> _bs;};template<size_t N>class twobitset{public:void set(size_t x){bool bit1 = _bs1.test(x);bool bit2 = _bs2.test(x);if (!bit1 && !bit2)// 00->01{_bs2.set(x);}else if (!bit1 && bit2)// 01->10{_bs1.set(x);_bs2.reset(x);}else if (bit1 && !bit2)// 10->11{_bs2.set(x);}}//返回0 出现0次//返回1 出现1次//返回2 出现2次//返回3 出现3次及以上int get_count(size_t x){bool bit1 = _bs1.test(x);bool bit2 = _bs2.test(x);if (!bit1 && !bit2){return 0;}else if (!bit1 && bit2){return 1;}else if (bit1 && !bit2){return 2;}elsereturn 3;}private:bitset<N> _bs1;bitset<N> _bs2;};
}void test_twobitset()
{Z::twobitset<100> tbs;int a[] = { 12,5,28,18,7,12,23,0,9,28,15,30,7,19,0,25,18,6,23,11 };for (auto e : a){tbs.set(e);}for (size_t i = 0; i < 100; i++){//cout << i << "->" << tbs.get_count(i) << endl;if (tbs.get_count(i) == 1)cout << i <<endl;}
}