22.位图和布隆过滤

1 位图(哈希的应用)

1.1 位图概念

1.面试题

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

对于这道面试题

我可以想出来的解决方法有

1.排序+二分查找

2.使用红黑树

3.使用哈希表

但是我们仔细的思考一下:

40亿个无符号的整数,要占据多少内存呢?

经过计算,大约需要16G的内存,在可以存储这么多的整数。因此使用我上述想出来的解决方案,一般的电脑是无法满足运行条件的

解决方案4:位图解决

​ 数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。

如下所示:

image-20230411160935406

  1. 位图概念

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

1.2 位图的实现

// 位图的类模板
template<size_t N>
class qwyset
{
public:
    // 位图的构造函数
	qwyset()
	{
        // 初始化确定大小为N的整数,需要开辟几个字节
        // N/8就可以确定开辟char的数量,char占1字节
        // 但是此时我们还需要注意一个问题,例如整数16/8 == 2,20/8 == 2,
        // 16对应的数组的下标为2,也就是_bits[i],对应的是第3个char
        // 20对应的数组的下标为2,也就是_bits[i],对应的是第3个char
        // 但是如果只开辟两个字节,我们就找不到16和20对应的bit位
        // 因此N除以8之后,需要再加1,开辟N/8+1个字节
        
        // 使用(N >> 3)一定注意要加括号,因为 + 的优先级高于 >>
		// _bits.resize(N/8+1, 0);
        // 将N右移一位,代表N除以2,右移3位就是除以8
        // 在vector类型的数组上面开辟(N >> 3) + 1个字节的空间,并将其初始化为0
		_qwys.resize((N >> 3) + 1, 0);
	}

    
    // set()的作用就是将x对应第i个字节的第j个bit位,从0标记为1
	void set(size_t x)
	{
        // 将x右移3位就相当于 x/8
        // i就是x对应的char的下标(也就是x属于第i个字节)
        // j就是x对应的char中bit位的下标(也就是x属于第i个字节的第j个bit位)
		size_t i = x >> 3;
		size_t j = x % 8;

        // _qwys[i]就是x对应的第i个字节的值
        // |(按位或) 有1,则按位或之后,为1(bit位进行按位或)
        // 将1左移j位,_bits[i] 按位或等上(1 << j) 就会将_qwys[i]的第i个字节的第j个bit位变为1
		_bits[i] |= (1 << j);
	}

    
    // reset()的作用就是将x对应的第i个字节的第j个bit位,从1标记为0
	void reset(size_t x)
	{
        // 通过一下两行代码,找到x映射的char对应的下标i,和在char中的bit位的下标j
		size_t i = x >> 3;
		size_t j = x % 8;

        // &(按位与) 对应的bit位都为1,则按位与之后为1,只要存在0则按位与后为0
        // (1 << j) 将1左移j为,则只有下标为j的bit位为1,取反之后(~(1 << j)),则只剩下下标为j的bit位为0
        // _qwys[i] 按位与等 (~(1 << j))之后,则_qwys[i]的下标为j的bit位必然为0
		_bits[i] &= (~(1 << j));
	}

    
    // test()的作用就是测试x对应的第i个字节的第j个bit位是否为1
    // 如果为1,则返回真,如果为0,则返回假
    // 也就是测试x是否被映射了
	bool test(size_t x)
	{
		size_t i = x >> 3;
		size_t j = x % 8;

        // (1 << j) 将1左移j位,则此时只有第j位的bit为1,其他位都为0
        // _qwys[i] 按位与 1 << j,则除了第j位,其他位与完后都为0
        // 所以如果x被映射了,那么_bits[i] 的第j位为1,按位与 (1 << j),得到一个非0的整数
        // 如果x没有被映射,那么_bits[i] 的第j位为0,按位与 (1 << j),得到整数0,被返回
		return _bits[i] & (1 << j);
	}

private:
	vector<char> _qwys;
};
  • 测试
