【C++/STL】位图和布隆过滤器

1.位图

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

在这里插入图片描述

1.1模拟实现位图

​ 因为需要对位图的每一个比特位进行管理,所以可以想到使用char类型的数组,从而实现对每一个比特位的管理。

template <size_t N>
class mybitset
{
public:
    //构造函数
    mybitset()
    {
        _bits.resize(N/8 +1);
    }
    //设置比特位
    void set(size_t x)
    {
        size_t i=x/8;
        size_t j=x%8;
        _bits[i]|=(1<<j);
    }
    void reset(size_t x)
    {
        size_t i=x/8;
        size_t j=x%8;
        _bits[i]&=~(1<<j);
    }
    //判断是否有x
    bool test(size_t)
    {
        size_t i=x/8;
        size_t j=x%8;
        return _bits[i]&(1<<j);
    }
private:
    vector<char> _bits;
}
1.2位图的应用
  • 快速判断一个数是否在一个集合中
  • 排序+去重
  • 求两个集合的交集和并集等
  • 操作系统中磁盘使用情况的标记。【比如在操作系统中,删除数据可能并不会“物理”删除数据,而是将该块内存设置为可以访问,然后后面的数据对原数据直接进行覆盖,所以这也是为什么删除数据后,数据有可能被找回】。

2.布隆过滤器

参考文献:https://zhuanlan.zhihu.com/p/43263751/

假如有100亿个ip地址在文件中,如何判断一个ip地址是否在这个文件中?

  • 1.用哈希表存储ip地址,缺点是:浪费空间。
  • 2.用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理。
  • 3.将哈希与位图结合,即布隆过滤器
2.1布隆过滤器概念

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

2.1.1单个哈希函数与位图结合

一个Key映射到一个比特位。

在这里插入图片描述

容易出现的问题:

  • 比特位只能标记该点对应的数据是否存在
  • 当发生哈希冲突时,存在误判的情况。
  • 所以可以判断数据一定不存在或者可能存在。
2.1.2多个哈希函数与位图结合

在这里插入图片描述

位图只能标记这个位置对应hashi存不存在;

最佳的哈希函数个数

​ 过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。

​ 哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高。

在这里插入图片描述

k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误报率

最佳的K值和m值的确定为:

在这里插入图片描述

可以计算得到结果:m=(k*n)/0.69

2.2布隆过滤器的实现

​ Hash函数的个数由自己确定最合适的个数,这里取三个哈希函数。

​ 布隆过滤器的查找思想:分别计算每个哈希值对应的比特位置存储的是否为零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中

template <size_t M,class T=string,class Hashfunc1=BKDRHash,class Hashfunc2=APHash,class Hashfun3=DJBHash>
class BloomFilter
{
public:
    //构造函数
    BloomFilter(){}	//默认构造函数足够使用
    //设置当前位置
    void set(const T&key)
    {
        size_t hash1=Hashfun1()(key)%M;
        size_t hash2=Hashfun2()(key)%M;
        size_t hash3=Hashfun3()(key)%M;
        _bs.set(hash1);
        _bs.set(hash2);
        _bs.set(hash3);
    }
    
    //检测是否存在
    bool Test(const T&key)
    {
        size_t hash1=Hashfun1()(key)%M;
        if(_bs.test(hash1)==false)
        {
            return false;
        }
        size_t hash2=Hashfun2()(key)%M;
        if(_bs.test(hash2)==false)
        {
            return false;
        }
        size_t hash2=Hashfun3()(key)%M;
        if(_bs.test(hash3)==false)
        {
            return false;
        }
        
        //存在误判
        return true;
    }
private:
    bitset<M> _bs;
}

常见的哈希函数

struct BKDRHash
{
	size_t operator()(const string& s)
	{
		size_t value = 0;
		for (auto ch : s)
		{
			value *= 31;
			value += ch;
		}
		return value;
	}
};

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

struct DJBHash
{
	size_t operator()(const string& s)
	{
		size_t hash = 5381;
		for (auto ch : s)
		{
			hash += (hash << 5) + ch;
		}
		return hash;
	}
};
struct JSHash
{
	size_t operator()(const string& s)
	{
		size_t hash = 1315423911;
		for (auto ch : s)
		{
			hash ^= ((hash << 5) + ch + (hash >> 2));
		}
		return hash;
	}
};

测试布隆过滤器的准确度和命中率

void TestBloomFilter()
{
	srand(time(0));
	const size_t N = 100000;
	BloomFilter<8 * 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(1234 + i));
	}
	for (auto& str : v1)
	{
		bf.set(str);
	}
	std::vector<std::string> v3;
	for (size_t i = 0;i < N;i++)
	{
		v3.push_back(url + std::to_string(99999+ i));
	}
	size_t n1 = 0;
	for (auto& str : v3)
	{
		if (bf.Test(str))
		{
			++n1;
		}
	}
	cout << "相似字符串误判率:" << (double)n1 / (double)N << endl;
	std::vector<std::string> v2;
	for (size_t i = 0; i < N; ++i)
	{
		string url = "zhihu.com";
		v2.push_back(url + to_string(rand()));
	}
	size_t n2 = 0;
	for (auto& str : v2)
	{
		if (bf.Test(str))
		{
			++n2;
		}
	}
	cout << "不相似字符串误判率:" << (double)n2 / (double)N << endl;
}

结果

相似字符串误判率:0.04026
不相似字符串误判率:0.02138

2.3布隆过滤器的删除

​ 如果使用位图结构,那么布隆过滤器是无法支持删除。因为如果只是单单将需要删除的元素对应的比特位修改为0; 那么可能会导致其他的元素,因为对应比特位被错误的修改为0而被“删除”。

在这里插入图片描述

