C++ 哈希思想应用:位图,布隆过滤器,哈希切分
一.位图
1.位图的概念
1.问题
给你40亿个不重复的无符号整数,没排过序.给一个无符号整数,如何快速判断一个数是否在这40亿个数中?
2.分析
1 Byte = 8 bit
1KB = 1024 Byte
1MB = 1024KB = 10241024 大约= 10的6次方Byte
1GB = 1024MB = 102410的6次方 大约= 10的9次方Byte = 10亿字节
因此4GB 约等于40亿字节
其实最快的方式就是记住1GB约等于10亿字节,这种题就好算了
我们知道40亿个整数,大概就是16GB
如果用排序+二分,
排序需要开16GB大的数组,就算用外排序(归并排序)排完序了,但是二分也需要数组啊…
如果用AVL树红黑树和哈希表
红黑树:三叉链结构+颜色 AVL树:三叉链结构+平衡因子 哈希表:负载因子每个节点的next指针等问题
内存当中更存不下
因此就需要用到位图了
3.位图的概念
4.演示
假设我们的位图使用一个char类型的数组实现的话
我们这个arr数组的最大值是22,因此只需要22个比特位即可
因此我们用一个char类型的数组,数组中有3个char即可
存放之前:
存放方式:
存放过程:
存放完毕后:
2.位图的操作
位图的三个核心操作:
set将x对应的比特位设置为1
将某一个比特位置为1,同时不影响其他比特位:
按位或一个数,这个数对应的那个比特位为1,其余比特位为0
void set(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] |= (1 << j);
}
reset将x对应的比特位设置为0
将某一个比特位置为0,同时不影响其他比特位:
按位与一个数,这个数对应的那个比特位为0,其余比特位为1
void reset(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
_bits[i] &= ~(1 << j);
}
test检查x在不在
跟一个数进行按位与
按位与一个数,这个数对应的那个比特位为1,其余比特位为0
如果结果
为0:说明不存在,
不为0说明存在
bool test(size_t x)
{
size_t i = x / 8;
size_t j = x % 8;
return _bits[i] & (1<<j);
}
3.位图的实现
1.char类型的数组
实现了set reset test之后
位图其实就已经实现完毕了
namespace wzs
{
// N是需要多少比特位
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)
{
size_t i = x / 8;
size_t j = x % 8;
_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;
};
}
2.int类型的数组
也可以采用int类型的数组来搞
此时就不是除8模8了
而是除32模32了
因为一个int类型有32个比特位
namespace wzs
{
// N是需要多少比特位
template<size_t N>
class bitset
{
public:
bitset()
{
//_bits.resize(N/32+1, 0);
_bits.resize((N>>5) + 1, 0);
}
void set(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
_bits[i] |= (1 << j);
}
void reset(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
_bits[i] &= ~(1 << j);
}
bool test(size_t x)
{
size_t i = x / 32;
size_t j = x % 32;
return _bits[i] & (1<<j);
}
private:
vector<int> _bits;
};
}
3.解决一开始的问题
因此对于一开始的那个问题:
位图开多大呢?
注意: 使用位图,且没有指定范围时,我们要按照该数据的范围大小来开位图
无符号整数的范围是0~4,294,967,295
因此我们需要开0~4,294,967,295大的范围
0~2的32次方-1的范围
一共有2的32次方个整数,
2的10次方是1024
2的30次方就是1024*1024*1024=1024*1024K=1024M=1G
因此2的32次方就是4G个整数
而我们使用一个比特位表示一个整数的,一个字节有8个比特位
因此我们只需要4G/8=0.5G个字节即可
因此我们的位图大小就是0.5G,正常情况下内存当中完全能存的下
无需担心
小小补充
而这个数字也不好记,其实它还有下面3种写法
- (size_t)-1 将-1强转为无符号整形
- UINT_MAX (unsigned_intMAX)
- 0xffffffff(16进制:8个f)
- pow(2,32)-1
一定注意:^在C++/C当中是异或,不是幂
因此(2^32)-1不等于那个数字
pow的返回值类型是double类型
验证
下面我们来验证一下位图能否完成这一任务
成功完成这一任务
4.位图的应用
1.给定100亿个整数,设计算法找到只出现一次的整数?
1.位图开多大?
我们先算一下100亿个整数要占多少G的内存?
一个比特位映射一个整数,一个字节有8个比特位
100亿个整数=100亿个比特位=100亿/8个字节=12.5亿字节=1.25G
我们真的要用1.25G的空间吗?
并不是!!!
而是刚才我们算的0.5G就足以
因为我们只存范围
如果要求必须使用位图来做,就算只有2个整数,不给我们范围,还是要用0.5G大小的位图
2.思路
找到只出现一次的整数,因为一个比特位只有0和1这两种状态,因此无法表示出现了1次以上的数字的状态
那么怎么办?
如果用2个比特位来表示一个整数的状态呢?
00就是出现0次
01就是出现1次
10就是出现2次
出现2次以后这个数我们就不再统计次数了
因此我们可以:
1.修改上面的位图,用2个比特位来表示一个整数
此时位图的大小就要乘以2,成为1G
2.用2个位图来做,每个位图依然是0.5G
只不过set,test函数要修改一下即可
下面我们就按照第2种来做吧,这个清晰易懂
3.代码
因为我们没有统计2次以上的次数,因此我们不允许进行reset操作
//利用组合来进行封装
template<size_t N>
class two_bitset
{
public:
void set(int x)
{
//00 -> 01
if (_bits1.test(x) == false && _bits2.test(x) == false)
{
_bits2.set(x);
}
//01 -> 10
else if (_bits1.test(x) == false && _bits2.test(x) == true)
{
_bits1