【数据结构】位图与布隆过滤器


在这里插入图片描述
在前面我们已经学习过了哈希表,但有些时候使用常规的哈希表并不能解决某些问题,我们需要对普通哈希表进行变种,例如:

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

对于该题目,有几种解法:

  1. 暴力求解:直接遍历查找,时间复杂度为O(N),太慢
  2. 排序+二分查找:时间复杂度为O(N*logN) + O(logN)

但是解法2是否可行呢?我们先算算40亿个数据⼤概需要多少内存。

  • 40亿个整型,就是4*40亿 = 160亿个字节。
  • 1G = 1024MB = 1024 * 1024KB = 1024 * 1024 *1024byte,1G大约10亿多字节。
  • 那40亿个整型就是16G内存,说明40亿个数是⽆法直接放到内存中的,只能放到硬盘⽂件中。⽽⼆分查找只能对内存数组中的有序数据进⾏查找,所以方法2不可行。
  1. 使用哈希的变种:位图。

数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使⽤⼀个⼆进制⽐特位来代表数据是否存在的信息,如果⼆进制⽐特位为1,代表存在,为0代表不存在。那么我们设计⼀个⽤位表⽰数据是否存在的数据结构,这个数据结构就叫位图。

我们来算一下使位图需要多少内存呢?

40亿个整数,每个比特位对应一个整数,也就需要40亿个比特位。一个字节是8个比特位,40亿 / 8 = 5亿字节;1MB大约是100万字节,那5亿字节大约就才500MB,能开1.25亿个int。

1. 位图

1.1 位图的设计与实现

位图本质是⼀个直接定址法的哈希表,每个整型值映射⼀个bit位,位图提供控制这个bit的相关接⼝。

在这里插入图片描述

由于C/C++没有对应位的类型,只能看int/char这样整形类型,我们再通过位运算去控制对应的⽐特位。

比如我们数据存到 vector< int >中,相当于每个int值映射对应的32个值,⽐如第⼀个整形映射0-31对应的位,第⼆个整形映射32-63对应的位,后⾯的以此类推,那么来了⼀个整形值x,i=x/32;j=x%32;计算出x映射的值在vector的第i个整形数据的第j位

解决给40亿个不重复的⽆符号整数,查找⼀个数据的问题,我们要给位图开232(无符号整型的最大值)个位,注意不能开40亿个位,因为映射是按⼤⼩映射的,我们要按数据⼤⼩范围开空间,范围是是0-232-1,所以需要开2^32个位。

在这里插入图片描述

下面我们就需要实现位图的插入与删除功能了

  1. 对于插入一个数据x时,我们首先要知道x应该对应在哪一个比特位上,然后将该比特位设置为1

在这里插入图片描述

		void set(size_t x)
		{
			size_t i = x / 32;//在第几个字节
			size_t j = x % 32;//在一个字节中的哪一个比特位上

			vt[i] |= (1 << j);//将"vt[i][j] 置为1"
		}
  1. 对于删除一个数据x时,我们首先要知道x应该对应在哪一个比特位上,然后将该比特位设置为0

在这里插入图片描述

		void reset(size_t x)
		{
			size_t i = x / 32;//在第几个字节
			size_t j = x % 32;//在一个字节中的哪一个比特位上

			vt[i] &= ~(1 << j);//将"vt[i][j] 置为0"
		}

下面来看一下效果

在这里插入图片描述

  1. 查找一个数在不在,我们首先要知道x应该对应在哪一个比特位上,然后看该比特位为0还是1

直接将该比特位与1相&,结果不是0就存在,否则不存在。

		bool test(size_t x)
		{
			if (x > N)
				return false;
			size_t i = x / 32;//在第几个字节
			size_t j = x % 32;//在一个字节中的哪一个比特位上

			return vt[i] & (1 << j);
		}

在这里插入图片描述

如果要开232个位的位图,我们需要这样写:

	bitset<-1> bs2;
	bitset<UINT_MAX> bs3;//一定是无符号整型
	bitset<0xffffffff> bs4;

