哈希_c++

  • unordered系列关联式容器
  • 底层结构-哈希
  • unordered_set,unordered_map模拟实现
  • 哈希的应用

一.unordered系列关联式容器

c++98,STL提供了底层为红黑树的关联式容器。比如map,set,multiset(允许键值冗余),multimap(允许键值冗余),但在查询时效率为logn,当树中节点非常多时,查询效果也不理想。因此在c++11,STL提供了unordered系列的4个关联式容器。unordered_set,unordered_map,unordered_multiset, unordered_multimap.这四个容器的底层结构是哈希。

1.unordered系列容器与map,set…
版本迭代器底层结构效率遍历元素
unordered系列c++11单向迭代器哈希一般高于下面的四个容器迭代器遍历元素无序
map,set,multiset,multimapc++98双向迭代器红黑树比unordered系列低迭代器遍历元素有序

2.接口

unordered系列关联式容器和map.set…的用法都差不多。
下面为查询库函数的链接:
https://legacy.cplusplus.com/reference/unordered_map/unordered_map/?kw=unordered_map
image.png

image.png

  • map存储的pair<Key, T>键值对,set存储的是Key,(但底层却是天差地别,模拟实现见)
  • Hash是将Key转换为可以取模的仿函数(为了便于哈希函数的计算)
  • Pred是比较相等的仿函数。(因为哈希中只需要比较是否相同即可)
unordered_map
//1.容量相关
size_t size() const;
bool empty() const;

//2.迭代器
iterator begin();
iterator end();
const_iterator begin() const;
const_iterator end() const;

//3.元素访问
T& operator[](const Key& key);

//4.元素查找
iterator find ( const key& k );
size_t count ( const key& k ) const;    //返回当前k有多少个
 //unordered_map中键值不能重复,因此最大为1,unordered_multimap可用

//5.修改元素
pair<iterator,bool> insert ( const pair<Key, T>& val );
    //插入成功,第二个参数为true,返回插入的元素的迭代器
    //插入失败,第二个参数为false,返回已经存在元素的迭代器
iterator erase ( const_iterator position );
void clear();    //清空容器
void swap ( unordered_map& ump );

//6.桶操作
size_t bucket_count()const;     //返回桶的个数
size_t bucket(const Key& k);       //返回元素k的桶号
size_t bucket_size(size_t n)const; //返回n号桶的有效元素个数

unordered_set

unordered_set库函数

二.底层结构-哈希

1.哈希概念

在顺序结构以及平衡树中,元素关键码和存储位置没有关系,因此在查找元素时,必须要进行多次的关键码比较,搜索的效率取决于元素的比较次数。
如果将元素的关键码和其存储位置建立关系,那么在查找元素时通过这种关系很快就能找到该元素。该方式就是哈希(散列)方法,哈希方法中关键码和存储位置的关系就是哈希(散列)函数,构造出来的存储结构就是哈希表(散列表)。

2.哈希函数

常见的哈希函数有:1.直接定址法。2.除留余数法 3.平方取中法 4.折叠法 5.随机数法 6.数学分析法。
(1).直接定址法
取关键字的某个线性函数为散列地址:Hash(key) = A*key + 8
优点:简单,均匀
缺点:需要元素集分布集中,且均匀
使用场景:查找比较小且连续数据的情况
例题:统计26个英文字母出现次数,建立一个大小为26的顺序表,依次对应a…z
(2).除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
上述两种方法的模拟实现:
在直接定址法中,数据与存储位置是一一对应的,所以不存在两个元素根据哈希函数求出同一存储位置的情况,但是除留余数法会存在这种情况。这种两个元素映射到同一位置的情况叫做哈希冲突(哈希碰撞)。

3.哈希冲突

解决哈希冲突的常用方法:闭散列和开散列。
(1)闭散列:空间利用率低
也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空呢?

  1. 线性探测法:从冲突位置开始依次向后探测,直到找到空位置即可。

插入:

  1. 通过哈希函数获取地址
  2. 如果该地址为空,则填入。不为空,则依次向后找空,插入新元素。

