布隆过滤器

位图

位图的概念

在介绍位图之前我们先看一个题目

给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在 这40亿个数中。

对于这个问题有我们可以想到很多解决的方法

  1. 遍历,时间复杂度$O(N)$
  2. 排序+二分法(排序我们可以使用快排)所以就是时间复杂度就是O(NlogN + logN)
  3. 使用红黑树或者哈希表

==但是以上的方法都是要在内存中的!而且这是一个**40亿个整数!**光是数据量就是16g!一般的内存其实是放不下的!所以红黑树和哈希表就已经被否决了!==

既然放内存放不下我们是能不能用归并排序进行外排序!但是在磁盘里面就不能用二分法查找!磁盘里面是不支持随机访问的!

因为数据量实在是太大了!所以导致了我们常规的方法都不能使用了!

因为==我们只要知道这个值到底在不在这个数据里面!==——==而如何标记一个值在不在呢?用0和1就可以了!我们只要一个bit位就可以==

位图的思想其实也就是一个哈希——但是不是一般的哈希表

image-20230503152550275

image-20230503162537654

==就像是上面的这幅图一样!——因为在计算机里面二进制是左大右小的!像是1 就是00000001所以0-7就是从右边开始写起来!==

==这个时候42亿个数据只要512m的内存大小!==

所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用 来判断某个数据存不存在的。

位图的实现

首先我们要解决如何开辟和映射的问题——==因为C++是不支持按bit位开辟空间的!

image-20230503153734074

==也可以按照int类型 除8与模8改成除32与模32==

成员变量

#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
       template<size_t N>//使用非类型模版参数
       class bitset
       {
           private:
           std::vector<char> _bit;
       };
}

成员函数

构造函数
#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
       template<size_t N>//使用非类型模版参数
       class bitset
       {
           public:
           bitset()
           {
               //我们开辟的空间不是N,而是N/8+1
               //因为我们要的是N个bit,而一个字节是8个bit
               //为什么要+1呢因为防止不是8的倍数,例如20,20/8=2,但是20个bit需要3个字节
               //所以我们要+1
               _bit.resize(N/8+1,0);
               //如果想使用位移要记得加括号,因为<<的优先级比+低
               //_bit.resize((N>>3)+1,0);
           }
           private:
           std::vector<char> _bit;
       };
} 	
set

set就是将bit位置为1

#include <vector>
namespace MySTL
{
    template<size_t N>//使用非类型模版参数
    class bitset
    {
        public:
        void set(size_t x)//set就是将bit位置为1
        {
            size_t index = x/8;//确定在哪个字节
            size_t pos = x % 8;//确定在字节中的哪个bit

            //将对应的BIT位置为1
            //可以使用或进行运算!
            //因为或要两个都为0才为0,其他情况都为1
            //我们只要有一个除了与这个BIT对应的位置为1,其他位置都为0的数
            //我们就可以通过或将这个BIT位置为1,其他位置不变
            //无论这个BIT原来是0还是1,都会变成1
            _bit[index] |= (1<<pos);
            //(1<<pos),将1左移pos位,就是将1放在pos位置上
            //因为二级制位是左高右低,所以要向左移动pos位
            //这里和大小端没有关系,因为我们使用的是char,char是一个字节
        }
        private:
        std::vector<char> _bit;
    };
}

image-20230503162916220

reset
#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
       template<size_t N>//使用非类型模版参数
       class bitset
       {
           public:
           void reset(size_t x)//reset就是将bit位置为0
           {
               size_t index = x/8;
               size_t pos = x % 8;

               //reset是将该bit位置为0.无论原先是0还是1
               //这时候我们可以使用与运算
               //因为与运算要两个都为1才为1,其他情况都为0
               //我们可以使用一个除了这个BIT位置为0,其他位置都为1的数
               //例如pos的值为 2,先将1左移2位,就是将1放在第二位上
               //然后进行取反,就是将这个数的所有位取反
               //然后再进行与运算,就可以将这个BIT位置为0,其他位置不变
               _bit[index] &= ~(1<<pos);
           }
           private:
           std::vector<char> _bit;
       };
}

image-20230504092406223

test
#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
       template<size_t N>//使用非类型模版参数
       class bitset
       {
           public:
           bool test(size_t x)//检查这个bit是否为1
           {
               //这个也很简单,我们只要有一个除了这个BIT位置为1,其他位置都为0的数
               //然后进行与运算,就可以得到这个BIT的值
               //如果为0,就是false,如果不为,就是true
               size_t index = x/8;
               size_t pos = x % 8;
               return _bit[index] & (1<<pos);
           }
           private:
           std::vector<char> _bit;
       };
}

位图总结

==位图用来判断大量的整形数据存不存在的时候效率是非常高的!==

==但是位图不是万能的!因为位图只能用来判断整形!==

而库里面也为我们提供了这个库

image-20230504155239606

==不过库里面的接口比我们实现的更多,我们主要实现了的它的几个关节接口的实现!==

位图应用

==位图一般都是用来处理大量的数据==

例1:给定100亿个整数,设计算法找到只出现一次的整数?