1.2 C++库中的位图

可以看核心接⼝还是set/reset/和test,当然后⾯还实现了⼀些其他接⼝,如to_string将位图按位转成01字符串,再包括operator[]等⽀持像数组⼀样控制⼀个位的实现

在这里插入图片描述

1.3 位图相关考察题目

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

虽然是100亿个数,但是还是按范围开空间,所以还是开2^32个位,跟前⾯的题⽬是⼀样的。但是我们的位图只有一个比特位,无法记录整数的个数,所以这里我们可以使用两个位图,根据两位图中比特位是1还是0构成4种组合

在这里插入图片描述

	template<size_t N>
	class twoBitset
	{
	public:
		void set(size_t x)
		{
			size_t bit1 = bt1.test(x);
			size_t bit2 = bt2.test(x);
			if (!bit1 && !bit2)//00->01
			{
				bt1.set(x);
			}
			else if(bit1 && !bit2)//01->10
			{
				bt1.reset(x);
				bt2.set(x);
			}
			else if(!bit1 && bit2)//10->11
			{
				bt1.set(x);
			}
		}
		// 返回0 出现0次数
		// 返回1 出现1次数
		// 返回2 出现2次数
		// 返回3 出现2次及以上
		int getCount(size_t x)
		{
			size_t bit1 = bt1.test(x);
			size_t bit2 = bt2.test(x);
			if (!bit1 && !bit2)//00-->0
				return 0;
			else if (bit1 && !bit2)//01-->1
				return 1;
			else if (!bit1 && bit2)//10-->2
				return 2;
			else
				return 3;
		}
	private:
		//上面的位图
		bitset<N> bt1;
		bitset<N> bt2;
	};
  1. ⼀个⽂件有100亿个整数,1G内存,设计算法找到出现次数不超过2次的所有整数

这一题可以复用上一题的两个位图,只需找出出现一次和两次的数即可

在这里插入图片描述

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

把数据读出来,分别放到两个位图,依次遍历,同时在两个位图的值就是交集

在这里插入图片描述

位图的优缺点:

优点:增删查改快,节省空间
缺点:只适⽤于整形

2. 布隆过滤器

有⼀些场景下⾯,有⼤量数据需要判断是否存在,而这些数据不是整形,那么位图就不能使用了,使用红黑树/哈希表等内存空间可能不够。这些场景就需要布隆过滤器来解决。

2.1 什么是布隆过滤器

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

布隆过滤器的思路就是把key先映射转成哈希整型值,再映射⼀个位,如果只映射⼀个位的话,冲突率会⽐较多,所以可以通过多个哈希函数映射多个位,降低冲突率

布隆过滤器这⾥跟哈希表不⼀样,它⽆法解决哈希冲突的,因为他压根就不存储这个值,只标记映射的位。它的思路是尽可能降低哈希冲突。

判断⼀个值key 在 是不准确的,判断⼀个值key 不在 是准确的。

如下图中,使用多个哈希函数计算出来的key可能有相同的,但是只要多个哈希函数计算出来的值不全部都相同,就可以确定一个数据在不在。
在这里插入图片描述

布隆过滤器的误判率为:

m:布隆过滤器的bit⻓度。
n:插⼊过滤器的元素个数。
k:哈希函数的个数。
f ( k ) = ( 1 − 1 e k n m ) k f(k)= (1 - {1 \over e^{kn\over m}})^k f(k)=(1emkn1)k
由误判率公式可知,在k⼀定的情况下,当n增加时,误判率增加,m增加时,误判率减少(即m/n越大,误判率越低)。在m和n⼀定,在对误判率公式求导,误判率尽可能⼩的情况下,可以得到hash函数个数: k = m n l n 2 k ={m \over n} ln2 k=nmln2 时误判率最低

而且由于使用布隆过滤器大多是字符串,所以这里准备了各种字符串的哈希函数,可随意挑选

