用我们的决心、信心和毅力来培植我们的生命之花🍀|

3的4次方

园龄:2年1个月粉丝:5关注:89

2025-02-25 11:46阅读: 3评论: 0推荐: 0

布隆过滤器

布隆过滤器

https://www.cnblogs.com/yzsn12138/p/17014415.html

布隆过滤器用于防止缓存击穿。

位图

bitmap 是一种高效的且占用内存很小的判断某个值存在与否的数据结构。它用二进制的某一位去表示某个值是否存在。

比如我们需要统计10亿用户是否签到,正常的做法是你可以设计一个10亿长度的map,将用户的uid设置为key,是否签到设计为value,假设uid是int64_t类型,占用8个字节,10亿用户就需要大约8G的内存 ,而如果设计成bitmap去存储,则只需要大约125M 。极大的节约了内存。

由于10亿用户所需10亿个bit位,没有一种数据类型能有这么大,所以我们只能用字节数组来模拟

原理

因为bitmap中用二进制位代表某个uid是否存在,所以一个字节能够代表8个uid是否存在。为了兼容大小端模式,应当使用字节数组。因此大概是这样的结构:

image

上图中,数字10是如何存储的?因为这里最大的数是31,所以我们用了32个bit,即char [5],因此10应该在第 10/8=1 个字节中的左移 10%8=2 个位置(由于这里下标计数是从0开始的)。按照小端模式就是上图中的存储位置。

实现

namespace test {
template<size_t N>
class bitmap {
public:
// 把x设置为1
void set(size_t x)
{
auto part = find(x);
data[part.idx] |= (1 << part.pos); // 这里我用左移,因此pos是从低位向高位数的。如果规定是从高位往低位数,则应该用右移
}
// 把x设置为0
void reset(size_t x)
{
auto part = find(x);
data[part.idx] &= ~(1 << part.pos);
}
// 判断x是否为1
bool test(size_t x)
{
auto part = find(x);
return data[part.idx] & (1 << part.pos);
}
void print() {
for (size_t i = 0; i <= N; ++i) {
std::cout << i << ":" << test(i) << " ";
if (i != 0 && i % 8 == 0)
std::cout << std::endl;
}
std::cout << std::endl;
}
private:
struct Part {
size_t idx; // 在哪个字节
size_t pos; // 在这个字节的第几个位置
};
Part find(size_t x) {
if (x > N) {
throw std::out_of_range("x 超出bitmap表示范围");
}
size_t idx = x / 8; // x 在第 idx 个字节
size_t pos = x % 8; // x 在这个字节的第 pos 个位置
return {idx, pos};
}
// 使用字节来作为基本单元可以避免大小端的问题
std::array<char, N / 8 + 1> data{}; // 至少能存储 0~N 的数
};
}
int main() {
test::bitmap<100> bs;
bs.set(99);
bs.set(0);
bs.set(98);
bs.set(55);
bs.set(75);
bs.set(35);
bs.reset(99);
bs.reset(87);
bs.print();
return 0;
}

布隆过滤器

布隆过滤器是一个大型位图(bit数组或向量) + 多个无偏哈希函数组成的概率型数据结构,特点是高效地插入和查询

假设我们要存储字符串类型,此时可以通过字符串哈希来得到一个id,从而插入位图。但是哈希可能冲突,所以需要多次哈希,每次哈希使用的哈希函数都不同,从而降低哈希冲突的概率。

此外,由于当数据量非常非常大时,使用一一对应的位图仍然会占用很大的内存。而如果我们使用复用多个bit位,使用这些bit的组合来表示存在某个元素,就可以减少bitmap的大小。综上我们就得到了布隆过滤器。

原理

当一个数据映射到位图中时,布隆过滤器会用多个哈希函数映射到多个比特位,当判断一个数据是否在位图当中时,需要分别根据这些哈希函数计算出对应的比特位,比特位设置了代表着当前状态的默认值,设置为 1 则判定为该数据存在。

举个例子:针对值 “source” 和三个不同的哈希函数分别生成了哈希值 2、4、7

image

现在,如果我们要查询"source"这个字符串是否存在,就要判断位图中下标2,4,7对应的值是否均为1,若是,则说明此字符串“可能”存在。注意这里就可能出现误判(下面介绍)

