位图的实现与应用

目录

位图

实现思路

模拟实现

类模板

构造函数

set

reset

test

twobitset 实现

成员变量

set

is_once

布隆过滤器

成员变量与模板

set

test

删除


位图

位图是哈希的另一种表现,与我们常用的 set map 的思想还是基本一样的,下面我们看一下简单位图的实现:

实现思路

  • 在位图的实现中,我们想要用一个比特位,来表示一个数据的状态信息(也就是是否存在)。

  • 所以一个位图并不能表示一个数据存储/出现过的次数。

  • 但是在 C/C++ 的内置类型中并未有单位是比特位的类型,所以这个还是需要我们开空间的时候自己计算。

  • 其中开空间我们可以使用我们已知的数据类型去开空间例如:char、int。

  • 虽然上面只说了两种数据类型,但是并不是只有这两种可以使用,只是这两种比较常见,且容易控制。

  • 如果是 int 的话,那么开一个 int 空间就是开了 32 个比特位,也就是一个 int 可以存储 32 个数据的状态。

模拟实现

  • 当我们使用位图的时候,由于只有用户知道自己想要存储的数据的范围,所以位图的空间需要有用户来决定

  • 所以位图需要一个非类型模板参数用来表示可以存储的数据范围

类模板

    template<size_t N>
    class bitset
    {
    public:
    private:
        vector<int> _a;
    };
  • 这里我们使用 int 类型来控制。

  • 这里的非类型模板参数传过来的值,就可以在构造函数里面为 _a 开一段所需要的空间。

构造函数

        bitset()
        {
            _a.resize(N / 32 + 1);
        }
  • 由于这里使用的是 int 所以我们开辟的空间需要 N / 32,但是 N / 32 是向下取整,所以如果这样的话,我们还需要对除后的值多开一个 int 类型,防止开的空间不够。

set

  • set 就是将一个值设置到位图中

  • 设置进位图就是将对应的比特位设置为 1

  • 我们只需要让该比特位或 1 ,其他位为 0 即可

  • 我们可以将 1 像左移动到该比特位,然后与该值或等,由于或是有1为1,所以我们可以将该比特位设置为1

  • 在设置之前,我们还需要计算出对应的比特位的位置吗,由于我们使用的是 int 所以我们想知道到第几个比特位,我们就可以对该值进行除32 就是在对应的数组位置,然后我们知道了对应的数组位置,我们还需要知道该比特位在该数组位置的第几个比特位,所以我们还可以用该值对 32 进行取模操作,我们就知道对应的比特位了

  • 下面看代码会很容易理解

        void set(int n)
        {
            int i = n / 32;
            int j = n % 32;
​
            _a[i] |= 1 << j;
        }

reset

  • reset 就是将一个比特位移除,也就是将该比特位设置为 0

  • 想要将一个比特位设置为 0 ,我们可以对该比特位与等一个0,其他位为1即可

  • 还是看代码容易理解,或者可以自己写一下

        void reset(int n)
        {
            int i = n / 32;
            int j = n % 32;
​
            _a[i] &= ~(1 << j);
        }

test

  • test 就是用来探测一个值是否在位图中,如果为1 则在位图中,如果为 0 ,则不在位图中

  • 其实我们想知道一个比特位在位图中是否为1或者是0,我们可以对该比特位与1,如果结果是1,则说明该比特位表示的值是存在的,如果为0,表示该比特位表示的值并不存在

        bool test(int n)
        {
            int i = n / 32;
            int j = n % 32;
​
            return _a[i] & 1 << j;
        }

如果我们这里有一堆数字,我们想要判断这些数字中只出现了一次的值是什么,那么我们怎么使用位图来解决?

思路:由于一个位图只能表示在不在,那么我们可不可以有两个比特位,入如果有两个比特位,比那么我们就可以表示很多情况了 例如: 0 0 0 1 1 0 1 1,有上面四种情况,但是我们只需要判断出现了一次的数字,那么我们其实用不了四种情况,我们只需要三个就可以了, 0 0 表示没有出现过, 0 1 表示出现了一次, 1 0 表示出现了两次或者是两次以上,所以我们可以使用两个位图来表示:

twobitset 实现

成员变量

  • 上面说了,我们需要用两个位图来表示,所以我们需要两个位图

template<size_t N>
    class twobitset
    {
    public:
    private:
        bitset<N> _b1;
        bitset<N> _b2;
    };

set

  • 这里的 set 就需要分情况了,如果一次都没有出现,那么就是让 _b1 变为 0, _b2 变为 1,也就是 0 1

  • 如果这里已经出现了一次了,那么就是让 0 1 变为 1 0 所以我们需要让_b1 变为 1, _b2 变为 0 也即是 1 0

        void set(int n)
        {
            // 0  0 -> 0  1
            if (!_b1.test(n) && !_b2.test(n))
            {
                _b2.set(n);
            } // 0  1 -> 1  0
            else if (!_b1.test(n) && _b2.test(n))
            {
                _b1.set(n);
                _b2.reset(n);
            }
        }

