模拟实现位图和布隆过滤器超详解(C++)

hello!我是bug。今天我们来进一步学习哈希的相关内容——位图和布隆过滤器
(代码可能会有一点问题,请各位老铁指正 )

文章目录

  • 前言
  • 一、位图
  • 二、位图的模拟实现
  • 三、布隆过滤器
  • 四、布隆过滤器的模拟实现

前言

当我们进行对数据进行查找时,红黑树、AVL树、哈希表都是很好的选择。因为它们查找的效率极高。但是随着数据的增多,使用它们存储数据时,要申请大量的空间。
当数据量达到十亿时(假设为int类型),单存储数据就需要以4个G的内存,但是红黑树中还有指针三叉链、颜色标识,哈希表就更不用提了,其需要申请更大的空间。这个时候内存就成为了一个大问题,而且面对更多的数据时,红黑树和哈希表不再适用。这个时候位图就被引入了。

一、位图

位图就是用每一位来存放某种状态,判断数据存不存在,通常用于海量数据,且数据的状态较少。

那么位图具体是什么呢?如下图:
模拟实现位图和布隆过滤器超详解(C++)_第1张图片
有8个二进制位,每个二进制位都对应一个下标,二进制中只能存储1和0,即两种状态。假设1表示数据存在、0表示数据不存在,那么当我们进行查找时,只要计算出数据的下标就可以找到对应的二进制,从而判断数据是否存在。⚽️⚽️
这里我们开辟一个数组,元素类型为int,一个int类型的元素占用32个bit位那么每个元素就可以表示32个数据。进行数据存储时,先计算数据位于哪个元素中,再计算数据在元素的第几个二进制位,通过位运算确定二进制位上存储的内容判断数据是否存在
⚽️⚽️

注意❗️ ❗️

位图一般用来查找整型数据,因为整型数据可以直接一一映射,不需要进行转换。而其他类型就需要转换成整型、再来设置二进制位、查找操作。但是,保证其他类型转换成二进制位不发生冲突几乎是不可能的,所以就引出了下面的布隆过滤器(下面介绍)。

BitSet的相关接口:

函数 用法
operator[] 通过[]判断数据是否存在
set 将数据对应的二进制位设置为1
reset 将数据对应的二进制位设置为0
test 判断数据是否存在
count 返回数据的个数
size 返回位图中比特位的总个数

二、位图的模拟实现

位图的实现代码⬇️ ⬇️ :

#pragma once
#include

namespace lz
{
	template<size_t n, class T = int>
	class BitSet
	{
	private:
		vector<T> _bit;
		size_t _bit_count;
	public:
		//多开一个桶,因为除法是向下取整,这里位运算优先级低,要加括号
		BitSet(size_t bit_count = n):_bit((bit_count >> 5) + 1), _bit_count(bit_count){}

		bool operator[](size_t pos)const { return test(pos); }

		BitSet& set()
		{
			//计算桶的个数
			size_t bucket = (_bit_count >> 5) + 1;
			//将每个桶中的比特位置为0
			for (size_t i = 0; i < bucket; i++)
				_bit[i] |= -1;
			return *this;
		}

		BitSet& set(size_t pos)
		{
			assert(pos <= _bit_count);
			size_t bucket = pos >> 5;
			size_t index = pos % 32;
			_bit[bucket ] |= (1 << index);
			return *this;
		}

		BitSet& reset()
		{
			//计算桶的个数
			size_t bucket = (_bit_count >> 5) + 1;
			//将每个桶中的比特位置为0
			for (size_t i = 0; i < bucket; i++)
				_bit[i] &= 0;
			return *this;
		}

		BitSet& reset(size_t pos)
		{
			assert(pos <= _bit_count);
			size_t bucket = (pos >> 5);
			size_t index = pos % 32;
			_bit[bucket] &= ~(1 << index);
			return *this;
		}

		bool test(size_t pos)const
		{
			assert(pos <= _bit_count);
			size_t bucket = (pos >> 5);
			size_t index = pos % 32;
			return _bit[bucket] & (1 << index);
		}

		size_t size()const { return _bit_count; }