void test_qwyset()
{
    // 传递100,就是开辟可以映射100以内所有数字的空间
	// qwyset<100> bs1;
    // 因为接收参数的N是一个无符号整型,因此传递-1,就代表32位下,unsigned int可以表示的最大正整数
	// qwyset<-1> bs2;
    
    // 0xffffffff也是 unsigned  int可以表示的最大值
	qwyset<0xffffffff> bs2;

	bs2.set(10);
	bs2.set(10000);
	bs2.set(8888);

	cout << bs2.test(10) << endl;
	cout << bs2.test(10000) << endl;
	cout << bs2.test(8888) << endl;
	cout << bs2.test(8887) << endl;
	cout << bs2.test(9999) << endl << endl;

	bs2.reset(8888);
	bs2.set(8887);

	cout << bs2.test(10) << endl;
	cout << bs2.test(10000) << endl;
	cout << bs2.test(8888) << endl;
	cout << bs2.test(8887) << endl;
	cout << bs2.test(9999) << endl;
}

1.3位图应用

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

image-20230411175820671

如上图所示:
我们可以使用两个bit位来映射一个整数数,这样的话,不仅可以反映出整数有没有被映射,而且能够反映出这个整数出现了几次。

  • ​ 我们使用 00 表示这个整数出现了0次

  • ​ 我们使用 01 表示这个整数出现了1次

  • ​ 我们使用 10 表示这个整数出现了1次以上

  • ​ 两个bit位,如果放入同一个vector中,这样操作其实是比较困难的,

  • ​ 但是如果整数映射的两个bit分别在对应的两个vector中,那么映射的逻辑与qwyset基本是一致的,

    接下来我们来实现twoqwyset

1.4位图的实现(twobitset)

// 注:100亿个整数,这些整数的范围是在0~2^32内, 因此是存在大量的重复值的
template<size_t N>
class twoqwyset
{
public:
    // 对x进行映射
	void set(size_t x)
	{
        // 当_bs1.test(x) 和 _bs2.test(x) 都为假时
        // 说明x在两个vector中对应的bit位都没有映射,所以x对应的两个bit位为00
		if (!_bs1.test(x) && !_bs2.test(x)) // 00
		{
            // 此时,是对x的第一次映射
            // 因此要给_bs2对应的底位的bit位,进行映射
            // 映射之后,此时x对应的两个bit位就变为了01
			_bs2.set(x); // 01
		}
		else if (!_bs1.test(x) && _bs2.test(x)) // 01
		{
            // 当_bs1.test(x)为假,并且_bs2.test(x)为真,
            // 则说明x映射的两个bit位为01,也就是说x已经出现了一次
            // 如果再次对x映射,就需要将01变为10
            // 那么就需要在_bs1中对x进行映射,将x对应的bit位从0变为1
            // 在_bs2中,将x对应的bit位从1变为0,也就是使用reset
			_bs1.set(x); 
			_bs2.reset(x); // 10
		}

        // 其他情况,都属于x被映射一次及一次以上,都标记为10
		// 10 不变
	}

    
    // 对只出现一次的整数进行打印
	void PirntOnce()
	{
		for (size_t i = 0; i < N; ++i)
		{
            // 当_bs1.test(i)为假,_bs2.test(i)为真
            // 说明i对应的两个bit位为01
            // 也就是i只出现了一次,所以对i进行打印
			if (!_bs1.test(i) && _bs2.test(i))
			{
				cout << i << endl;
			}
		}
		cout << endl;
	}

private:
    // qwyset是库里面有的类型(可以使用我们自己实现的,也可以使用c++库中的)
	qwyset<N> _bs1;
	qwyset<N> _bs2;
};

