C++哈希应用之位图,哈希切分与布隆过滤器详解

2023-05-14 08:05:00 过滤器 切分 位图

一、位图

1、位图的概念

所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来标记某个数据在或不在,它解决不了哪个数据出现次数最多的问题。

2、大厂面试题

2.1位图应用

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

开一个位图,使用哈希的直接定址法,值是几,就把位图中的比特位标记成1,仅占用512M空间。

但我们不能按照比特位来开辟空间,所以使用char或int等内置类型进行空间的开辟:

仿写bitset:

#pragma once
#include <iOStream>
#include <vector>
namespace jly
{
	template <size_t N>
	class bitset
	{
	public:
		bitset()
		{
			_bits.resize(N / 8 + 1, 0);
		}
	public:
		void set(size_t x)//将某个比特位标记为1
		{
			size_t i = x / 8;//算出x位于哪个字节
			size_t j = x % 8;//算出x位于该字节的哪一位
			_bits[i] |= (1 << j);
		}
		void reset(size_t x)//将某个比特位标记为0
		{
			size_t i = x / 8;
			size_t j = x % 8;
			_bits[i] &= (~(1 << j));
		}
		bool test(size_t x)//测试这个值在不在位图中
		{
			size_t i = x / 8;
			size_t j = x % 8;
			return _bits[i] & (1 << j);
		}
	private:
		std::vector<char> _bits;
	};
	void test_bitset()
	{
		bitset<-1> bs;
	}
}

这是一种又快又省空间的办法,也是面试官最想听到的回答。

但个人认为如果将题目要求的40亿数字全部录入位图中,等于遍历了一遍40亿个数字,既然都遍历一遍原数据了,那还不如在遍历的时候直接比对呢,对吧,相比之下直接比对数据连512M的位图都不用开。

2.2位图应用

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

可以认为这里的整数的最大值为unsigned int的最大值,一个整数共有三种状态:00,01,02,分别代表不存在,出现一次,出现两次及以上,代码如下:

template <size_t N>
class twobitset
{
public:
	void set(size_t x)
	{
		if (_bs1.test(x))//01->10
		{
			_bs1.reset(x);
			_bs2.set(x);
		}
		else if (_bs1.test(x)==false&&_bs2.test(x)==false)//00->01
		{
			_bs1.set(x);
		}
	}
	void PrintOnce()
	{
		for (size_t i = 0; i < N; ++i)
		{
			if (_bs1.test(i) && !_bs2.test(i))
			{
				std::cout << i << std::endl;
			}
		}
	}
private:
	std::bitset<N> _bs1;
	std::bitset<N> _bs2;
};
void test()
{
	twobitset<100> tbs;
	int a[] = { 3,5,6,3,5,8,9,4,3,6,9,4 };
	for (auto& e : a)
	{
		tbs.set(e);
	}
	tbs.PrintOnce();//打印8
}

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

和第一问类似,开两个位图,分别将两组数据映射进位图,两个位图对应的比特位均为1即为交集。

3、位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数

同第一问,开两个位图,00代表不存在,01代表出现一次,10代表出现两次,11代表出现两次以上。

3、位图的优缺点

优点:节省空间,查找速度快

缺点:要求范围相对集中,范围特别分散的,空间消耗大;位图只对整型使用,浮点数、string等其他类型无法使用。

如果要判断其他类型,该类型如果可以使用哈希函数转为整型的,可以考虑下布隆过滤器哈(见下文布隆过滤器的介绍)。

二、哈希切分

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

如果ai冲突桶超过1G怎么办?

1、这个桶冲突的IP很多,大多都是不重复的,map统计不下;

2、这个桶冲突的IP很多,大多都是重复的,map可以统计;

直接使用map中的insert将每一个冲突桶的元素插入到map中。情况一:如果insert插入失败,说明空间不足,new节点失败,抛出异常。解决方法是换个哈希函数,递归再次对这个冲突桶进行切分。情况二:map可以正常统计。

三、布隆过滤器

1、布隆过滤器的概念

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

既然这种方法能判断某个元素一定不存在,那么如何降低“误判”(映射为1的概率)的概率,提升准确判定(映射为0的概率)的概率呢?

