【c++进阶】--哈希

1.unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 ,即最差情况下
需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次
数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑
树结构的关联式容器使用方式基本类似,只是其底层结构不同,本文中只对unordered_map和
unordered_set进行介绍,unordered_multimap和unordered_multiset学生可查看文档介绍

1.1unordered_map

unordered_map在线文档说明

  1. unordered_map是存储键值对的关联式容器,其允许通过keys快速的索引到与其对应的
    value。
  2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键
    和映射值的类型可能不同。
  3. 在内部,unordered_map没有对按照任何特定的顺序排序, 为了能在常数范围内找到key所
    对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
  4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率
    较低。
  5. unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
  6. 它的迭代器至少是前向迭代器。

1.1.2unordered_map的接口说明

1、unordered_map的构造

函数声明 功能介绍
unordered_map 构造不同格式的unordered_map对象

2、unordered_map的容量

函数声明 功能介绍
bool empty()const 监测unordered_map是否为空
size_t size() const 获取unordered_map的有效元素个数

3、unordered_map的迭代器

函数声明 功能介绍
begin 返回unordered_map第一个元素的迭代器
end 返回unordered_map最后一个元素下一个位置的迭代器
cbegin 返回unordered_map第一个元素的const迭代器
cend 返回unordered_map最后一个元素下一个位置的const迭代器

4、unordered_map的元素访问

函数声明 功能介绍
operator[] 返回与key对应的value,没有一个默认值

注意:该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶中插入,如果key不在哈希桶中,插入成功,返回V(),插入失败,说明key已经在哈希桶中,将key对应的value返
回。
5、unordered_map的查询

函数声明 功能介绍
iterator find(const K& key) 返回key在哈希桶中的位置
size_t count(const K& key) 返回哈希桶中关键码为key的键值对的个数

6、 unordered_map的修改操作

函数声明 功能介绍
insert 向容器中插入键值对
erase 删除容器中的键值对
void clear() 清空容器中有效元素个数
void swap(unordered_map&) 交换两个容器中的元素

7、unordered_map的桶操作

函数声明 功能介绍
size_t bucket count() const 返回哈希桶中桶的总个数
size_t bucket size(size_t n)const 返回n号桶中有效元素的总个数
size_t bucket(const K& key) 返回元素key所在的桶号

1.2 unordered_set

unordered_set 在线文档说明

2.底层结构

unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。

2.1 哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(log2N),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向结构中

  • 插入元素
    根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
  • 搜索元素
    对元素的关键码进行同样的计算,把求得的函数值当作元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。

该方式即为哈希(散列)方法,哈希方法中使用的转换函数为哈希(散列)函数,构造 出来的结构称为哈希表(Hash Table)(或者称散列表)

例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key%capacity;(capacity为存储元素底层空间总的大小)
【c++进阶】--哈希_第1张图片
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快。
问题:按照上述哈希方式,向集合中插入44,会出现什么问题?

2.2 哈希冲突

对于两个数据元素的关键字ki和kj(i != j),有ki != kj,但有:Hash(ki) == Hash(kj),即:不同关键字通过相同哈希函数计算出相同的哈希地址,这种现象称为哈希冲突或哈希碰撞。

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

发生哈希冲突该如何处理呢?

2.3 哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理。 哈希函数设计原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

常见哈希函数

  1. 直接定制法–(常用)
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况
    使用场景:适合查找比较小、连续、范围比较集中,每个数据分配一个位置。
    面试题:字符串中第一个只出现一次字符

  2. 除留余数法–(常用)
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
    使用场景:范围不集中,分布分散。

  3. 平方取中法–(了解)
    假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为
    4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合:不知
    道关键字的分布,而位数又不是很大的情况

  4. 折叠法–(了解)
    折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加
    求和,并按散列表表长,取后几位作为散列地址。
    折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况

  5. 随机数法–(了解)
    选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为
    随机数函数。
    通常应用于关键字长度不等时采用此法

  6. 数学分析法–(了解)
    设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能
    在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出
    现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。例如:
    【c++进阶】--哈希_第2张图片

    假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是 相同的,那么我
    们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还可以对抽取出来的数字
    进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改
    成12+34=46)等方法。
    数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布
    较均匀的情况

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