void test_twoqwyset()
{
	twoqwyset<100> tbs;
	int a[] = { 3, 5, 6, 7, 8, 9, 33, 55, 67, 3, 3, 3, 5, 9, 33 };
	for (auto e : a)
	{
		tbs.set(e);
	}

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

解题思路:

  • ​ 两个文件都有100亿个整数,那么我们将两个文件分别放入两个位图_bs1_ bs2,
  • ​ 这个的话,我们就完成了两个文件中整数的去重,
  • ​ 我们再将两个位图_bs1_bs2对应的字节进行按位与,即_bs1[i] &= _bs2[i]
  • ​ 进行按位与之后,在位图_bs1中的bit为1对应的整数,就是两个文件的交集
  1. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数

解题思路:
我们可以使用两个bit位来映射一个整数数,这样的话,不仅可以反映出整数有没有被映射,而且能够反映出这个整数出现了几次。

  • ​ 我们使用 00 表示这个整数出现了0次
  • ​ 我们使用 01 表示这个整数出现了1次
  • ​ 我们使用 10 表示这个整数出现了2次
  • ​ 我们使用 10 表示这个整数出现了3次及3次以上

1.5哈希切割

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

image-20230411211652048

2 布隆过滤器

2.1 布隆过滤器提出

我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。 如何快速查找呢?

用哈希表存储用户记录,缺点:浪费空间

  1. 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理了。
  2. 将哈希与位图结合,即布隆过滤器

2.2布隆过滤器概念

1.最开始的布隆过滤器

将哈希与位图结合,即布隆过滤器

如下图的3个字符串,使用哈希函数来确定 i = hashFunc(str),这样就可以将不同的字符串映射到不同的bit位当中

但是这种方法存在误判

1.判断一个字符串在是不准确的:因为这个字符串可能本来不存在,但是这个位置和其他字符串的的位置冲突了,这样的话,它自己虽然

不在,但是如果别人在,那么对应的bit位依旧为1,则这样也会误判它也存在。

2.不在是准确的:因为一个字符串映射的bit位为0,则这个字符串是一定不存在的。

image-20230411215748329

2.布隆过滤器的改良

布隆过滤器经过改良之后,降低了其误判率

image-20230411220615558

2.3布隆过滤器的应用场景

布隆过滤器的应用场景,也就是不需要一定准确的场景

​ 1.注册时的昵称判重

​ 2.判断网址是否在黑名单上

​ 3.如下图所示

image-20230411221151904

2.4布隆过滤器的实现

哈希函数的个数与布隆过滤器的长度

  • 根据上面这篇文章我们可以得出这样一个公式: k = m/n*ln2
  • 其中,k为哈希函数的个数,m为布隆过滤器的长度,n为插入的元素个数,ln2 约等于 0.7
  • 因此,我们可以得出 m = k*n/0.7
  • 假设 k==3, 则 m == 4.2n
  • 假设 k==4, 则 m == 5.7n

4个哈希函数

struct BKDRHash
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto ch : key)
		{
			hash *= 131;
			hash += ch;
		}
		return hash;
	}
};

struct APHash
{
	size_t operator()(const string& key)
	{
		unsigned int hash = 0;
		int i = 0;

		for (auto ch : key)
		{
			if ((i & 1) == 0)
			{
				hash ^= ((hash << 7) ^ (ch) ^ (hash >> 3));
			}
			else
			{
				hash ^= (~((hash << 11) ^ (ch) ^ (hash >> 5)));
			}

			++i;
		}

		return hash;
	}
};

struct DJBHash
{
	size_t operator()(const string& key)
	{
		unsigned int hash = 5381;

		for (auto ch : key)
		{
			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;
	}
};

布隆过滤器的实现