解决方法就是对同一个元素使用多组哈希函数进行映射,它能降低误判率,但是增加了空间消耗。使用时需要把控好布隆过滤器的哈希函数的个数布隆过滤器的长度。公式为k=(m/n)*lg2

【k:哈希函数的个数;m:布隆过滤器的长度;n:插入元素的个数】(选型可参照本文)

2、布隆过滤器的应用场景

1、不需要一定准确的场景,例如个人网站注册时候的昵称判重,使用布隆过滤器可以判断某个昵称一定没有被使用过,但会误判某些造成冲突但没有被使用的昵称。

2、提高效率。例如客户端查找信息时,先用布隆过滤器筛一下,如果不在,则直接将未查到的信息反馈给客户端;如果布隆过滤器发现查找信息与位图匹配,则将需要查找的信息推送给服务器中的数据库进行精确查找。

3、布隆过滤器的删除

单纯的布隆过滤器是不支持删除的,因为一个比特位可能被多个元素所映射。如果非要在布隆过滤器中实现reset,那就只能将位图结构修改为计数器结构。数据set时,每被映射一次,计数器加1,reset时,该位计数器-1,直到该位计数器为0。毫无疑问,这种操作所需的空间消耗急剧增加。

4、布隆过滤器的优缺点

优点:

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

缺点:

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

5、布隆过滤器面试题

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

近似算法:使用布隆过滤器,先将其中一个文件set进布隆过滤器中,再将另一个文件的数据进行比对,可以淘汰一定不是交集的那部分,不过余下的那部分数据中,仍会有非交集的存在。

精确算法:使用哈希切分,将两个大文件分别切成一个个小文件A0-A99,B0-B99(单个小文件超过1G参照上文哈希切分对于此问题的解决方法);因为使用的是相同的哈希函数,所以交集必定存在于A0和B0,A1和B1这种相同下标的小文件中。可以先将A0存放至哈希表中,B0去重后与哈希表比对,就能够精确得到交集。

6、布隆过滤器的实现

#pragma once
#include <iostream>
#include <bitset>
#include <string>
using namespace std;
struct BKDRHash
{
	size_t operator()(const std::string& key)
	{
		size_t hash = 0;
		for (auto& ch : key)
		{
			hash *= 131;
			hash += ch;
		}
		return hash;
	}
};
struct APHash
{
	size_t operator()(const std::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 std::string& key)
	{
		unsigned int hash = 5381;
 
		for (auto ch : key)
		{
			hash += (hash << 5) + ch;
		}
 
		return hash;
	}
};
 
struct jsHash
{
	size_t operator()(const std::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=5,class K=std::string,
class HashFunc1= BKDRHash,
class HashFunc2= APHash,
class HashFunc3= DJBHash>
class BloomFilter
{
public:
	void set(const K& key)
	{
		size_t hash1 = HashFunc1()(key) % (X * N);
		size_t hash2 = HashFunc2()(key) % (X * N);
		size_t hash3 = HashFunc3()(key) % (X * N);
		_bs.set(hash1);
		_bs.set(hash2);
		_bs.set(hash3);
	}
	bool test(const K& key)
	{
		size_t hash1 = HashFunc1()(key) % (X * N);
		size_t hash2 = HashFunc2()(key) % (X * N);
		size_t hash3 = HashFunc3()(key) % (X * N);
		return _bs.test(hash1) && _bs.test(hash2) && _bs.test(hash3);
	}
private:
	std::bitset<X*N> _bs;
};
void test_bloomfilter1()
{
	// 10:46继续
	string str[] = { "a", "s", "d", "w", "a1","1a","白1a","c11a","1a1" };
	BloomFilter<10> bf;
	for (auto& str : str)
	{
		bf.set(str);
	}
 
	for (auto& s : str)
	{
		cout << bf.test(s) << endl;
	}
	cout << endl;
 
	srand((unsigned int)time(0));
	for (const auto& s : str)
	{
		cout << bf.test(s + to_string(rand())) << endl;
	}
}
void test_bloomfilter2()
{
	srand((unsigned int)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;
}

到此这篇关于c++哈希应用之位图,哈希切分与布隆过滤器详解的文章就介绍到这了,更多相关C++哈希内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

相关文章