image.png
查找:

  1. 通过哈希函数获取地址
  2. 如果该地址不为空,则比较该元素,如果等于待查找元素,则返回位置,如果不相同,依次向后探测,直到为空。
  3. 这里查找不能全部遍历一遍,否则查询和顺序结构一样,失去了哈希的优势。

删除:

  1. 通过哈希函数获取地址
  2. 如果该地址不为空,则比较该元素,如果等于待删除元素,则删除该元素,如果不相同,依次向后探测,直到为空。
  3. 删除元素和空绝对不能一视同仁。否则在查找时,会出错。例如上述图片中,假设删除了5,并且把该位置置空,那么在查找44时,就会在5位置停止搜索,影响搜索的正确性。所以我们应该在每个位置,新增了一个变量,记录该位置是空,删除还是存在

哈希表在什么情况下需要扩容,怎么扩容?
image.png
线性探测的缺陷:
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。如何缓解呢?

  1. 二次探测法:

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法:
index = hashi + i^2;
image.png
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在
搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
闭散列解决冲突的方法有很多,只要保证插入的逻辑和查找,删除时的逻辑相同即可。

namespace openAddress
{
	enum Status
	{
		EMPTY,
		EXIST,
		DELETE
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		Status _status = EMPTY;

	};
	template<class K, class V>
	class hashTable
	{
	public:
		bool insert(const pair<K, V>& kv)
		{
			if (find(kv.first))
			{
				return false;
			}

			//载荷因子越大,空间利用率越高,查找效率越低,碰撞率越高
			//载荷因子越小,空间利用率越低,查找效率越高,碰撞率越低
			if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
			{
				//这里扩容必须新开一块空间,否则有可能会发生数据覆盖
				size_t newSize = _tables.size() == 0 ? 10 : 2 * _tables.size();
				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 index = hashi;
			size_t i = 1;
			while (_tables[index]._status == EXIST)
			{
				index = hashi + i;
                //index = hashi + i*i;    二次探测
				index %= _tables.size();
				++i;

			}

			_tables[index]._kv = kv;
			_tables[index]._status = 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 index = hashi;
			size_t i = 1;
			//不能遍历一遍,否则效率太低,应该走到空为止,但也有可能会死循环,全部都不为空
			while (_tables[index]._status != EMPTY)
			{
				if (_tables[index]._status == EXIST && _tables[index]._kv.first == key)
				{
					return &_tables[index];
				}

				index = hashi + i;
				index %= _tables.size();
				++i;

				//死循环
				if (index == hashi)
				{
					break;
				}

			}

			return nullptr;
		}

		bool erase(const K& key)
		{
			HashData<K, V>* cur = find(key);
			if (cur)
			{
				cur->_status = DELETE;
				--_n;
				return true;
			}

			return false;
		}
	private:
		vector<HashData<K, V>> _tables;
		int _n = 0;     //记录当前的有效元素个数
	};

}

(2)开散列:
开散列法又叫链地址法(开链法,哈希桶法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
image.png
开散列的扩容?
桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希
表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。

//哈希映射为整数
//如果是数值类型,直接转换为整数
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return key;
	}
};
//如果是string的话,模板全特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& str)
	{
		size_t hash = 0;
		//BKDR-映射,可以减少发生函数冲突的次数。
		for (auto& ch : str)
		{
			hash += ch;
            //每次可以×31,131...
			hash *= 31;
		}

		return hash;
	}
};

namespace HashBucket
{
	template<class T>
	struct HashNode
	{
		HashNode<T>* _next;
		T _data;

		HashNode(const T& data)
			:_data(data)
			,_next(nullptr)
		{}
	};

	template<class K, class V, class KOfV, class Hash>
	class hashTable
	{

		~hashTable()
		{
			for (auto& cur : _tables)
			{
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				cur = nullptr;
			}
		}
		bool insert(const V& data)
		{
			KOfV kov;
			if (find(kov(data)) != nullptr)
			{
				return false;
			}

			Hash hash;
			//载荷因子更新,平均每个桶只有一个元素
			if (_n == _tables.size())
			{
				//新建一个表
				vector<Node*> newTables;
				newTables.resize(_tables.size() == 0 ? 10 : 2*_tables.size());
				

				for (auto& cur : _tables)
				{
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = hash(kov(cur->_data)) % newTables.size();

						cur->_next = newTables[hashi];
						newTables[hashi] = cur;
						cur = next;
					}
				}
				_tables.swap(newTables);

			}