  • 假设最多存储n个数据
  • 平均存储一个值,开辟X
  • 因此我们使用了4个哈希函数,则根据公式 m = k*n/0.7 = 4/0.7*n = 5.7n,因此当n==1时,我们取m==6,也就是当存储的数据个数为1时,x为6
  • 因为布隆过滤器传递的大部分的类型都为string,因此我们给到class K = string缺省值,后续用到其他类型,我们也可以自己进行传参
// 布隆过滤器的类模板
template<size_t N,

// 存储一个值,那么就开辟6
size_t X = 6,

// 类型缺省为string
class K = string,

// 4个缺省的hash函数
class HashFunc1 = BKDRHash,
class HashFunc2 = APHash,
class HashFunc3 = DJBHash,
class HashFunc4 = JSHash>
class BloomFilter
{
public:
    // 将key映射到位图中
	void set(const K& key)
	{
        // 使用四种哈希函数,计算出对应key的不同的hashi的值
        // 这样就可以将key映射到_bs的4个bit位中
        // N为插入数据的个数,X为插入一个数据我们所要开辟的空间
        // 因此布隆过滤器的长度 m = N*x
        
        // HashFunc1()(key)(模)% 布隆过滤器的长度 就是key映射的4个位置
        // 也就可以得到4个hashi
		size_t hash1 = HashFunc1()(key) % (N*X);
		size_t hash2 = HashFunc2()(key) % (N*X);
		size_t hash3 = HashFunc3()(key) % (N*X);
		size_t hash4 = HashFunc4()(key) % (N*X);

        // 将对应的hashi的bit位从0标记为1
		_bs.set(hash1);
		_bs.set(hash2);
		_bs.set(hash3);
		_bs.set(hash4);
	}

    // 测试key是否已经被映射
	bool test(const K& key)
	{
        // 以下四个hashi对应位置的bit位的标记如果不为1
        // 则说明key并没有被映射到_bs中
        // 因此只要_bs.test(hashi)为假,则返回false
		size_t hash1 = HashFunc1()(key) % (N*X);
		if (!_bs.test(hash1))
		{
			return false;
		}

		size_t hash2 = HashFunc2()(key) % (N*X);
		if (!_bs.test(hash2))
		{
			return false;
		}

		size_t hash3 = HashFunc3()(key) % (N*X);
		if (!_bs.test(hash3))
		{
			return false;
		}

		size_t hash4 = HashFunc4()(key) % (N*X);
		if (!_bs.test(hash4))
		{
			return false;
		}

		// 前面判断不在都是准确,不存在误判
		return true; // 可能存在误判,映射几个位置都冲突,就会误判
	}

private:
	std::qwyset<N*X> _bs;
};

测试

  • 测试1
void test_bloomfilter1()
{
	string str[] = { "猪八戒", "孙悟空", "沙悟净", "唐三藏", "白龙马1","1白龙马","白1龙马","白11龙马","1白龙马1" };
	BloomFilter<10> bf;
    
    // 将string对象的数据进行映射
	for (auto& str : str)
	{
		bf.set(str);
	}

    // 测试,string对象的数据是否被映射
	for (auto& s : str)
	{
		cout << bf.test(s) << endl;
	}
	cout << endl;

	// 测试string对象的数据+随机值后,会不会被误判
	srand(time(0));
	for (const auto& s : str)
	{
		cout << bf.test(s + to_string(rand())) << endl;
	}
}
  • 测试2
void test_bloomfilter2()
{
	srand(time(0));
	const size_t N = 100000;
    
    // 初始化为存储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)
	{
        // 将改造后的字符串(url + std::to_string(i))插入到v1中
		v1.push_back(url + std::to_string(i));
	}

    // 将字符串(url + std::to_string(i))映射到bf中
	for (auto& str : v1)
	{
		bf.set(str);
	}

	// v2跟v1是相似字符串集,但是不一样
	std::vector<std::string> v2;
	for (size_t i = 0; i < N; ++i)
	{
        // 将(url += std::to_string(999999 + i))插入到v2中
		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)
	{
        // (bf.test(str))如果为真,则证明bf存在误判,误判了v2中的相似于v1的字符串映射到了bf中
		if (bf.test(str))
		{
            // 每误判一个字符串就++呢n2,因此n2代表的就是误判的字符串的个数
			++n2;
		}
	}
	cout << "相似字符串误判率:" << (double)n2 / (double)N << endl;

	// v3和v1是不相似字符串集
	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;
}

完整实现

#pragma once
#include <bitset>

namespace qwy
{
struct BKDRHash
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto ch : key)
		{
			hash *= 131;
			hash += ch;
		}
		return hash;
	}
};

