思考问题:
给 40亿个 不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。【腾讯面试题】
像这类题基础的思路应该排序+查找,但是这里题目要求有40亿个不同的整数,仅存储数据就要占用16G的内存,内存消耗过大,所以这里常规方法肯定行不通。
采用位图解决:
数据是否在给定的整形数据集合中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。如图:
位图
所谓位图,就是用每一个比特位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
数据在位图中的存储位置应该怎么找:
我们可以将位图看作一个个连续的整数数组,每个数组有32位,每一位代表一个数字是否存在:
相当于用一个比特位来表示一个整数是否存在,内存占用减少了32倍,40亿个整数完整存放需要占用16G的内存空间,而位图仅占用512M左右的内存空间。
比如我们现在要在位图中表示15这个数据是否存在:
- 先用 15/32=0 求出15应该存储在位图的第0个整数数组;-----数组位置
- 再用 15%32=15 求出应该存储在第15个bit位;-----bit位置
- 将该位置 置1表示15存在。
所以位图应该开多大的空间与它数据的大小范围有关系,与数据的个数没有关系。
位图的特点
使用场景:存放海量不重复数据的简单信息,但不需要存放数据本身
优点:节省空间,查找效率高:O(1)
位图的实现
底层封装一个整数数组,数组中每个bit位用1或0表示一个整数是否存在,初始化应该传入数据的范围
封装三个接口:实现在位图中插入,删除以及查找数据。
class BitSet {
private:
//整数数组
vector<int> _bit;
public:
//位图的大小和数据范围有关,与数据个数无关
BitSet(size_t rang) :_bit(rang / 32 + 1)
{}
//存储信息
//查找信息
//删除信息
};
插入
使用位置计算方法(先除以32,再对32取模)找到整数对应的bit位置,将其置1
//存储信息
void Set(size_t num)
{
//先计算位置: /32 %32
int idx = num / 32; //整数位置
int bitIdx = num % 32; //bit位置
//把对应bit位置1,按位或操作
_bit[idx] |= 1 << bitIdx;
}
查找
先算位置,再看值
//查找信息
bool find(size_t num)
{
//先算位置
int idx = num / 32;
int bitIdx = num % 32;
//再看值
return _bit[idx] >> bitIdx & 1;
删除
计算该整数的bit位置,将值置为0
//删除信息
void Eraser(size_t num)
{
//先算位置
int idx = num / 32;
int bitIdx = num % 32;
//置为0
_bit[idx] &= (~(1 << bitIdx));
}
位图完整代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
using namespace std;
class BitSet {
private:
//整数数组
vector<int> _bit;
public:
//位图的大小和数据范围有关,与数据个数无关
BitSet(size_t rang) :_bit(rang / 32 + 1)
{}
//存储信息
void Set(size_t num)
{
//先计算位置: /32 %32
int idx = num / 32; //整数位置
int bitIdx = num % 32; //bit位置
//把对应bit位置1,按位或操作
_bit[idx] |= 1 << bitIdx;
}
//查找信息
bool find(size_t num)
{
//先算位置
int idx = num / 32;
int bitIdx = num % 32;
//再看值
return _bit[idx] >> bitIdx & 1;
}
//删除信息
void Reset(size_t num)
{
//先算位置
int idx = num / 32;
int bitIdx = num % 32;
//置为0: 将1左移后取反,按位与操作
_bit[idx] &= (~(1 << bitIdx));
}
};
void test()
{
BitSet bit(512);
bit.Set(1);
bit.Set(512);
bit.Set(2);
bit.Set(64);
bit.Set(15);
cout << "1是否存在:" << bit.find(1) << endl;
cout << "2是否存在:" << bit.find(2) << endl;
cout << "3是否存在:" << bit.find(3) << endl;
cout << "512是否存在:" << bit.find(512) << endl;
cout << "------------" << endl;
bit.Reset(512);
cout << "512是否存在:" << bit.find(512) << endl;
}
int main()
{
test();
return 0;
}
运行结果:
布隆过滤器
布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。
例如:S1、S2通过三个哈希函数计算得到如下位置:
虽然S1对应的哈希位置是1,4,8;但是1,4,8位置存在并不能保证S1一定存在,因为这些哈希位置也可能由其他的一个或多个数据得到。
假如我们还有一个对象S3的三个哈希位置对应是 2,3,7
那么只要2,3,7中有一个位为0就可以表示S2不存在。
那么我们如何确定应该使用多少个哈希函数来构建布隆过滤器:
有一个公式:k=m/n* ln2
k:哈希函数个数;
m:需要的bit位个数
n:元素个数
将上面的式子进行转换,也可以先确定哈希函数个数,再确定需要的bit位个数。
布隆过滤器的特点
应用场景:存放各种数据的简单信息
概率型容器:可以判断数据是否一定不存在或可能存在
一般不能删除:可能会存在误删
时间复杂度:O(k) k是哈希函数的个数
实现
- 底层封装一个位图用来执行对bit位的操作
- 插入:用哈希函数计算出对应的k个位置,然后置1
- 查找:查找k个对应哈希位置的值,如果都位1,表示可能存在;只要有一个不为1就表示绝对不存在
- 注意:布隆过滤器不提供删除接口,因为有可能造成误删,有可能将其他数据对应的哈希位置清零
代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
using namespace std;
//位图
class BitSet {
private:
//整数数组
vector<int> _bit;
public:
//位图的大小和数据范围有关,与数据个数无关
BitSet(size_t rang) :_bit(rang / 32 + 1)
{}
//存储信息
void Set(size_t num)
{
//先计算位置: /32 %32
int idx = num / 32; //整数位置
int bitIdx = num % 32; //bit位置
//把对应bit位置1,按位或操作
_bit[idx] |= 1 << bitIdx;
}
//查找信息
bool find(size_t num)
{
//先算位置
int idx = num / 32;
int bitIdx = num % 32;
//再看值
return _bit[idx] >> bitIdx & 1;
}
//删除信息
void Reset(size_t num)
{
//先算位置
int idx = num / 32;
int bitIdx = num % 32;
//置为0: 将1左移后取反,按位与操作
_bit[idx] &= (~(1 << bitIdx));
}
};
template <class T,class Hash1, class Hash2, class Hash3>
class BloomFilter {
private:
//底层封装位图
BitSet _bit;
size_t _bitCount; //记录bit位的个数
public:
BloomFilter(size_t num)
:_bit(5 * num)
,_bitCount(5 * num)
{}
//存储信息:使用多个bit位
void set(const T& val)
{
Hash1 h1;
Hash2 h2;
Hash3 h3;
int idx1 = h1(val) % _bitCount;
int idx2 = h2(val) % _bitCount;
int idx3 = h3(val) % _bitCount;
//用位图中封装的接口将三个哈希位置的值置1
_bit.Set(idx1);
_bit.Set(idx2);
_bit.Set(idx3);
}
//查找
bool find(const T&val)
{
Hash1 h1;
Hash2 h2;
Hash3 h3;
int idx1 = h1(val) % _bitCount;
int idx2 = h2(val) % _bitCount;
int idx3 = h3(val) % _bitCount;
//
if (!_bit.find(idx1))
return false;
if (!_bit.find(idx2))
return false;
if (!_bit.find(idx3))
return false;
return true;//可能存在
}
};
struct HashFun1 {
size_t operator()(const string & str)
{
size_t hash = 0;
for (const auto &ch : str)
{
hash = hash * 131 + ch;
}
return hash;
}
};
struct HashFun2 {
size_t operator()(const string & str)
{
size_t hash = 0;
for (const auto &ch : str)
{
hash = hash * 1313131 + ch;
}
return hash;
}
};
struct HashFun3 {
size_t operator()(const string & str)
{
size_t hash = 0;
for (const auto &ch : str)
{
hash = hash * 65599 + ch;
}
return hash;
}
};
void test()
{
BloomFilter<string, HashFun1, HashFun2, HashFun3> blm(10);
string str1 ="1https://editor.csdn.net/md?not_checkout=1&articleId=117413980";
string str2 ="2https://editor.csdn.net/md?not_checkout=1&articleId=117413980";
string str3 ="3https://editor.csdn.net/md?not_checkout=1&articleId=117413980";
blm.set(str1);
blm.set(str2);
blm.set(str3);
string str4 ="4https://editor.csdn.net/md?not_checkout=1&articleId=117413980";
cout<<"str4存在吗: " << blm.find(str4) << endl;
}
int main()
{
test();
return 0;
}