2.4 哈希冲突解决

解决哈希冲突两种常见的方法是:闭散列和开散列

2.4.1 闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那
么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?
1、线性探测
比如2.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,因此44理论
上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。同时由于一些相邻聚集位置连续冲突,可能形成“踩踏”。

  • 插入

··通过哈希函数获取待插入元素在哈希表中的位置
··如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探
测找到下一个空位置,插入新元素
【c++进阶】--哈希_第3张图片

  • 删除

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他
元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标
记的伪删除法来删除一个元素

#pragma once
#include
#include
#include
using namespace std;
enum State
{
	EMPTY,
	EXIST,
	DELETE,
};
template<class K, class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = EMPTY;
};
template<class K,class V>
class HashTable
{
public:
	bool Insert(const pair<K, V>& kv)
	{
		if(Find(kv.first))
		负载因子超过0.7就扩容
		//if (_table.size()==0 ||_n * 10 / _table.size() >= 7)
		//{
		//	1、表为空,扩不上去
		//	2、reserv扩容只扩capacity
		//	//_tables.reserve(_tables.capacity() * 2);


		//	/*size_t newsize = tables.size() == 0 ? 10 : _tables.size() * 2;
		//	_tables.resize(newsize);
		//	//size变了之后,查找的时候,hashi也会跟着变,导致以前的值找不到

		//	*/

		//	size_t newsize = tables.size() == 0 ? 10 : _tables.size() * 2;
		//	vector newtables(newsize);
		//	//遍历旧表,重新映射到新表
		//	for (auto& data : _tables)
		//	{
		//		if (data._state == EXIST)
		//		{
		//			//重新算在新表的位置
		//			size_t hashi = kv.first % _tables.size();
		//			//线性探测
		//			size_t i = 1;
		//			size_t index = hashi;
		//			while (_tables[hashi]._state == EXIST)
		//			{
		//				index == hashi + i;
		//				index %= _table.size();
		//				++i;
		//			}
		//			_tables[index]._kv = kv;
		//			_tables[index]._state = EXIST;
		//			_n++;
		//		}
		//	}
		//	_tables.swap(newtables);
		
		//负载因子超过0.7就扩容
		if (_tables.size()==0 ||_n * 10 / _tables.size() >= 7)
		{
			size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
			HashTable<K, V> newht;
			newht._tables.resize(newsize);

			// 遍历旧表,重新映射到新表
			for (auto& data : _tables)
			{
				if (data._state == EXIST)
				{
					newht.Insert(data._kv);
				}
			}
			_tables.swap(newht._tables);
		}
		size_t hashi = kv.first % _tables.size();
		//线性探测
		size_t i = 1;
		size_t index = hashi;
		while (_tables[hashi]._state == EXIST)
		{
			index ==hashi+i;
			index %= _tables.size();
			++i;
		}
		_tables[index]._kv = kv;
		_tables[index]._state = EXIST;
		_n++;
		return true;
	}
	HashData<K, V>* Find(const K& key)
	{
		if (_tables.size() == 0)
		{
			return nullptr;
		}
		size_t hashi = key % _tables.size();
		//线性探测
		size_t i = 1;
		size_t index = hashi;
		while (_tables[index]._state==EXIST&&_tables[index]._state != EMPTY)
		{
			if (_tables[index]._kv.first == key)
			{
				return &_tables[index];
			}
			index = hashi + i;
			index %= _tables.size();
			++i;
			//如果已经查找了一圈,说明全是EXIST或DELETE
			if (index == hashi)
				break;
		}
		return nullptr;
	}
	bool Erase(const K& key)
	{
		HashData<K, V>* ret = Find(key);
		if (ret)
		{
			ret->_state = DELETE;
			return true;
		}
		else
			return false;
	}
private:
	vector<HashData<K,V>> _tables;
	size_t _n = 0;//存储的数据个数
};

思考:哈希表什么情况下进行扩容?如何扩容?
【c++进阶】--哈希_第4张图片