有三种状态 1. 0次,2. 1次,3.1次以上

==这其实就是位图的变形!==

image-20230504160055818

==那么我们应该如何实现这个呢?——将我们上面的代码进行修改么?==

不用那么麻烦!我们可以复用上面的代码!——我们可以开两个位图!

image-20230504160408946

//代码实现
#pragma once
#include <iostream>
#include <vector>
namespace MySTL
{
    template<size_t N>
    class TowBitSet
    {
        public:
        void set(size_t x)
        {
            if(!_bit1.test(x) && !_bit2.test(x))//00的情况
            {
                _bit2.set(x);//第一次出现,变成01
            }
            else if(!_bit1.test(x) && _bit2.test(x))//01的情况!
            {
                _bit1.set(x);
                _bit2.reset(x);//变成10
            }
            else if(!_bit1.test(x) && _bit2.test(x))//10的情况
            {
                return;//什么不动
            }
        }
        //打印出现一次的数
        void printonce()
        {
            for(size_t i = 0;i<N;++i)
            {
                if(_bit1.test(i) && !_bit2.test(i))
                {
                    std::cout<<i<<" ";
                }
            }
        }
        private:
        bitset<N>  _bit1;
        bitset<N>  _bit2;
    };
}

而且我们是不用担心内存不够的!因为位图的大小只与范围相关!而与实际的数据多少无关

==哪怕是1000亿个整数,它的范围大小都是整数的范围(0~42亿9千万多)

列2:给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?

思路1:==我们可以将其中一个文件给映射进位图里面,然后将另一个文件去位图里面判断在不在!然后将结果放在另一个文件!如果在那么就是交集,如果不再就不是交集!==——但是要去重!因为如果判断的交集的文件里面有很多重复的!那么存放交集的文件里面也会有很多重复的元素!所以我们要将交集去重!

image-20230505145127349

思路2:==将两个文件都分别放进两个位图里面!然后依次判断两个位图中相同的位置!如果位置都为1,就是交集,如果不都为1,那么就不是交集!==

image-20230505145606626

这样子就不用去重了!因为位图自己就已经完成了去重

例3:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数

这个其实和例1是相似的!就是通过多个位图来标记状态!

==不超过两次那么就是说有四种状态==

image-20230505150504877

位图优缺点

位图的优点

  1. 节省空间
  2. 效率高(快)

位图的缺点

  1. 要求范围相对集中,范围特别分散,空间消耗就提升(例如有100个值,一个最小是1,最大是42亿,那么消耗就很大了!)
  2. 只能针对整形!

布隆过滤器的概念

为什么会有布隆过滤器这种东西呢?

==布隆过滤器其实是在位图的基础上进一步的产物==

从位图我们可以看出来,判断一个值是否存在其实不用那么麻烦,如果是使用哈希表或者红黑树,我们还要把值都存起来!然后再找!这样子其实消耗很大!如果数据量太大了!那么红黑树和哈希表就不够用了!——所以有了位图,==只用一个标记位来标记一个值在或者不再!==

==上面我们知道位图的缺点之一就是只能针对整形!那么我们有没有办法字符串进行映射呢?——答案是使用哈希函数(hashFunc(str))将字符串转换整形!然后通过这个整形来映射!不止是字符串!我们可以通过不同的哈希函数!将不同的类型转换为整形!==

image-20230505170815383当我们需要知道是否存在的时候,我们就将这个字符串转化为整形,去查看这个位图所在的位置是不是1,是就是存在!

但是这个方法也是有缺陷的!因为万一有多个字符串同时映射一个位置的时候就会导致误判!

因为字符串是无限的!但是整形是有限的!

所以判断==在这种情况是==不准确的!

因为可能不在,但是位置与别的字符串冲突了导致了误判!

也就是说即使不再对应的位置也可能是1

但是反过来!==不在这种情况却是准确的!==

如果不在那么那个位置一定是0

同时我们也要将映射的范围进行限定!

如果才100个字符串!却将所有整形开出来,那么空间浪费也很大!但是如果不全部开出来会导致hashFunc算出来的值有小,有的大!范围可能不够!所以我们必须模一下

==布隆过滤器就是上面方法的一种优化!==

既然会出现误判!那么我们有什么办法来减少误判呢?

image-20230505180439903

==但是这种方法是不能避免误判的!只能减小误判的概率!因为有可能所有的位置都跟别的冲突了!==——这种方法就叫布隆过滤器!

==布隆过滤器的改进==

映射多个位置,降低误判率!(但是不是映射位置越多越好!应该这样子会造成很大空间浪费!)

布隆过滤器的应用

1 )可以适用于一些不一定准确的场景——注册时候的昵称判重!

在比较简单的场景下面,假如我们想要快速的判断!我们就可以用用到布隆过滤器!将所有的名称都映射进位图里面!然后再

2 )提高效率——做一个前置过滤

image-20230506093058837

==如果对某些字段经常进行查找,但是又发现有很多值都是不在的!那么就可以提高效率了!==

但是布隆过滤器也不能太多了!因为布隆过滤器也是要消耗内存的!如果太多了也可能回影响效率!