is_once

  • 既然我们需要知道师傅出现了一次,那么我们当然需要有一个函数用来判断是否出现了一次

  • 如果 _b1 是 0 _b2 是 1 那么就表明出现了一次,否则就不是一次

        bool is_once(int n)
        {
            return !_b1.test(n) && _b2.test(n);
        }

布隆过滤器

  • 位图只能用来表示整数是否存在,那么如果还想表示其他类型,例如:字符串?

  • 布隆过滤器就可以用来解决这个问题

  • 这里可以使用 hash 函数对字符串进行映射,然后将映射出来的值作为整数放到位图里面

  • 但是,我们知道hash函数是会出现冲突的,如果字符串很多的话,那么冲突是很多的

  • 其中,我们可以将一个字符串使用多个hahs函数映射出来不同的值,然后将hahs函数映射出来的值都放到位图里面,这样有几个hahs函数就可以映射几个比特位,这样就可以减少冲突

  • 怎么探测是否存在呢?既然一个字符串映射了好几个hash比特位,所以我们探测是否存在的时候,也需要探测这些对应的比特位是否存在,只要有一个比特位不存在,则说明该字符串不存在

  • 但是即使是通过了多次的映射,也还会产生冲突,剩下的介绍冲突的做法就是增加空间了

成员变量与模板

  • 我们可以将hash函数作为模板传过去,可以使用仿函数来调用这些函数

  • 我们的成员变量就是一个位图

template<size_t N, class K = string, class Hash1 = BKDRHash, class Hash2 = APHash, class Hash3 = DJBHash>
    class bloomfilter
    {
    public:
    private:
        bitset<N> _bs;
    };

我们可以看一下这些hash函数

struct BKDRHash
{
    size_t operator()(const string& str)
    {
        size_t hash = 0;
        for (char ch : str)
        {
            hash = hash * 131 + ch;
        }
        return hash;
    }
};
 
struct APHash
{
    size_t operator()(const string& str)
    {
        size_t hash = 0;
        size_t ch;
        for (long i = 0; i < str.size(); i++)
        {
            if ((i & 1) == 0)
            {
                hash ^= ((hash << 7) ^ str[i] ^ (hash >> 3));
            }
            else
            {
                hash ^= (~((hash << 11) ^ str[i] ^ (hash >> 5)));
            }
        }
        return hash;
    }
};
struct DJBHash
{
    size_t operator()(const string& str)
    {
        if (str.size() == 0)
            return 0;
        size_t hash = 5381;
        for (char ch : str)
        {
            hash += (hash << 5) + ch;
        }
        return hash;
    }
};

set

  • set 函数就是有几个 hash 函数,我们就映射几个位置,但是也不是hash函数越多越好,只是在特定的范围内,hash函数多了可以减少冲突

  • 上面我们使用了3个hash函数

  • 上面的hash函数可以参考该网站:

    [各种字符串 hash 函数]  https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html 

           void set(const K& key)
            {
                size_t hash1 = Hash1()(key) % N;
                _bs.set(hash1);
                size_t hash2 = Hash2()(key) % N;
                _bs.set(hash2);
                size_t hash3 = Hash3()(key) % N;
                _bs.set(hash3);
                cout << hash1 << " " << hash2 << " " << hash3 << endl;
             }

test

  • 在布隆过滤器中,一个字符串映射了多个位置,所以如果想要探测该字符串是否存在,那么就需要探测这些是否位置是否被映射,只要有一个为假,那么该字符串便不存在

  • 我们前面说过,布隆过滤器是有冲突的,所以又可能有一个不存在的字符串被识别为存在,所以说,判断是否存在不是很准确的

  • 但是不存在是存在是准确的

        bool test(const K& key)
        {
            size_t hash1 = Hash1()(key) % N;
            size_t hash2 = Hash2()(key) % N;
            size_t hash3 = Hash3()(key) % N;
            return _bs.test(hash1) && _bs.test(hash2) && _bs.test(hash3);
        }

删除

  • 如果我们想要删除布隆过滤器中的一个值,那么我们怎么删除?

  • 其实布隆过滤器中的值是不能删除的,为什么?我们前面为了减少冲突,我们是一个值映射了多个位置。

  • 所以我们的一个比特位可能被多个值映射过,如果我们直接删除了的话,我们就可能导致其他值也找不到,所以不能删除。

  • 那么如果想要实现删除呢?

  • 我们可以多搞一些比特位,其中两个比特位可以表示到 3,所以如果我们想要删除,我们至少要 8 个比特位,而且 8 个比特位也不一定就足够了,如果我们搞这么多比特位,那么范围违背了我们的原则,我们使用布隆过滤器就是为了占用少量的空间,此时反而使用了很多空间,那么显然是不划算的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Naxx Crazy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值