2、二次探测
线性探测的缺陷是产生冲突的数据堆积在一块二,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:Hi=(H0+i2)%m,或者:Hi=(H0+i2)%m。其中:i=1,2,3……,H0是通过散列函数Hash(x)对元素的关键码key进行计算得到的位置,m是表的大小。对于上述插入44引起的哈希冲突,使用二次探测解决后的情况为。
【c++进阶】--哈希_第5张图片
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置
都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装
满的情况,但在插入时必须确保表的装载因子a不超过0.5
,如果超出必须考虑增容。
因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

2.4.2 开散列

1、开散列概念

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码
归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
【c++进阶】--哈希_第6张图片
【c++进阶】--哈希_第7张图片
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素

2、开散列的扩容

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。也就是

先来复习一下头插
【c++进阶】--哈希_第8张图片

所以我们不难得出
【c++进阶】--哈希_第9张图片
2、开散列的思考

  • 只能存储key为整形的元素,其他类型怎么解决?
//哈希函数采用处理余数法,被模的key必须为整形才可以处理,此处提供将key转化为整形的方法


template<class K,class V>
struct HashNode
{
	HashNode<K, V>* _next;
	pair<K, V> _kv;

	HashNode(const pair<K, V>& kv)
		:_next(nullptr)
		,_kv(kv)
	{}
};
//整数类型不需要转化
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return key;
	}
};
//特化,key为string时,转化为整形
template<>
struct HashFunc<string>
{
	//BKDR 避免字母相同但是顺序不同 (ab ba)
	size_t  operator ()(const string& s)
	{
		size_t hash = 0;
		for (auto ch : s)
		{
			hash += ch;
			hash *= 31;
		}
		return hash;
	}
};
template<class K, class V,class Hash = HashFunc<K>>
class HashTable
{
	typedef HashNode<K, V> Node;
public:
/*
	函数增删查改等其他函数
*/
private:
	vector<Node*> _tables;
	size_t _n = 0;//存储有效数据个数
};
  • 除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?
 size_t GetNextPrime(size_t prime)
   	{
   		//SGI
   		static const __stl_num_primes = 28;
   		static const unsigned long __stl_prime_list[__stl_num_primes] =
   		{
   			53ul, 97ul, 193ul, 389ul, 769ul,
   			1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
   			49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
   			1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
   			50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
   			1610612741ul, 3221225473ul, 4294967291ul
   		};
   		size_t i = 0;
   		for (; i < __stl_num_primes; i++)
   		{
   			if (__stl_prime_list[i] > prime)
   				return __stl_prime_list[i];
   		}
   		return __stl_prime_list[i];
   	}

开散列代码实现

namespace HashBacket
{
	template<class K,class V>
	struct HashNode
	{
		HashNode<K, V>* _next;
		pair<K, V> _kv;

		HashNode(const pair<K, V>& kv)
			:_next(nullptr)
			,_kv(kv)
		{}
	};
	template<class K>
	//默认仿函数,内置类型如int不用传参数直接调用
	struct HashFunc
	{
		size_t operator()(const K& key)
		{
			return key;
		}
	};
	//特化
	template<>
	struct HashFunc<string>
	{
		//BKDR
		size_t  operator ()(const string& s)
		{
			size_t hash = 0;
			for (auto ch : s)
			{
				hash += ch;
				hash *= 31;
			}
			return hash;
		}
	};
	template<class K, class V,class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		~HashTable()
		{
			for (auto& cur : _tables)
			{
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				cur = nullptr;
			}
		}
		bool Erase(const K& key)
		{
			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];

