有一道非常经典的题目:给40亿个不重复的无序的无符号整数,再给一个无符号整数,如何快速判断这个数是否在这40亿个数中。
关于这道题目我们首先想到的解法就是排序二分查找,或者借助关联式容器进行求解,但是40亿个数据的数据量是非常巨大的(16G),没有办法放入内存中,所以在面对这种海量数据处理的时候,我们今天来学习位图和布隆过滤器的相关知识,用来解决海量数据处理的问题。
位图其实就是哈希结构的变形,同样通过映射来处理数据,采用直接定址法存储数据,只不过位图本身并不存储数据,而是通过存储一个比特位来标记这个数据是否存在,1代表存在,0代表不存在。
位图通常情况下用在海量数据的处理上,且数据不重复的情景下判断某个数据是否存在。
代码实现:
class bitmap{
public:
bitmap(size_t N){
_bits.resize(N / 32 + 1, 0); // 多开一个整型32bit
_num = 0;
}
// 标记
void set(size_t x){
// 寻找x的标记存放在第几个整型
size_t index = x / 32;
// 寻找x的标记在这个整型的第几个位
size_t pos = x % 32;
//左移是向高位移动
_bits[index] |= (1 << pos);
}
void reset(size_t x){
// 寻找x的标记存放在第几个整型
size_t index = x / 32;
// 寻找x的标记在这个整型的第几个位
size_t pos = x % 32;
// 第pos个位置置为0
_bits[index] &= ~(1 << pos);
}
// 判断x的映射位是否为1
bool test(size_t x){
// 寻找x的标记存放在第几个整型
size_t index = x / 32;
// 寻找x的标记在这个整型的第几个位
size_t pos = x % 32;
return _bits[index] & (1 << pos);
}
private:
std::vector<int> _bits;
size_t _num; // 存储的数据个数
};
位图的应用:
位图的优缺点分析:
布隆过滤器是由布隆提出的 一种概率型的数据结构,布隆过滤器实则就是位图与哈希表的结合,特点是高效地插入和查询,它是用多个哈希函数,将一个数据映射到位图结构中。这种方式不仅可以提升查询效率,还可以节省大量的内存空间。
代码实现:
template<class K = std::string, class Hash1 = HashStr1, class Hash2 = HashStr2, class Hash3 = HashStr3>
class bloomfilter{
public:
bloomfilter(size_t num){
_bs(-1);
}
void set(const K& key){
// 通过多个哈希函数将数据映射到位图中
size_t index1 = Hash1()(key);
size_t index2 = Hash2()(key);
size_t index3 = Hash3()(key);
_bs.set(index1);
_bs.set(index2);
_bs.set(index3);
}
// 存在误删的问题
void reset(const K& key){
// 不支持删除
}
bool test(const K& key){
// 判断在是不准确的,可能存在误判,判断不在是准确的。
size_t index1 = Hash1()(key);
if (_bs.test(index1) == false)
return false;
size_t index2 = Hash2()(key);
if (_bs.test(index2) == false)
return false;
size_t index3 = Hash3()(key);
if (_bs.test(index3) == false)
return false;
return true;
}
private:
// 底层其实是一个位图
bitmap _bs;
size_t len;
};
布隆过滤器的应用:
布隆过滤器的优缺点分析:
问题:给定100亿个整数,设计算法找到只出现一次的整数。
1G大概是10亿个字节,100亿个整数就是400亿个字节,400亿个字节40G。
方法一:
出现0次的,出现1次的,出现2次及以上的,这里存在3种数据状态,出现0次的标志为00,出现1次的标志为01,出现2次及以上的标志为10。
方法二:
设置两个位图,位图1和位图2的对应位都提供一个位来标记数据。再遍历两个位图找出所有对应位映射为01的整数。
问题:给两个文件分别有100亿个整数,我们只有1G内存,如何找到两个文件的交集。
方法一:
将其中一个文件中的数据放到一个位图当中,读取另一个文件中的整数,判断在不在位图当中,若在就是交集,不在就不是交集。
方法二:
将其中一个文件的整数映射到一个位图种,将另一个文件的整数映射到另一个位图种,再将两个位图中的对应位按位与,与之后位为1的位就是交集。
问题:给两个文件分别有100亿个query(请求),只有1G内存,找到两个文件的交集,分别给出精确算法和近似算法。
方法一:
将一个文件中的query映射到一个布隆过滤器中,读取另一个文件中的query,判断在不在布隆过滤器中,在就是交集,不在就不是交集。
缺陷:布隆过滤器判断数据在可能会存在误判,所以交集中有可能有些数据不准确。
方法二:
将大文件切分成小文件加载到内存中(平均切割),一般切出来的小文件的大小能放进内存就可以,但是这里要不断地进行比较(不是暴力比较,小文件中数据放入set当中,搜索效率还是要高出不少)。
用同一个哈希函数处理query,i = hashstr(query) % 份数,则两个文件中相同的query一定会进入编号相同的小文件,将一个大文件分成的小文件放进一个set中,读取另一个大文件切割成的小文件中对应的小文件中的query,在第一个小文件中就是交集,不在则不是交集。
问题:如何扩展布隆过滤器使得它支持删除元素的操作。
解法一:
每个位标记为计数器
那么到底用几个位来表示计数器呢?若位数太少可能会导致计数器溢出,但如果使用更多的位来映射一个位置,那么空间的消耗就会增大。
一般采用16位来标记计数器
问题:给一个超过100G大小的log file,log中存着IP地址,设计算法找到出现次数最多的IP地址。
统计次数一般用KV模型的map解决,但是100G大小放不进内存中。
IP有可能出现在不同的小文件当中,平均切分是不合理的。
读取IP用哈希算法计算编号,IP就会进入相同编号的小文件,运用map
问题:给一个超过100G大小的log file,log中存着IP地址,设计算法找到topK的IP地址,如何直接用Linux系统命令实现。
统计次数一般用KV模型的map解决,但是100G大小放不进内存中。
IP有可能出现在不同的小文件当中,平均切分是不合理的。
读取IP用哈希算法计算编号,IP就会进入相同编号的小文件,出现次数最多的IP,就建小堆进行操作。