位图(bitset)是一种特殊且高效的数据结构,用比特位0/1表示数据是否存在,常用于对查找速度和存储空间有较高要求的场景中,还可以配合宏定义使用,例如系统调用open的的第二个参数就是简单位图结构。
哈希常用与面试场景试题中,例如:
问题1:给出40亿个不重复的无符号整数,给出指定的无符号整数,如何快速判断该整数是否存在于给出40亿个整数中。
1G 约等于10亿多字节,一个整数4个字节,160亿字节,存储这些数据约等于16G,这些数据无法存储于内存中。暴力查找太慢,而二分查找+排序,该算法只能对内存中的数据进行查找,所以这就产生位图 。
位图的本质是直接定址法的体现,存储数据使用vector,而里面存储的0/1表示数据是否存在, 例如vector数组第32位置对应的比特位为1,就表示32这个数存在,为0则表示不存在。
无符号的最大整数:

假设上面40亿多个无符号整数都存在,即范围[0,4294967295] 一个int可以表示32个整数,看看存储上述这些数需要多少个int,4294967295 / 32 = 134217727.96875约等于1.35亿个int,需要多少空间存储看看,1.35亿 * 4 = 5.4亿字节,也就是说需要不超过1G内存就可以存储,内存肯定存的下。
下面看看工作原理:

使用传进来的整数,再调用构造函数时来进行开空间,例如当传入63时,需要两个int才可以包括该数,一个int表示范围是[0,32],不够表示63。 使用下面公式判断该数是否存在???
template<size_t N>
class bitset
{
public:
bitset()
{
_bs.resize(N / 32 + 1);
}
private:
std::vector<int> _bs;
};
个人感觉有点像哈希桶里面,桶就是下标,而取余的余数表示该数据在桶中的偏移量。 比如34在第二个桶,34%32 =2,在第二个桶中偏移量为2就是34该数,只不过使用0/1表示。
功能:就是将指定数据的比特位设置为1,表示此时该数据存在了,如何实现呢??? 先找出该数据在第一个下标,然后取出该下标中的数据按位异或上1左移偏移量的值的结果。 下面用图解释一下:
下面假设下标1中存储的数据为,我们将34对应的比特位设置为1。

代码如下:
// x映射的位标记成1
void set(size_t x)
{
size_t i = x / 32;//第几个位置,这个数存在于该数组下标之中
size_t j = x % 32;//简单点说就是偏移量
_bs[i] |= (1 << j);
}功能:就是将指定数据的比特位设置为0,表示此时该数据不存在了,如何实现呢??? 先找出该数据在第一个下标,然后取出该下标中的数据按位异与上1左移偏移量的值然后进行取反后的结果。 下面用图解释一下:
下面假设下标1中存储的数据为4,我们将34对应的比特位设置为0。

伪代码如下:
// x映射的位标记成0
void reset(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
_bs[i] &= (~(1 << j));
}功能:判断指定的数据是否存在,如何实现呢??? 先找出该数据在第一个下标,然后取出该下标中的数据按位异与上1左移偏移量的值然后进行取反后的结果。 下面用图解释一下:
下面假设下标1中存储的数据为4,我们需要判断下标为34的比特位是0或者1。

若结果为非0,则表示存在;0则表示不存在。伪代码如下:
// x映射的位是1返回真
// x映射的位是0返回假
bool test(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
return _bs[i] &= (1 << j);
}问题2:
给定100亿个整数,设计算法找到只出现⼀次的整数?
如何设计算法??? 规定:
思路:用两个位图存储,把位图1看做高位,位图2看作低位,第一次出现给位图1设置,第二次出现时给两个位图都设置。
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);
}
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};先获取指定数据两个位图对应的比特位,然后进行判断00即0次,01即1次,10即2次等等。 获取出现个数代码:
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;
}
else
{
return 3;
}
}问题3:
给两个⽂件,分别有100亿个整数,我们只有1G内存,如何找到两个⽂件交集?
用两个位图分别对两个文件数据对应比特位设置为1,然后同时进行判断即可。 问题4:
⼀个⽂件有100亿个整数,1G内存,设计算法找到出现次数不超过2次的所有整数
思路与上述相似,这⾥要统计出现次数不超过2次的,可以每个值⽤两个位标记即可,00代表出现0次,01代表出现1次,10代表出现2次,11代表出现2次以上。最后统计出所有01和10标记的值即可,然后直接调用统计次数(出现1或2次的)的接口即可。
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);
}
}
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;
}
else
{
return 3;
}
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
void test_bitset2()
{
twobitset<100> tbs;
int a[] = { 5,7,9,2,5,99,5,5,7,5,3,9,2,55,1,5,6,6,6,6,7,9 };
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 || tbs.get_count(i) == 2)
{
cout << i << endl;
}
}
}位图优点:
位图缺点:
应用场景:
本文详细先给出一个面试题引入位图可以解决该问题,然后介绍位图概念及应用场景,并模拟实现位图,通过简单的面试题可以加深对位图的理解及实践能力,最后得出位图是一个非常高效数据结构尤其是在查找和对空间有较高要求的场景下,而对于字符串无法进行处理,哈希的另一个应用可以解决该问题:布隆过滤器。详情文章移布置下一篇文章。