			while (cur)
			{
				if (cur->_kv.second == key)
				{
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;

					}
					delete cur;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}

		}
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
			{
				return false;
			}
			//负载因子==1时扩容
			if (_n == _tables.size())
			{
				/*size_t newsize = _table.size() == 0 ? 10 : J_tables.size() * 2;
				HashTable newht;
				newht.resize(newsize);
				for (auto cur : _tables)
				{
					while (cur)
					{
						newht.Insert(cur->_kv);
						cur = cur->_next;
					}
				}
				_tables.swap(newht._tables);*/

				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				vector<Node*>newtables(newsize, nullptr);
				for (auto& cur : _tables)
				{
					while (cur)
					{
						Node* next = cur->_next;
						Hash hash;
						size_t hashi = hash(kv.first) % _tables.size();
						//头插到新表
						cur->_next = newtables[hashi];
						newtables[hashi] = cur;

						cur = next;
					}
				}
				_tables.swap(newtables);
			}
			Hash hash;
			size_t hashi = hash(kv.first) % _tables.size();
			//头插
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;

			++_n;
			//return make_pair(iterator(newnode,this),false);
			return true;
		}
		Node* Find(const K& key)
		{
			if (_tables.size() == 0)
				return nullptr;
			Hash hash;

			size_t hashi =hash( key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}
	private:
		vector<Node*> _tables;
		size_t _n = 0;//存储有效数据个数
	};

}

3.模拟实现

4.哈希的应用

4.1 位图

4.1.1 位图的概念

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

  1. 遍历,时间复杂度O(N)
  2. 排序O(NlogN),利用二分查找
  3. 位图解决

数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0代表不存在。比如:
【c++进阶】--哈希_第10张图片
2、位图概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。

4.1.2位图的优缺点

优点:速度快、节省空间
缺点:只能映射整形,其他类型如:浮点数,string等不能存储映射

4.1.3 位图的实现

#pragma once
#include
#include
using namespace std;

template<size_t N>
class bitset
{
public: 
	bitset()
	{
		_bits.resize(N / 8+1, 0);
	}
	void set(size_t x)
	{
		//计算x映射在char数组的位置
		size_t i = x / 8;
		size_t j = x % 8;
		_bits[i] |= (1 << j);
	}
	void reset(size_t x)
	{
		//计算x映射在char数组的位置
		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:
	vector<char> _bits;
};
void test_bitset1()
{
	bitset<100> bs;
	bs.set(10);
	bs.set(11);
	bs.set(15);
	cout << bs.test(10) << endl;
	cout << bs.test(15) << endl;

	bs.reset(10);

	cout << bs.test(10) << endl;
	cout << bs.test(15) << endl;

	bs.reset(10);
	bs.reset(15);

	cout << bs.test(10) << endl;
	cout << bs.test(15) << endl;
}

4.2 布隆过滤器

4.2.1 布隆过滤器的提出

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

  1. 用哈希表存储用户记录,缺点:浪费空间
  2. 用位图存储用户记录,缺点:不能处理哈希冲突
  3. 将哈希与位图结合,即布隆过滤器

4.2.2 布隆过滤器概念

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

4.2.3 布隆过滤器的插入

【c++进阶】--哈希_第12张图片
向布隆过滤器中插入“baidu”
【c++进阶】--哈希_第13张图片

【c++进阶】--哈希_第14张图片

void set(const K& key)
	{
		size_t len = N * _X;
		size_t hash1 = Hash1()(key) % len;
		_bs.set(hash1);

		size_t hash2 = Hash2()(key) % len;
		_bs.set(hash2);

		size_t hash3 = Hash3()(key) % len;
		_bs.set(hash3);

		//cout << hash1 << " " << hash2 << " " << hash3 << endl;
	}

4.2.3 布隆过滤器的查找

** 注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判**
比如:在布隆过滤器中查找“alibaba”时,假设3个哈希函数计算的哈希值为:1,3,7,刚好和其他元素的比特位重叠,此时布隆过滤器告诉该元素不存在,但其实该元素是不存在的。

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

		size_t hash2 = Hash2()(key) % len;
		if (!_bs.test(hash2))
		{
			return false;
		}

		size_t hash3 = Hash3()(key) % len;
		if (!_bs.test(hash3))
		{
			return false;
		}
		return true;
		//在   --不准确的、存在误判、可能是别的数映射的
		//不在 --准确的、只要有一个不存在,就一定说明不存在
	}

4.2.5 布隆过滤器删除

布隆过滤器不能直接支持删除工作,因为再删除一个元素时,可能会影响其他元素
比如:删除上图中“tencent”元素,如果直接将该元素所对应的二进制比特位置0,“baidu”元素也被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。

一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删除操作。