		// 计算位图中比特为1的个数
		size_t count()const
		{
			//每个数字里面bit为1的数量
			int bitCnttable[256] = {
			0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2,
			3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3,
			3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3,
			4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4,
			3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5,
			6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4,
			4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5,
			6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5,
			3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3,
			4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6,
			6, 7, 6, 7, 7, 8 };

			size_t count = 0;
			for (size_t i = 0; i < _bit.size(); ++i)
			{
				int value = _bit[i];
				int j = 0;
				//一次判断一个字节,即8个bit位
				while (j < sizeof(_bit[0]))
				{
					unsigned char c = value;
					count += bitCnttable[c];
					++j;
					value >>= 8;
				}
			}
			return count;
		}
	};
}

count:计算位图中被set的bit个数
不进行遍历,直接对字节进行判断。一个字节中有八个比特位,那我们要概括所有情况,就开辟一个数组,数组的大小为一个字节所有的可能情况,每个情况即下标对应存储的就是比特位为1的个数,那么我们直接通过下标就可以访问获得该数字中比特位为1的个数,再遍历每一个字节求和即可。

测试代码⬇️ ⬇️ :

void Test_BitSet()
{
	lz::BitSet<200> bs1;
	bs1.set(100);
	bs1.set(100);
	bs1.set(32);
	bs1.set(40);
	bs1.set(0);

	//清除
	//bs1.reset(100);
	//bs1.reset(0);
	// 全部清空
	bs1.reset();

	bs1.set();

	cout << "bs1.operator[](100) : " << bs1.operator[](100) << endl;;
	cout << "bs1.operator[](50) : " << bs1.operator[](50) << endl;;
	cout << "bs1.test(100) : " << bs1.test(100) << endl;
	cout << "bs1.test(50) : "<< bs1.test(50) << endl;
	cout << "bs1.size() : " << bs1.size() << endl;
	cout << "bs1.count() : " << bs1.count() << endl;
}

三、布隆过滤器

布隆过滤器:是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。即:为bitset和hash的结合。

针对其它类型的数据,冲突是必然的,我们只能进行缓解。即使用多个哈希函数进行映射,映射到多个位置中,多个位置全部冲突的概率比单个位置冲突要低得多,但是还是有一定几率的。布隆过滤器判断数据不存在没有误差,但是判断数据存在是有一定误差几率,所以如果只要求不存在的判断精确、存在的判断可以有误差的话,选择布隆过滤器。

优点:
1、插入和查找的时间复杂度为O(K),K为哈希函数的个数(一般较小)。
2、哈希函数之间没有关系,方便硬件并行运算。
3、 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势。
4、在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势。
5、数据量很大时,布隆过滤器可以表示全集,其他数据结构不能。
6、使用同一组散列函数的布隆过滤器可以进行交、并、差运算。

缺点:
1、有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白
名单,存储可能会误判的数据)
2、 不能获取元素本身
3、一般情况下不能从布隆过滤器中删除元素
4、如果采用计数方式删除,可能会存在计数回绕问题

注意❗️ ❗️

BloomFilter的接口很简单,就插入和查找。它是不支持删除的,因为删除的过程中,很可能影响到其它数据,因为多个数据可能会映射到同一个位置。

这个时候,如果一定要实现删除,那么就要使用计数操作。用多个比特位来替代原来的一个比特位。每当发生哈希冲突时,就进行自增操作,记录当前位置映射的次数,那么删除时,直接将次数自减即可。但是这种方法缺点也很明显,即比特位数量的选用,如果比特位选用过多,那么表示的数据也会更多,同时空间的消耗更大。相反,比特位选用过少,表示的数据也会更少,但是空间消耗也会随之降低。


四、布隆过滤器的模拟实现

BloomFilter的实现代码⬇️ ⬇️:

#pragma once
#include"BitSet.h"

namespace lz
{
    struct BKDRHash
    {
        size_t operator()(const string& str)
        {
            size_t sum = 0;
            for(size_t i = 0; i < str.size(); i++)
            {
                sum = sum * 131 + str[i];
            }
            return sum;
        }
    };