unsigned int SDBMHash(char *str)
{
    unsigned int hash = 0;
 
    while (*str)
    {
        // equivalent to: hash = 65599*hash + (*str++);
        hash = (*str++) + (hash << 6) + (hash << 16) - hash;
    }
 
    return (hash & 0x7FFFFFFF);
}
 
// RS Hash Function
unsigned int RSHash(char *str)
{
    unsigned int b = 378551;
    unsigned int a = 63689;
    unsigned int hash = 0;
 
    while (*str)
    {
        hash = hash * a + (*str++);
        a *= b;
    }
 
    return (hash & 0x7FFFFFFF);
}
 
// JS Hash Function
unsigned int JSHash(char *str)
{
    unsigned int hash = 1315423911;
 
    while (*str)
    {
        hash ^= ((hash << 5) + (*str++) + (hash >> 2));
    }
 
    return (hash & 0x7FFFFFFF);
}
 
// P. J. Weinberger Hash Function
unsigned int PJWHash(char *str)
{
    unsigned int BitsInUnignedInt = (unsigned int)(sizeof(unsigned int) * 8);
    unsigned int ThreeQuarters    = (unsigned int)((BitsInUnignedInt  * 3) / 4);
    unsigned int OneEighth        = (unsigned int)(BitsInUnignedInt / 8);
    unsigned int HighBits         = (unsigned int)(0xFFFFFFFF) << (BitsInUnignedInt - OneEighth);
    unsigned int hash             = 0;
    unsigned int test             = 0;
 
    while (*str)
    {
        hash = (hash << OneEighth) + (*str++);
        if ((test = hash & HighBits) != 0)
        {
            hash = ((hash ^ (test >> ThreeQuarters)) & (~HighBits));
        }
    }
 
    return (hash & 0x7FFFFFFF);
}
 
// ELF Hash Function
unsigned int ELFHash(char *str)
{
    unsigned int hash = 0;
    unsigned int x    = 0;
 
    while (*str)
    {
        hash = (hash << 4) + (*str++);
        if ((x = hash & 0xF0000000L) != 0)
        {
            hash ^= (x >> 24);
            hash &= ~x;
        }
    }
 
    return (hash & 0x7FFFFFFF);
}
 
// BKDR Hash Function
unsigned int BKDRHash(char *str)
{
    unsigned int seed = 131; // 31 131 1313 13131 131313 etc..
    unsigned int hash = 0;
 
    while (*str)
    {
        hash = hash * seed + (*str++);
    }
 
    return (hash & 0x7FFFFFFF);
}
 
// DJB Hash Function
unsigned int DJBHash(char *str)
{
    unsigned int hash = 5381;
 
    while (*str)
    {
        hash += (hash << 5) + (*str++);
    }
 
    return (hash & 0x7FFFFFFF);
}
 
// AP Hash Function
unsigned int APHash(char *str)
{
    unsigned int hash = 0;
    int i;
 
    for (i=0; *str; i++)
    {
        if ((i & 1) == 0)
        {
            hash ^= ((hash << 7) ^ (*str++) ^ (hash >> 3));
        }
        else
        {
            hash ^= (~((hash << 11) ^ (*str++) ^ (hash >> 5)));
        }
    }
 
    return (hash & 0x7FFFFFFF);
}

2.2 布隆过滤器的实现

实现也非常简单,通过K个哈希函数,计算出K个key,然后找去K个key在位图中位置,对比与设置即可。

#pragma once
#include"bitset.h"

namespace my2
{
	//几个字符串的哈希函数
    struct HashFuncBKDR
    {
		// @detail 本 算法由于在Brian Kernighan与Dennis Ritchie的《The CProgramming Language》
		// 一书被展示而得名,是一种简单快捷的hash算法,也是Java目前采用的字符串的Hash算法累乘因子为31。
        size_t operator()(const std::string & s)
        {
            size_t hash = 0;
            for (auto ch : s)
            {
                hash *= 31;
                hash += ch;
            }
            return hash;
        }
    };