3 )黑名单——这种不太要求准确,但是要快速判断的!

例如一些不宜内容的网页,或者垃圾邮箱,因为如果要传入到服务器里面进行判断那么太慢了

但是放到布隆里面就很快了!

布隆过滤器的实现

#pragma once
#include <iostream>
#include <bitset>
#include <string>
using namespace std;
namespace MySTL
{
    template<class T>
    struct BKDRHash
    {
        size_t operator()(const T &str)
        {
            size_t hash = 0;
            for(auto chi :str)
            {
                size_t ch = (size_t)chi;
                hash = hash * 131 + ch;
            }
            return hash;
        }
    };
    template<class T>
    struct SDBMHash
    {
        size_t operator()(const T &str)
        {
            size_t hash = 0;
            for(auto chi :str)
            {
                size_t ch = (size_t)chi;
                hash = 65599 * hash + ch;
                //hash = (size_t)ch + (hash << 6) + (hash << 16) - hash;
            }
            return hash;
        }
    };


    template<class T>
    struct RSHash
    {
        size_t operator()(const T &str)
        {
            size_t hash = 0;
            size_t magic = 63689;
            for(auto chi :str)
            {
                size_t ch = (size_t)chi;
                hash = hash * magic + ch;
                magic *= 378551;
            }
            return hash;
        }
    };

    template<size_t N,//需要存储的最大数据!
            size_t X= 5,//平均存储一个值,开辟X位的空间!
            class K = std::string,
            class HashFunc1 = BKDRHash<K>,
            class HashFunc2 = SDBMHash<K>,
            class HashFunc3 = RSHash<K>>//哈希函数个数和玻璃长度有关系!
    class BloomFilter
    {
    public:
        void set(const K& key)
        {
            size_t hash1 = HashFunc1()(key) % (X*N);//要加上()因为 % 优先级高于*
            size_t hash2 = HashFunc2()(key) % (X*N);
            size_t hash3 = HashFunc3()(key) % (X*N);
            _bs.set(hash1);
            _bs.set(hash2);
            _bs.set(hash3);
        }
        bool test(const K& key)
        {
            size_t hash1 = HashFunc1()(key) % (X*N);
            if(!_bs.test(hash1))
                return false;
            size_t hash2 = HashFunc2()(key) % (X*N);
            if(!_bs.test(hash2))
                return false;
            size_t hash3 = HashFunc3()(key) % (X*N);
            if(!_bs.test(hash3))
                return false;

            //前面判断不在是不准确的!
            return true;//可能存在误判!映射的几个位置都冲突了!
        }
    private:
        std::bitset<N*X> _bs;
    };
}

关于要几个哈希函数,和布隆过滤器的长度有关!

详解布隆过滤器的原理,使用场景和注意事项 - 知乎 (zhihu.com)关于想在这个方面更加的深入可以看这篇文章

image-20230506145656648

==过滤器是用来减低误判的!不是用来避免误判的!==

==这个不支持reset!因为一个位置可能会被多个值给映射!如果reset掉可能就会影响其他的查找!==

image-20230506171219407

如果我们把find给reset了!那么我们就会影响Insert!

==但是我们也可以强制让它支持reset!那就是进行计数!有几个值映射相同的位置!那么计数器就是几,如果要reset,那么就去计数器--,等计数器为0的时候,才将位置置为0!==

但是这样子有一个很大的麻烦就是会导致空间成倍的增加!因为我们不知道一个位置究竟有多少个映射!所以这就导致我们必须尽可能大的去开

image-20230506171927711

==所以与其这样,不如就不支持reset!==

测试
#pragma once
#include <iostream>
#include <bitset>
#include <string>
namespace MySTL
{
    void test_bloomfilter()
    {
        srand(time(0));
        const size_t N = 100000;
        BloomFilter<N> bf;

        std::vector<std::string> v1;
        std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";

        for (size_t i = 0; i < N; ++i)
        {
            v1.push_back(url + std::to_string(i));
        }

        for (auto& str : v1)
        {
            bf.set(str);
        }

        // v2跟v1是相似字符串集,但是不一样
        std::vector<std::string> v2;
        for (size_t i = 0; i < N; ++i)
        {
            std::string url = "https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html";
            url += std::to_string(999999 + i);
            v2.push_back(url);
        }

        size_t n2 = 0;
        for (auto& str : v2)
        {
            if (bf.test(str))
            {
                ++n2;
            }
        }
        cout << "相似字符串误判率:" << (double)n2 / (double)N << endl;

        // 不相似字符串集
        std::vector<std::string> v3;
        for (size_t i = 0; i < N; ++i)
        {
            string url = "zhihu.com";
            url += std::to_string(i+rand());
            v3.push_back(url);
        }

        size_t n3 = 0;
        for (auto& str : v3)
        {
            if (bf.test(str))
            {
                ++n3;
            }
        }
        cout << "不相似字符串误判率:" << (double)n3 / (double)N << endl;
    }
}

image-20230506164808095

==我们可以通过增大X与增加哈希函数个数用来减少误判率!==