			size_t hashi = hash(kov(data)) % _tables.size();
			Node* newNode = new Node(data);
			newNode->_next = _tables[hashi];
			_tables[hashi] = newNode;
			++_n;
			return true;


		}

		Node* find(const K& key)
		{
			if (_tables.size() == 0)
			{
				return nullptr;
			}
			Hash hash;
			KOfV kov;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kov(cur->_data) == key)
				{
					return cur;
				}

				cur = cur->_next;
			}

			return nullptr;
		}

		bool erase(const K& key)
		{
			if (_tables.size() == 0)
			{
				return false;

			}
			Hash hash;
			KOfV kov;
			size_t hashi = hash(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kov(cur->_data) == key)
				{
					//如果是头节点
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;

					--_n;
					return true;
				}

				prev = cur;
				cur = cur->_next;
			}

			return false;
			
		}
	private:
		vector<Node*> _tables;
		size_t _n = 0;
	};

}

开散列的一些优化场景:

  1. 如果key是自定义类型,则需要有将自定义类型对象转换为可以取模的仿函数和比较相同的函数重载。比如string,为什么STL库中,可以直接用于string呢,因为由于string和int是最常用的,所以在STL内部,已经实现了基于string转换的仿函数。但如果是你自己定义的自定义类型,则需要你显示给出仿函数。
  2. 表的长度最好是素数。可以减少冲突的概率
  3. 即便已经通过负载因子控制每个桶的元素个数,但还有很小的概率会出现一个桶有很多元素的情况。那么这种情况怎么办呢?
    1. 通过减少负载因子,
      1. 负载因子越小,空间利用率越低,搜素效率越高,冲突概率越低
      2. 负载因子越大,空间利用率越高,搜素效率越低,冲突概率越高
    2. 规定,如果桶中元素个数超过一定值,改为挂一个红黑树,这样即便冲突元素多,但查找次数也会很低

(3).闭散列和开散列的比较
应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <=0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。

三.模拟实现

为了使unordered_set和unordered_map共用一份hash代码,所以在传参时,

  • 第二个参数为确定每个元素的类型
  • 第一个参数的作用是在find,erase中使用。
  • KOfV:因为V的类型不同,通过value取key的方式就不同,详细见unordered_map/set的实现
  • Hash:就是自定义类型转换为可以取模的仿函数
	template<class K, class V, class KOfV, class Hash>
	class hashTable;
//哈希映射为整数
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return key;
	}
};
//如果是string的话,模板全特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& str)
	{
		size_t hash = 0;
		//BKDR-映射,可以减少发生函数冲突的次数。
		for (auto& ch : str)
		{
			hash += ch;
			hash *= 31;
		}

		return hash;
	}
};

namespace HashBucket
{
	template<class T>
	struct HashNode
	{
		HashNode<T>* _next;
		T _data;

		HashNode(const T& data)
			:_data(data)
			,_next(nullptr)
		{}
	};
	template<class K, class V, class KOfV, class Hash>
	class hashTable;

	//迭代器需要什么,节点指针√      哈希表√     
	//先试一试传vector,传vector必须传指针,否则在hashTable内部修改_tables,会导致信息不匹配
	template<class K, class V, class Ref, class Ptr, class KOfV, class Hash>
	struct __HashIterator
	{
		typedef HashNode<V> Node;
		typedef __HashIterator<K, V, Ref, Ptr, KOfV, Hash> self;
		typedef __HashIterator<K, V, V&, V*, KOfV, Hash> Iterator;


		__HashIterator(Node* node, const hashTable<K, V, KOfV, Hash>* ht)
			:_node(node)
			, _ht(ht)
		{}
		//支持普通迭代器转换为const迭代器
		//如果是普通迭代器就是拷贝构造,拷贝构造参数必为引用,
		//如果是const迭代器就是普通构造
		__HashIterator(const Iterator& it)
			:_node(it._node)
			, _ht(it._ht)
		{}