	struct HashFuncAP
	{
		// 由Arash Partow发明的一种hash算法
		size_t operator()(const std::string& s)
		{
			size_t hash = 0;
			for (size_t 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 HashFuncDJB
	{
		// 由Daniel J. Bernstein教授发明的一种hash算法。 
		size_t operator()(const std::string& s)
		{
			size_t hash = 5381;
			for (auto ch : s)
			{
				hash = hash * 33 ^ ch;
			}

			return hash;
		}
	};
/
    template<size_t N,
        size_t X = 5, //M是N的X倍,M越大,误判率越低
        class K = std::string,
        class Hash1 = HashFuncBKDR,
        class Hash2 = HashFuncAP,
		class Hash3 = HashFuncDJB>
	class BloomFilter
	{
	public:
		void set(const K& key)
		{
			//计算每个哈希函数计算出的key在长度为M的位图中的位置
			size_t hash1 = Hash1()(key) % M;
			size_t hash2 = Hash2()(key) % M;
			size_t hash3 = Hash3()(key) % M;

			//在位图中映射 K 个值
			bt.set(hash1);
			bt.set(hash2);
			bt.set(hash3);
		}

		bool test(const K& key)
		{
			//判断不在一定准确
			size_t hash1 = Hash1()(key) % M;
			if (bt.test(hash1) == 0)
				return false;
			size_t hash2 = Hash2()(key) % M;

			if (bt.test(hash2) == 0)
				return false;
			size_t hash3 = Hash3()(key) % M;
			if (bt.test(hash3) == 0)
				return false;

			return true;//判断存在,可能会误判
		}
	private:
		//N是数据的个数,M是位图的大小,K是哈希函数个数
		static const size_t M = N * X;
		bitset<M> bt; //开N*X个数据的位图
	};
///
	void test1()
	{
		string strs[] = { "百度","字节","腾讯" };
		BloomFilter<10> bf;
		for (auto& s : strs)
		{
				bf.set(s);
		}
		for (auto& s : strs)
		{
				cout << bf.test(s) << endl;
		}
		for (auto& s : strs)
		{
				cout << bf.test(s + '1') << endl;
		}
		cout << bf.test("摆渡") << endl;
		cout << bf.test("百渡") << endl;
	}
}

在这里插入图片描述

  • 删除功能

在这里插入图片描述

布隆过滤器默认是不⽀持删除的,因为⽐如"猪⼋戒“和”孙悟空“都映射在布隆过滤器中,他们映射的位有⼀个位是共同映射的(冲突的),如果我们把孙悟空删掉,那么再去查找“猪⼋戒”会查找不到,因为那么“猪⼋戒”间接被删掉了。

2.3 布隆过滤器的应用

⾸先我们分析⼀下布隆过滤器的优缺点:

  • 优点:效率高,节省空间,相比位图,可以适用于各种类型的标记过滤
  • 缺点:存在误判(在是不准确的,不在是准确的),不好⽀持删除

布隆过滤器在实际中的⼀些应用:

  • 爬虫系统中URL去重:

在爬⾍系统中,为了避免重复爬取相同的URL造成死循环,可以使⽤布隆过滤器来进⾏URL去重。爬取到的URL可以通过布隆过滤器进⾏判断,已经存在的URL则可以直接忽略;不存在则存入布隆过滤器中,避免重复的⽹络请求和数据处理。

  • 预防缓存穿透

在分布式缓存系统中,布隆过滤器可以⽤来解决缓存穿透的问题。缓存穿透是指恶意⽤⼾请求⼀个不存在的数据,导致请求直接访问数据库,造成数据库压⼒过⼤。布隆过滤器可以先判断请求的数据是否存在于布隆过滤器中,如果不存在,直接返回不存在,避免对数据库的⽆效查询

  • 对数据库查询提效

在数据库中,布隆过滤器可以⽤来加速查询操作。例如:⼀个app要快速判断⼀个电话号码是否注册过,可以使用布隆过滤器来判断⼀个用户电话号码是否存在于表中,如果不存在,可以直接返回不存在,避免对数据库进行无用的查询操作。如果在,再去数据库查询进行二次确认。

评论 35
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值