目录
位图的引入
面试题【腾讯】:给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中
先考虑内存的问题:40亿个整数, 每个整数4字节,换算大约16G的空间,寻常的查找算法都是不可能完成的。因此引入位图。
位图概念
所谓位图,就是用每一位比特位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的 ,位图实际上也是使用哈希思想,只不过是哈希的变形
通过这种同样是哈希的思想,就可以极大的节省空间,无符号整形范围在0~2的32次方-1,因此我们可以开2的32次方个比特位,换算下来只有512M的大小,通过直接映射就能够判断。
我们无法开这么大的数组,但我们采用的是bit位的标记,即值是几,就把第几个位标记成1。
那么如何去找呢?实际上我们把数组的元素类型规定为char(int也可以),这样就可以通过如下的方式去找任意一个数:x
-
x映射的值,在第几个char对象上:x/8
-
x映射的值,在这个char对象的第几个比特位:x%8
比特位顺序问题:
事实上,比特位的顺序与机器的大小端无关,仍是和地址一样从右到左:
1字节 = 8bit,代表的整数就是0~7,比特位为1则表示该数存在
位图代码
对于位图的功能,要有插入,删除,检测在不在三个功能,如果这样赋值:
- size_t i = x / 8;
- size_t j = x % 8;
插入就可以这样:_bits[i] |= (1 << j);
删除就可以这样:_bits[i] &= (~(1 << j));
测试这个值在不在就可以这样:return _bits[i] & (1 << j);
此外,一开始同样需要开辟指定大小的空间,因此构造函数中要写出来。
template<size_t N> // 非类型模板参数:用一个整形作为模板的一个参数,在模板中可以当常量使用
class bitset
{
public:
bitset()
{
_bits.resize(N / 8 + 1, 0);
}
void set(size_t x)//标记
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] |= (1 << j);
}
void reset(size_t x)//清除
{
_bits[i] &= (~(1 << j));
}
bool test(size_t x)//测试这个值在不在
{
size_t i = x / 8;
size_t j = x % 8;
return _bits[i] & (1 << j);
}
private:
vector<char> _bits;
};
下面是int类型的代码
class bitset {
public:
bitset(size_t N) {
_bits.resize(N / 32 + 1, 0);
_num = 0;
}
void set(size_t x) {
size_t index = x / 32; // 算出x映射的位在第几个整形
size_t pos = x % 32; // 算出x在这个整形中第几个位置(bit位)
_bits[index] |= (1 << pos); // 第pos个位置成1
// 左移、右移这里的左右不是方向
// 左移是向高位移
// 右移是向低位移
++_num;
}
void reset(size_t x) {
size_t index = x / 32; // 算出x映射的位在第几个整形
size_t pos = x % 32; // 算出x在这个整形中第几个位置(bit位)
_bits[index] &= ~(1 << pos); // 第pos个位置成0
--_num;
}
// 判断x在不在
bool test(size_t x) {
size_t index = x / 32; // 算出x映射的位在第几个整形
size_t pos = x % 32; // 算出x在这个整形中第几个位置(bit位)
return _bits[index] & (1 << pos);
}
private:
std::vector<int> _bits; // 一个int4个字节,一个字节8个bit位
size_t _num;
};
测试
void test_bitset() {
bitset bs(100);
bs.set(99);
bs.set(98);
bs.set(97);
bs.set(96);
for (size_t i = 0; i < 100; ++i) {
printf("[%d]:%d\n", i, bs.test(i));
}
cout << bs.test(96);
//bitset bs1(-1);//无符号最大数直接传-1,会转成最大数量
//bitset bs2(pow(2,32));
//bitset bs3(0xffffffff);
}
库中也有位图结构 bitset ,也可以直接使用库中的位图。
#include <bitset>
位图应用
一. 给定100亿个整数,设计算法找到只出现一次的整数?
对于此整数有三种状态:
- 出现0次
- 出现1次
- 出现一次以上
因此,我们可以通过两个比特位来标记:00代表出现0次;01代表出现1次,其他的代表出现一次以上。(两个标记位可以表示从1到3),此方式虽然可行,但需要第上述代码做出很大变动。那么我们可以采用另一种方式代表两个比特位,即以开两个位图的方式,每个位图的一个比特位组合成两个比特位进行标记
template<size_t N>
class solution {
public:
void set(size_t x) {
if (_bs1.test(x) == false && _bs2.test(x) == false) { // 00
_bs2.set(x); // 01
}
else if (_bs1.test(x) == false && _bs2.test(x) == true) { // 01
_bs1.set(x);
_bs2.reset(x); // 10
}
}
void PrintOnce()
{
for (size_t i = 0; i < N; ++i)
{
if (!_bs1.test(i) && _bs2.test(i))
{
cout << i << endl;
}
}
cout << endl;
}
private:
bitset<N> _bs1;
bitset<N> _bs2;
};
测试
void test_solution()
{
solution<100> tbs;
int a[] = { 3, 5, 6, 7, 8, 9 ,33, 55, 67, 3,3,3,5,9,33 };
for (auto e : a)
{
tbs.set(e);
}
tbs.PrintOnce();
}
二. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
方案1: 将其中一个文件1的整数映射到一个位图中,读取另外一个文件2中的整数,判断在在不在位图,在就是交集。消耗5OOM内存
方案2: 将文件1的整数映射到位图1中,将文件2的整数映射到位图2中,然后将两个位图中的数按位与。与之后为1的位就是交集。消耗内存1G。
三. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数
本题找的不超过2次的,也就是要找出现1次和2次的
本题还是用两个位表示一个数,分为出现0次00表示,出现1次的01表示,出现2次的10表示出现3次及3次以上的用11表示
总结一下位图的应用:
- 快速查找某个数据是否在一个集合中
- 排序 + 去重
- 求两个集合的交集、并集等
- 操作系统中磁盘块标记
位图有如下优点:1.节省空间。2.快。但相对的,位图同样有缺点存在:1. 一般要求范围相对集中,如果范围特别分散,空间消耗就会上升。2. 只能针对整形