缺陷:

  • 无法确认元素是否真的在布隆过滤器中
  • 存在计数回绕
    代码实现
struct BKDRHash
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto ch : s)
		{
			hash += ch;
			hash *= 31;
		}

		return hash;
	}
};

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

template<size_t N, class K = string,
	class Hash1 =BKDRHash ,
	class Hash2 = APHash,
	class Hash3 = DJBHash>
	class BloomFilter
{
public:
	void set(const K& key)
	{
		size_t len = N * _X;
		size_t hash1 = Hash1()(key) % len;
		_bs.set(hash1);

		size_t hash2 = Hash2()(key) % len;
		_bs.set(hash2);

		size_t hash3 = Hash3()(key) % len;
		_bs.set(hash3);

		//cout << hash1 << " " << hash2 << " " << hash3 << endl;
	}
	bool test(const K& key)
	{
		size_t len = N * _X;
		size_t hash1 = Hash1()(key) % len;
		if(!_bs.test(hash1))
		{
			return false;
		}

		size_t hash2 = Hash2()(key) % len;
		if (!_bs.test(hash2))
		{
			return false;
		}

		size_t hash3 = Hash3()(key) % len;
		if (!_bs.test(hash3))
		{
			return false;
		}
		return true;
		//在   --不准确的、存在误判、可能是别的数映射的
		//不在 --准确的、只要有一个不存在,就一定说明不存在
	}
private:
	static const size_t _X= 4;
	bitset<N * _X> _bs;

};

4.2.6 布隆过滤器优点

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

4.2.7 布隆过滤器缺陷

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

5.海量数据处理面试题

5.1 哈希切割

给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址? 与上题条件相同,
如何找到top K的IP?如何直接用Linux系统命令实现?

5.2 位图应用

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

解决思路:用两个单位图,用两个相同位置的比特位,来反映出现次数(00–0次,01–一次,10二次及以上)

template <size_t N>
class twobitset
{
public:
	void set(size_t x)
	{
		//00->01 
		if (_bs1.test(x) == false
			&& _bs2.test(x)== false)
		{
			_bs2.set(x);
		}
		//01->10
		else if (_bs1.test(x) == false
			&& _bs2.test(x) == true)
		{
			_bs1.set(x);
			_bs2.reset(x);
		}
		//10
	}
	void Print()
	{
		for (size_t i = 0; i < N; i++)
		{
			if (_bs2.test(i) == 1)
			{
				cout << i<<" ";
			}
		}
	}
private:
	bitset<N> _bs1;
	bitset<N> _bs2;
};
void test_twobitset1()
{
	int a[] = { 1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,0 };
	twobitset<1000> bs;
	for (auto e : a)
	{
		bs.set(e);
	}
	bs.Print();

}
  1. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
  2. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数

与上面位图应用1同理(00–零次,01一次,10两次,11–三次及以上)

template <size_t N>
class twobitset
{
public:
	void set(size_t x)
	{
		//00->01 
		if (_bs1.test(x) == false
			&& _bs2.test(x)== false)
		{
			_bs2.set(x);
		}
		//01->10
		else if (_bs1.test(x) == false
			&& _bs2.test(x) == true)
		{
			_bs1.set(x);
			_bs2.reset(x);
		}
		//10->11
		else if (_bs1.test(x) == true
			&& _bs2.test(x) == false)
		{
			_bs2.set(x);
		}
	}
	void Print()
	{
		for (size_t i = 0; i < N; i++)
		{
			if (_bs2.test(i) == 1)
			{
				cout << i<<" ";
			}
		}
	}
private:
	bitset<N> _bs1;
	bitset<N> _bs2;
};
void test_twobitset1()
{
	int a[] = { 1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,0 };
	twobitset<1000> bs;
	for (auto e : a)
	{
		bs.set(e);
	}
	bs.Print();

}

5.3 布隆过滤器

  1. 给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
    【c++进阶】--哈希_第15张图片
  2. 如何扩展BloomFilter使得它支持删除元素的操作
    通过引用计数,每次删除让对应映射的计数值–

你可能感兴趣的:(c++,哈希算法,java)