		Ref operator*()
		{
			return _node->_data;
		}

		Ptr operator->()
		{
			return &_node->_data;
		}

		self& operator++()
		{
			//如果当前节点的下一个为空,则找下一个桶的第一个节点
			if (_node->_next)
			{
				_node = _node->_next;
				return *this;
			}
			else
			{
				Hash hash;
				KOfV kov;

				size_t hashi = hash(kov(_node->_data)) % _ht->_tables.size();
				++hashi;
				while (hashi < _ht->_tables.size())
				{

					if (_ht->_tables[hashi] == nullptr)
					{
						++hashi;
					}
					else
					{
						_node = _ht->_tables[hashi];

						return *this;
					}
				}

				if (hashi == _ht->_tables.size())
				{
					_node = nullptr;
				}

				return *this;
			}
		}

		bool operator!=(const self& s)
		{
			return _node != s._node;
		}


		Node* _node;
		const hashTable<K, V, KOfV, Hash>* _ht;

	};


	template<class K, class V, class KOfV, class Hash>
	class hashTable
	{
		template<class K, class V, class Ref, class Ptr, class KOfV, class Hash>
		friend struct __HashIterator;
		typedef HashNode<V> Node;
	public:
		typedef __HashIterator<K, V, V&, V*, KOfV, Hash> iterator;
		typedef __HashIterator<K, V, const V&, const V*, KOfV, Hash> const_iterator;
		
		iterator begin()
		{
			//找第一个桶的第一个节点

			for (auto& cur: _tables)
			{
				if (cur != nullptr)
				{
					return iterator(cur, this);
				}
			}

			return iterator(nullptr, this);
		}

		iterator end()
		{
			return iterator(nullptr, this);
		}
		const_iterator begin() const
		{
			//找第一个桶的第一个节点

			for (auto& cur : _tables)
			{
				if (cur != nullptr)
				{
					return const_iterator(cur, this);
				}
			}

			return const_iterator(nullptr, this);
		}

		const_iterator end() const
		{
			return const_iterator(nullptr, this);
		}


		~hashTable()
		{
			for (auto& cur : _tables)
			{
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				cur = nullptr;
			}
		}
		bool insert(const V& data)
		{
			KOfV kov;
			if (find(kov(data)) != nullptr)
			{
				return false;
			}

			Hash hash;
			//载荷因子更新,平均每个桶只有一个元素
			if (_n == _tables.size())
			{
				//新建一个表
				vector<Node*> newTables;
				newTables.resize(_tables.size() == 0 ? 10 : 2*_tables.size());
				

				for (auto& cur : _tables)
				{
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = hash(kov(cur->_data)) % newTables.size();

						cur->_next = newTables[hashi];
						newTables[hashi] = cur;
						cur = next;
					}
				}
				_tables.swap(newTables);

			}


			size_t hashi = hash(kov(data)) % _tables.size();
			Node* newNode = new Node(data);
			newNode->_next = _tables[hashi];
			_tables[hashi] = newNode;
			++_n;
			return true;


		}

		Node* find(const K& key)
		{
			if (_tables.size() == 0)
			{
				return nullptr;
			}
			Hash hash;
			KOfV kov;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kov(cur->_data) == key)
				{
					return cur;
				}

				cur = cur->_next;
			}

			return nullptr;
		}

		bool erase(const K& key)
		{
			if (_tables.size() == 0)
			{
				return false;

			}
			Hash hash;
			KOfV kov;
			size_t hashi = hash(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kov(cur->_data) == key)
				{
					//如果是头节点
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;

					--_n;
					return true;
				}

				prev = cur;
				cur = cur->_next;
			}

			return false;
			
		}
	private:
		vector<Node*> _tables;
		size_t _n = 0;
	};

}
#include"hash.h"