此外,布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素的哈希值对应的bit位。一般是直接重新构建布隆过滤器:

  1. 创建一个新的空布隆过滤器
  2. 将原布隆过滤器中的所有元素(除了要删除的元素)重新添加到新的布隆过滤器中
  3. 用新的布隆过滤器替换原有的布隆过滤器

另一种支持删除的方法是计数法删除:

  • 将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加1,删除元素时,给k个计数器减1,减到0说明该bit被清空,从而避免影响其他元素的bit位。
  • 缺陷:
    1. 无法确认元素是否真正在布隆过滤器中(还是存在假阳率,也由于hash冲突的原因可能导致删不干净,也就是误判)
    2. 占用空间更多了
    3. 存在计数回绕(就是数据类型溢出导致一直加到最小值0或者一直减到最大值)

那为什么至今过滤器都没有提供对应的删除接口呢?其实过滤器的本来目的就是为了提高效率和节省空间,但是在确认存在时去遍历文件,文件 IO 和磁盘 IO 的时间开销是不小的,其次在每个比特位增加额外的计数器,更是让空间开销飙升到本身的好几倍。

误判

在上述基础上,我们再存一个字符串"create",假设哈希函数返回3,4,8,则对应的图如下:

image

此时,下标为4的bit位出现了哈希冲突。假设这里"create"的另外两个哈希函数也出现冲突了,那么我们就无法判断"create"到底是不是真的出现在bitmap中。因此:

  • 布隆过滤器判断存在,是可能误判的
  • 布隆过滤器判断不存在,是不可能误判的

那么怎么降低误判几率?

  • 执行更多的哈希操作
  • 不要把底层的bitmap设置得太小

学者们给出了一个权衡效率和误判率的公式:

\[\begin{cases} m=-\frac{n\ln p}{(\ln 2)^2}\\ k=\frac{m}{n}\ln 2 \end{cases} \]

其中 \(m\) 是bitmap长度,\(n\) 是插入元素个数,\(k\) 是哈希函数个数,\(p\) 是误判率。

实现

以存储string字符串类型为例

namespace test {
// BKDRHash
struct BKDRHash
{
size_t operator()(const std::string& str)
{
size_t hash = 0;
for (size_t i = 0; i < str.length(); ++i) {
hash = hash * 131 + str[i];
}
return hash;
}
};
// SDBHash
struct SDBHash
{
size_t operator()(const std::string str)
{
size_t hash = 0;
for (size_t i = 0; i < str.length(); ++i) {
hash = 65599 * hash + str[i];
//hash = (size_t)ch + (hash << 6) + (hash << 16) - hash;
}
return hash;
}
};
// RSHash
struct RSHash
{
size_t operator()(const std::string str)
{
size_t hash = 0;
size_t magic = 63689;
for (size_t i = 0; i < str.length(); ++i) {
hash = hash * magic + str[i];
magic *= 378551;
}
return hash;
}
};
template<size_t N, typename T = std::string, typename Hash1 = BKDRHash, typename Hash2 = SDBHash, typename Hash3 = RSHash>
class bloomfilter {
public:
// 存储x
void set(const T& x)
{
auto idice = hash(x);
bs.set(std::get<0>(idice));
bs.set(std::get<1>(idice));
bs.set(std::get<2>(idice));
}
// 检查x是否存在(可能会误报,判断存在是不准确的,判断不在是准确的)
bool test(const T& x)
{
auto idice = hash(x);
return bs.test(std::get<0>(idice))
&& bs.test(std::get<1>(idice))
&& bs.test(std::get<2>(idice));
}
private:
std::tuple<size_t, size_t, size_t> hash(const T& x)
{
return { Hash1()(x) % N, Hash2()(x) % N, Hash3()(x) % N };
}
bitmap<N * 5> bs; // *5是根据上述公式算出来的,大概取这个数可以降低误判率
};
}
int main() {
test::bloomfilter<100> bf;
bf.set("douyin");
bf.set("kuaishou");
bf.set("pass cet6");
bf.set("aabb");
std::cout << bf.test("pass cet6") << std::endl;
std::cout << bf.test("kuaishou") << std::endl;
std::cout << bf.test("douyin") << std::endl;
std::cout << bf.test("abab") << std::endl;
return 0;
}

本文作者:3的4次方

本文链接:https://www.cnblogs.com/3to4/p/18735907

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @ 2025-02-25 11:46  3的4次方  阅读(3)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起