struct APHash
{
	size_t operator()(const string& key)
	{
		unsigned int hash = 0;
		int i = 0;

		for (auto ch : key)
		{
			if ((i & 1) == 0)
			{
				hash ^= ((hash << 7) ^ (ch) ^ (hash >> 3));
			}
			else
			{
				hash ^= (~((hash << 11) ^ (ch) ^ (hash >> 5)));
			}

			++i;
		}

		return hash;
	}
};

struct DJBHash
{
	size_t operator()(const string& key)
	{
		unsigned int hash = 5381;

		for (auto ch : key)
		{
			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;
	}
};

// 假设N是最多存储的数据个数
// 平均存储一个值,开辟X
template<size_t N,
size_t X = 6,
class K = string,
class HashFunc1 = BKDRHash,
class HashFunc2 = APHash,
class HashFunc3 = DJBHash,
class HashFunc4 = JSHash>
class BloomFilter
{
public:
	void set(const K& key)
	{
		size_t hash1 = HashFunc1()(key) % (N*X);
		size_t hash2 = HashFunc2()(key) % (N*X);
		size_t hash3 = HashFunc3()(key) % (N*X);
		size_t hash4 = HashFunc4()(key) % (N*X);

		_bs.set(hash1);
		_bs.set(hash2);
		_bs.set(hash3);
		_bs.set(hash4);
	}

	bool test(const K& key)
	{
		size_t hash1 = HashFunc1()(key) % (N*X);
		if (!_bs.test(hash1))
		{
			return false;
		}

		size_t hash2 = HashFunc2()(key) % (N*X);
		if (!_bs.test(hash2))
		{
			return false;
		}

		size_t hash3 = HashFunc3()(key) % (N*X);
		if (!_bs.test(hash3))
		{
			return false;
		}

		size_t hash4 = HashFunc4()(key) % (N*X);
		if (!_bs.test(hash4))
		{
			return false;
		}

		// 前面判断不在都是准确,不存在误判
		return true; // 可能存在误判,映射几个位置都冲突,就会误判
	}

private:
	std::bitset<N*X> _bs;
};

	void test_bloomfilter1()
	{
		string str[] = { "猪八戒", "孙悟空", "沙悟净", "唐三藏", "白龙马1","1白龙马","白1龙马","白11龙马","1白龙马1" };
		BloomFilter<10> bf;
		for (auto& str : str)
		{
			bf.set(str);
		}

		for (auto& s : str)
		{
			cout << bf.test(s) << endl;
		}
		cout << endl;

		srand(time(0));
		for (const auto& s : str)
		{
			cout << bf.test(s + to_string(rand())) << endl;
		}
	}

	void test_bloomfilter2()
	{
		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;
	}
}

2.5 布隆过滤器的应用

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

image-20230412101108207

  • 24
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
位图(Bitmap)和布隆过滤器(Bloom Filter)都是常用的数据结构,用于处理大规模数据集合,但它们有着不同的应用场景和用途。 位图是一种压缩数据结构,用于快速地判断某个元素是否在集合中。位图的实现方式是将每个元素映射到一个二进制位上,如果该元素存在于集合中,则将对应的二进制位标记为1,否则标记为0。这样,当需要查询某个元素是否在集合中时,只需要查找对应的二进制位即可。由于位图的实现方式非常简单,因此可以快速地进行插入和查询操作,而且占用的空间也非常小,适合处理大规模数据集合。 布隆过滤器也是一种快速判断元素是否存在于集合中的数据结构,但其实现方式与位图略有不同。布隆过滤器使用一组哈希函数将元素映射到多个二进制位上,并将对应的二进制位标记为1。当查询某个元素是否在集合中时,将该元素进行哈希映射,并查找对应的二进制位,如果所有的二进制位都被标记为1,则说明该元素可能存在于集合中,否则可以确定该元素不存在于集合中。布隆过滤器的优点是可以快速地判断一个元素不存在于集合中,而且占用的空间也比较小,但存在误判率的问题。 因此,位图布隆过滤器虽然都可以用来处理大规模数据集合,但它们的实现方式和应用场景有所不同。位图适用于需要快速地判断某个元素是否在集合中的场景,而布隆过滤器适用于需要快速地判断一个元素不存在于集合中的场景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值