​ 假如我们把上面的IP1删除,因为IP2对应的比特位和IP4对应的比特位有相同的,所以当IP1被删除后,IP2也会被认为“删除”。

在这里插入图片描述

一种支持删除的布隆过滤器的设计方式是:

将每个比特位扩展为一个小的计数器,当插入元素时,该哈希地址对应的计数器加1,删除的时候减1。当然该方法也无法解决哈希冲突导致的误判。

在这里插入图片描述

2.4布隆过滤器的优缺点

优点

  • 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无关
  • 哈希函数相互之间没有关系,方便硬件并行运算
  • 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
  • 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
  • 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
  • 使用同一组散列函数的布隆过滤器可以进行交、并、差运算【可以直接进行逻辑运算】

缺点

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

3.海量数据处理面试题

3.1哈希切割

给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?

思路:

在这里插入图片描述

3.2位图的应用
3.2.1题目一
给定100亿个整数,设计算法找到只出现一次的整数?

思路:因为只使用一个位图只能记录是否出现了该整数,不能判断出现了几次。所以我们考虑使用两个位图。

template<size_t N>
class doubitset
{
	//默认的构造函数够使用
	//设置参数函数
		void set(size_t x)
		{
			int in1 = _bs1.test(x);
			int in2 = _bs2.test(x);
			if (in1 == 0 && in2 == 0)
			{
				_bs2.set(x);
			}
			else if (in1 == 0 && in2 == 1)
			{
				_bs1.set(x);
				_bs2.reset(x);
			}
		}

		bool is_once(size_t x)
		{
			return _bs1.test(x) == 0 && _bs2.test(x) == 1;
		}
private:
	mybitset _bs1;
	mybitset _bs2;
};
1.3.2题目二
给两个文件,分别有100亿个整数。只有1G内存,如何找到两个文件的交集?

思路:

将两个文件读取到两个位图中,如何将两个位图取与

3.3布隆过滤器应用

给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法

近似算法:布隆过滤器

精确算法:哈希切割

4.哈希与加密

哈希与加密的内容下面博客介绍的十分详细,感兴趣的读者可以点击下面网址:

哈希(Hash)与加密(Encrypt)的基本原理、区别及工程应用 - T2噬菌体 - 博客园 (cnblogs.com)

5.一致性哈希算法

一致性哈希算法重要用于解决海量数据存储中,分布式数据缓存。

5.1哈希在分布式中的应用

哈希在分布式系统中得到了广泛的使用,涉及到集群部署,缓存Redis集群,数据库集群。

比如我们需要存放大量的用户信息【由于数据量过于庞大,需要将数据存放在不同的服务器中】,可以对用户ID取模得到哈希值,从而将用户信息分布在不同的服务器存储。

在这里插入图片描述

使用哈希出现的问题

使用哈希算法可以将用户数据缓存到不同的服务器。

但是当缓存服务器数量发生变化,几乎所有的缓存位置都会发生变化,大量的缓存在同一时间失效,从而导致整体系统压力过大而崩溃。

为了解决上面的问题,一致性哈希算法诞生。

5.2一致性哈希

​ 一致性哈希算法也是取模算法,上面是对服务器的数量取模。**而一致性哈希是对232取模**,哈希值的取值范围为(0~23-1),将整体的哈希值空间组织成一个虚拟的圆环。

在这里插入图片描述

​ 整个空间按顺时针方向组织,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、5、6……直到232-1**,也就是说0点左侧的第一个点代表232-1, 0和232-1在零点中方向重合,*我们把这个由232个点组成的圆环称为Hash环**。

​ 假设当前有四台服务器A,B,C,D位于圆环上,根据这四台服务器的IP地址或主机MAC作为关键字进行哈希计算,并对2^32取模,最后每台服务器一定都在Hash环上。

n=Hash(IP地址或者MAC)%2^32

在这里插入图片描述

​ 一致性Hash算法使用以下规则数据访问到相应服务器: 将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器

​ 假如考虑现在有四个对象Object A,Object B,Object C,Object D需要存储在服务器上,经过哈希映射,可以得到相应的存储位置。

在这里插入图片描述

**现在考虑在Hash圆上添加一台新的服务器Node X **

在这里插入图片描述

可以看到上图,原本存放在Node C的数据Object C,在添加一台服务器后应该存放在Node X上。

**一般的,在一致性哈希算法中,增加一台服务器影响的只是新增服务器与新增服务器前面一台服务器之间的数据【逆时针方向遇见的第一台服务器】。**这里影响的是Node X与Node B之间的数据。

一致性Hash算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。

5.3 Hash环中的数据倾斜问题

​ 一致性哈希算法中,如果服务器节点是数量过少,容易因为节点不均匀而导致数据倾斜问题。【大部分数据存储缓存在某一台或者几台服务上】。

在这里插入图片描述

​ 此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上,从而出现hash环偏斜的情况,当hash环偏斜以后,缓存往往会极度不均衡的分布在各服务器上,如果想要均衡的将缓存分布到2台服务器上,最好能让这2台服务器尽量多的、均匀的出现在hash环上,但是,真实的服务器资源只有2台,我们怎样凭空的让它们多起来呢?

5.4虚拟节点设置

​ 既然没有多余的真正的物理服务器节点,我们就只能将现有的物理节点通过虚拟的方法复制出来。这些由实际节点虚拟复制而来的节点被称为"虚拟节点",即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器IP或主机名的后面增加编号来实现。

​ 比如现在我们让每一台服务器产生三个Hash值,每台服务器包括真正的服务器节点一共产生三个Node节点。

在这里插入图片描述

​ 同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,就解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

影中人lx

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

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

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

打赏作者

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

抵扣说明:

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

余额充值