    struct SDBMHash
    {
        size_t operator()(const string& str)
        {
            size_t sum = 0;
            for(size_t i = 0; i < str.size(); i++)
            {
                sum = 65599 * sum + str[i];
            }
            return sum;
        }
    };

    struct RSHash
    {
        size_t operator()(const string& str)
        {
            size_t sum = 0;
            for (size_t i = 0; i < str.size(); i++)
            {
                sum = 63689 * sum + str[i];
            }
            return sum;
        }
    };

    struct APHash
    {
        size_t operator()(const string& str)
        {
            size_t sum = 0;
            for (size_t i = 0; i < str.size(); i++)
            {
                if ((i & 1) == 0)
                    sum ^= ((sum << 7) ^ str[i] ^ (sum >> 3));
                else
                    sum ^= (~((sum << 11) ^ str[i] ^ (sum >> 5)));
            }
            return sum;
        }
    };

    struct JSHash
    {
        size_t operator()(const string& str)
        {
            if (!str[0])        // 保证空字符串返回哈希值0  
                return 0;

            size_t sum = 1315423911;
            for (size_t i = 0; i < str.size(); i++)
            {
                sum ^= ((sum << 5) + str[i] + (sum >> 2));
            }
            return sum;
        }
    };

    struct DJBHash
    {
        size_t operator()(const string& str)
        {
            if (!str[0])   // 这是由本人添加,以保证空字符串返回哈希值0  
                return 0;
            size_t sum = 5381;
            for (size_t i = 0; i < str.size(); i++)
            {
                sum += (sum << 5) + str[i];
            }
            return sum;
        }
    };

	template<class T, size_t n, class Hash1 = BKDRHash,class Hash2 = SDBMHash,class Hash3 = RSHash, class Hash4 = APHash, class Hash5 = JSHash, class Hash6 = DJBHash>
	class BloomFilter
	{
	public:
		BloomFilter(size_t bf = n):_bf(bf * 6),_size(0){}
        void insert(const T& val)
        {
            vector<size_t> v1(6);
            v1.push_back(Hash1()(val) % _bf.size());
            v1.push_back(Hash2()(val) % _bf.size());
            v1.push_back(Hash3()(val) % _bf.size());
            v1.push_back(Hash4()(val) % _bf.size());
            v1.push_back(Hash5()(val) % _bf.size());
            v1.push_back(Hash6()(val) % _bf.size());
            for (auto e : v1)
            {
                _bf.set(e);
            }
            _size++;
        }

        bool IsInBloomFilter(const T& val)
        {
            //6个哈希函数分别映射
            vector<size_t> v1(6);
            v1.push_back(Hash1()(val) % _bf.size());
            v1.push_back(Hash2()(val) % _bf.size());
            v1.push_back(Hash3()(val) % _bf.size());
            v1.push_back(Hash4()(val) % _bf.size());
            v1.push_back(Hash5()(val) % _bf.size());
            v1.push_back(Hash6()(val) % _bf.size());
            //开始判断,有一个没找到,就不存在
            for (auto e : v1)
                if (!_bf.test(e))//如果没有找到,那么直接返回false
                    return false;

            return true;
        }

	private:
		//位图
		lz::BitSet<n> _bf;
		//有效元素个数
		size_t _size;
	};
}

测试代码⬇️ ⬇️:

void Test_BloomFilter()
{
	//字符串
	lz::BloomFilter<string,256> bf;
	string arr[] = {
	"left", "左边" ,"right", "右边","up", "向上"
	,"down", "向下","left","左边","eat","吃"
	,"sleep","睡觉","run","跑","jump","跳" };
	//插入
	for (const auto& str : arr)
		bf.insert(str);

	//判断
	for (const auto& str : arr)
		cout << bf.IsInBloomFilter(str) << " ";

	cout << endl << "bf.IsInBloomFilter : " << bf.IsInBloomFilter("abc") << endl;
}

今天的内容到这里就结束了,希望各位小伙伴们能够有所收获,我们下期再见!
模拟实现位图和布隆过滤器超详解(C++)_第2张图片

你可能感兴趣的:(c++,hash)