【C++】哈希的应用:位图、哈希切分与布隆过滤器


目录

一、位图

1、位图的概念

2、大厂面试题

2.1位图应用(腾讯)

2.2位图应用

3、位图的优缺点

二、哈希切分

三、布隆过滤器

1、布隆过滤器的概念

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

3、布隆过滤器的删除

4、布隆过滤器的优缺点

5、布隆过滤器面试题

6、布隆过滤器的实现


一、位图

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、布隆过滤器的优缺点

优点:

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

缺点:

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

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;
}

猜你喜欢

转载自blog.csdn.net/gfdxx/article/details/129911476