namespace zs
{
	template<class K, class Hash=HashFunc<K>>
	class set
	{
		struct SetKeyOfV
		{
			//这里是const还是?
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
    	//因为set中不允许修改迭代器,所以不管是普通迭代器还是const迭代器都封装为const迭代器就可以实现上述功能
		typedef typename HashBucket::hashTable<K, K, SetKeyOfV, Hash>::const_iterator iterator;
		typedef typename HashBucket::hashTable<K, K, SetKeyOfV, Hash>::const_iterator const_iterator;


		iterator begin()
		{ 
            //普通对象调用普通迭代器,需要有对应的构造函数才可以完成转为const迭代器
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}

		const_iterator begin() const
		{
			return _ht.begin();
		}

		const_iterator end() const
		{
			return _ht.end();
		}
		bool insert(const K& key)
		{
			return _ht.insert(key);
		}

		bool find(const K& key)
		{
			return _ht.find(key);
		}

		bool erase(const K& key)
		{
			return _ht.erase(key);
		}

	private:
		HashBucket::hashTable<K, K, SetKeyOfV, Hash> _ht;
	};

}
#include"hash.h"


namespace zs
{
	template<class K, class V, class Hash = HashFunc<K>>	
	class map
	{
		struct MapKeyOfV
		{
			//这里是const还是?
			const K& operator()(const pair<const K, V>& kv)
			{
				return kv.first;
			}
		};
	public:
    	//map中,可以修改迭代器,第一个元素不能修改,因为是constK,第二个元素可以修改
		typedef typename HashBucket::hashTable<K, pair<const K, V>, MapKeyOfV, Hash>::iterator iterator;
		typedef typename HashBucket::hashTable<K, pair<const K, V>, MapKeyOfV, Hash>::const_iterator const_iterator;


		iterator begin()
		{
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}

		const_iterator begin() const
		{
			return _ht.begin();
		}

		const_iterator end() const
		{
			return _ht.end();
		}
		bool insert(const pair<const K, V>& kv)
		{
			return _ht.insert(kv);
		}
		bool find(const K& key)
		{
			return _ht.find(key);
		}

		bool erase(const K& key)
		{
			return _ht.erase(key);
		}
		

	private:
		HashBucket::hashTable<K, pair<const K, V>, MapKeyOfV, Hash> _ht;
	};

}

四.哈希应用

1.位图

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

1.1例题:

给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
分析:
显然这40亿个整数不可能放到内存中,一个整数四个字节,40亿个整数就需要160字节,约等于16G,更别说还需要存放指针等等。这道题主要的目标是判断整数在不在的问题,只需要两种状态就可以判断,所以我们用一个bit位映射一位数字,即hash中的直接定址法,到底需要多少个bit呢,取决于题目中给定的值范围,而题目中没有明确给定范围,所以可能会出现整数最大值,故此我们共需要42亿多个bit位,也就是差不多512M字节即可存储全部整数。
image.png

1.2位图的优缺点

优点:

  • 速度快,节约内存

缺点:

  • 只能映射整数,不能映射字符串,浮点数等等,当然这个缺点可以用布隆过滤器解决。
1.3位图的模拟实现
//非类型模板参数,N即使范围,也就是整数个数
template<size_t N>
class bitset
{
public:
	bitset()
	{
		_bitset.resize(N/8+1, 0);
	}


	//将目标位置置1
	void set(const size_t& x)
	{
		size_t i = x / 8;
		size_t j = x % 8;

		_bitset[i] |= (1 << j);

	}

	//将目标位置置0
	void reset(const size_t& x)
	{
		size_t i = x / 8;
		size_t j = x % 8;

		_bitset[i] &= ~(1 << j);

	}


	bool test(const size_t& x)
	{
		size_t i = x / 8;
		size_t j = x % 8;

		return _bitset[i] & (1 << j);
	}
private:
	vector<char> _bitset;
};

1.4位图应用
  1. 快速查找某个数据是否在一个集合中
  2. 排序 + 去重
  3. 求两个集合的交集、并集等
  4. 操作系统中磁盘块标记
1.5位图应用例题
  1. 给定100亿个整数,设计算法找到只出现一次的整数?

分析:将两个位图封装起来,同一位的状态是00,说明出现0次;01,说明出现1次;10,说明出现1次以上

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

分析:

  • 法一:先将一个文件的数据读入一个位图,然后遍历第二个文件中的数据,判断是否为交集
  • 法二:分别将两个文件的数据读入两个位图(本质是去重),然后遍历位图中的数据,如果两个按位与的结果是0,说明不是交集,为1,则是交集
  1. 位图应用变形:1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整

分析:和例题1类型

2.布隆过滤器

由于位图只能映射整数数据,不能映射string等其他自定义类型。但是我们可以将string通过仿函数转换为整数,然后将这个整数映射到位图中即可解决这一问题。但是如果仅仅是用一个哈希函数来映射,出现冲突的概率很高,布隆提出了一种方法,可以有效的减少冲突的概率。

2.1 概念
 布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “**某样东西一定不存在或者可能存在**”,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式不仅可以提升查询效率,也可以节省大量的内存空间。<br />![image.png](https://cdn.nlark.com/yuque/0/2023/png/29626751/1690254164215-410c7ce2-95f2-4a7e-97b6-06eeb677f1cf.png#averageHue=%23eeecec&clientId=ua40b53ec-3c19-4&from=paste&height=242&id=u456b2aea&originHeight=303&originWidth=856&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=67155&status=done&style=none&taskId=u6b79e530-0eaf-425f-b133-9d5fc1a6528&title=&width=684.8)

2.2布隆过滤器的优缺点

优点:

  • 速度快,节省空间

缺点:

  • 判断不在一定准确,判断在可能不准确。
2.3布隆过滤器的模拟实现
#pragma once
#include"bitset.h"

#include<string>

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<class N, class Key, class Hash1 = BKDRHash, class Hash2 = APHash, class Hash3 = DJBHash>
class BloomFilter
{
public:
	//置1
	void set(const Key& key)
	{
		size_t len = N * _x;
		size_t hash1 = Hash1(key) % len;
		_bitset.set(hash1);

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

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

	}

	//布隆过滤器一般不用于删除,如果要想支持删除的话,添加多个位图,作为计数即可。

	//布隆过滤器判断不在是准确的,判断在是不准确的
	bool test(const Key& key)
	{
		size_t len = N * _x;
		size_t hash1 = Hash1(key) % len;
		if (!_bitset.test(hash1))
		{
			return false;
		}

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


		size_t hash3 = Hash3(key) % len;
		if (!_bitset.test(hash3))
		{
			return false;
		}


		return true;

	}
private:
	//_x是由一个Key类型映射为几个位置决定的
	static const size_t _x = 4;
	bitset< N * _x> _bitset;
};

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

分析:
image.png
假设一个query占用50字节, 那么100亿个query占用5000亿字节,即500G。此时可以将该500G的文件,分割成1000份的小文件,这1000份小文件就可以用unordered_set/set进行处理了。
那么如何切割这个文件呢?平均切割的话,效率太低了。因为A0要在B0-B999中都找一遍。
这里引入了一种切割方法:哈希切割,i = hash(query)%1000,将文件A中的query根据hash函数分到不同的文件中,只要文件A和B的哈希函数一致,在A和B中相同的query就会进入相同下标的文件中,这时只要A0和B0比较,A1和B1比较…这样就可以提高效率。
但这样会出现一个bug,就是我们无法控制每个小文件的大小,有可能一个冲突较多导致占用几个G。这里具体分析引发这种冲突:

  • 第一种可能就是因为相同元素较多
  • 第二种可能就是不同元素但是冲突较多。

怎么解决这一bug呢?我们直接将该小文件中的数据放入unordered_set/set即可,如果可以放下,即就是出现情况1,如果在插入过程中报bad_alloc错误,说明出现了情况2,此时需要换hash函数继续切割这一小文件。

  1. 如何扩展BloomFilter使得它支持删除元素的操作

分析:设置引用计数,但是会出现计数环绕的问题

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

分析:
情况与例题1类型。先哈希切分文件,然后每个文件放入unordered_map/map中,记录出现次数最多的数据,每次更新max_count,即可。top K,需要建立K个数的小堆,只要比堆顶的次数大,就进堆。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值