哈希思想及其应用

目录

1.unordered系列容器

1.1简介

1.2接口 (只对效果进行描述,具体可以自行参考文档)

1.2.1构造

1.2.2容量

1.2.3迭代器

1.2.4访问

1.2.5查询

1.2.6修改

1.2.7桶操作

2.哈希 

2.1概念

2.2哈希冲突

2.2.1闭散列

2.2.2开散列

2.2.2.1哈希文件

2.2.2.2unordered_map模拟

2.2.2.3unordered_set模拟

3.位图

模拟

应用 

4.布隆过滤器

模拟

优点:

缺陷:

应用

5.补充


首先要说明的是unordered系列的容器,但是使用上unordered_map、unordered_set跟map、set区别不大,我这里不会再详细的介绍每个接口

1.unordered系列容器

1.1简介

set和unordered_set的区别,效率上unordered_set只是略微胜出一点,跟下面unordered_map和map的区别是差不多的,都是底层实现不同,所以不多赘述。详情可自行参考文档

unordered_map跟map的区别

1.unordered_map内部存储不会排序,为了能够常数级别访问,会有个哈希值,且把哈希值相同的放在同样的桶中(哈希值是什么,看下面具体哈希的内容,这里只是对容器做个说明)

unordered_map访问单个元素,比map快,但是遍历元素方面效率较低。

2.只有单向迭代器,没有反向迭代器。

1.2接口 (只对效果进行描述,具体可以自行参考文档)

1.2.1构造

unordered_map<int>has

1.2.2容量

empty()检测容器是否为空。

size获取容器有效元素个数

1.2.3迭代器

begin(),end(),cbegin(),cend().

1.2.4访问

has[3]。支持括号访问

原理跟map的括号一样,都是调用插入操作。

1.2.5查询

find(),返回key在哈希桶中的位置

count返回哈希桶中关键码为key的键值对的个数(unordered_map里面key不能重复,所以unordered_map的count只能返回1或0)

1.2.6修改

insert()插入,erase删除,clear清空,swap(unordered_map &)交换两个容器的元素

1.2.7桶操作

bucket count()   返回哈希中桶的总个数

bucket size(size_t n) 返回n号桶中有效元素的总个数

bucket(const K&key)返回元素key所在的桶号

2.哈希 

2.1概念

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

理想情况:不经过任何比较,一次直接获得要搜索的元素,即常数级别。

因此,哈希这种元素存储位置与关键码有映射关系的结构就出现了。

哈希:
根据插入元素的关键码,通过哈希函数获得对应存储位置,然后进行存放查找。

若查找的话,就是根据搜索元素的关键码通过哈希函数获得位置,然后与此位置的关键码进行比较。相等则成功。

在思想上跟桶排序有很大的关系,但是桶排序的空间浪费很大,所以这里不能直接通过下标确定位置。

哈希函数就是:hash(key)=key%capacity  capacity就是存储元素空间的总大小。

负载因子:对于闭散列,基本都是0.几,而开散列,可以到1。负载因子就是有效数据占容器容量的比例。

2.2哈希冲突

但是有哈希冲突,就是有可能不同的关键码,获得的存储位置确实一模一样的,这就是冲洗冲突或哈希相撞

哈希函数的设定方式有很多种,上面只是一种。

直接定址法、除留余数、平方取中、折叠法、随机数法、数学分析法。

而解放方式常见的:闭散列、开散列。

2.2.1闭散列

也叫开放定址法,发生哈希冲突时,如哈希表未满,则一定还有位置,那就把key放在冲突位置的下一个空位置去。注意,哈希结构我们人为的限制不能装满,有一个负载因子\载荷因子做评估,实际元素个数/总空间大小,闭散列一般是0.7,因为如果满了的话,查找效率是O(n)。所以对于元素存储个数达到限制后,一般就直接空间扩容,用空间换取效率

线性探测:

从发生冲突的位置开始,依次向后探测,如果找到的最后一个位置,则跳到空间的开头,直到找到一个空位置。

        插入:通过哈希函数获取元素的在哈希表的位置,如果该位置没有元素直接插入,有元素的话利用线性探测获取下一个空位置的地址,然后把该值放进去。

        搜索:如果该位置元素不相等,则线性往后找,直到找到\遇到空

        删除:闭散列删除的时候,不能直接删,因为我们搜索的逻辑是从直接获得地址开始往后找,这样的话,如果前面一个元素已经被删了,但是我们要查找的元素在后面,这样遇到空就不会继续找,也就不会找到我们要的元素而且我们如何确定删除的状态呢?。所以要采取标记的伪删除法来删除一个元素。

实现代码:

#pragma once
#include<vector>
enum State {
	EMPTY,
	EXIST,
	DELETE
};
template<class K, class V>
struct HashData {
	pair<K, V> _kv;
	State _state=EMPTY;//标记该位置状态
};

//对于非整数的key,默认采用强转的方式
template<class K>
struct HashFunc {
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
//如果不能通过强制的类型,则单独写个仿函数
//自己写的类,也可以这样。日期类就可以把年月日加起来,中间*权值
//不一定都是成员加起来,像是个人信息,就可以单独把身份证号的ASCII码加起来,中间*权值
//核心就是把非唯一的值尽量拼起来,然后以拼起来的值(中间可以*权值),作为key
//struct HashFuncString {
//	size_t operator()(const string& s) {
//		size_t hash = 0;
//		for (auto e : s)
//		{
//			hash += e;
//			hash *= 131;//权值,否则的话,abcd==dcba,太容易冲突了
//		}
//		return hash;
//	}
//};

//一般来说,string经常用,所以直接特化(库里也是这样的,但自己写的类,还是要自己传仿函数)
//而且库里还有一个仿函数,是用来比较两个key是否相等的仿函数
template<>
struct HashFunc<string> {
	size_t operator()(const string& s) {
		size_t hash = 0;
		for (auto e : s)
		{
			hash += e;
			hash *= 131;//权值,否则的话,abcd==dcba,太容易冲突了
		}
		return hash;
	}
};
template<class K,class V, class Hash=HashFunc<K>>
class HashTable {
public:
	HashTable(size_t size=10) {
		_tables.resize(size);
	}
	HashData<K,V>* Find(const K&key)
	{
		Hash hs;
		//线性探测
		size_t hashi = hs(key) % _tables.size();
		while (_tables[hashi]._state!=EMPTY)
		{
			if (key == _tables[hashi]._kv.first && _tables[hashi]._state == EXIST)
			{
				return &_tables[hashi];
			}
			hashi++;
			hashi %= _tables.size();
		}
		return nullptr;
	}
	bool Insert(const pair<K, V>& kv)
	{
		if (Find(kv.first))return false;
		//如果负载因子大于等于0.7要扩容
		if (_n*10 / _tables.size() >= 7)
		{
			HashTable<K, V,Hash>newHT(_tables.size() * 2);
			//遍历旧表,插入新表
			for (auto& e : _tables)
			{
				if (e._state == EXIST)
				{
					newHT.Insert(e._kv);
				}
			}
			//最后交换,旧表在newHT出了作用域之后,
			//newHT会自动调用析构
			//这里也不用特意写析构,因为默认生成的析构足够了
			//vector会调用vector的析构函数,int类型内置类型
			_tables.swap(newHT._tables);
		}
		Hash hs;
		//线性探测
		size_t hashi = hs(kv.first)%_tables.size();
		while (_tables[hashi]._state == EXIST)
		{
			hashi++;
			hashi %= _tables.size();
		}
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;
		return true;
	}
	bool Erase(const K& key)
	{
		HashData<K, V>* ret = Find(key);
		if (ret)
		{
			_n--;
			ret->_state = DELETE;
			return true;
		}
		else
		{
			return false;
		}
	}
private:
	vector<HashData< K,V>>_tables;
	size_t _n;//实际存储的数据个数
};


void TestHT1() {
	int a[] = { 1,4,24,34,45,7,44,17,37 };
	HashTable<int, int>ht;
	for (auto& e : a)
	{
		ht.Insert(make_pair(e, e));
	}
	for (auto& e : a)
	{
		auto ret = ht.Find(e);
		cout << ret->_kv.first << ":" << ret->_state << endl;
	}
	ht.Erase(34);
	ht.Erase(4);
	cout << endl;
	for (auto& e : a)
	{
		auto ret = ht.Find(e);
		if (ret)cout << ret->_kv.first << ": EXIST" << endl;
		else cout << e << ": DELETE" << endl;
	}
 }
void TestHT2() {
	HashTable<string,string>ht;
	ht.Insert(make_pair("dwadwa", "dwadwa"));
	ht.Insert(make_pair("dsxzdwa", "dwadwa"));
}

二次探测:在线性探测的基础上,每次跨越的步伐更加大些。

线性探测是:hashi+i,i>=0,二次探测就是hashi+i^2,i>=0

让整个空间的存放更加均匀,而不是挤在某一块

结论:

在闭散列的结构下,这些探测本质还是一种互相挤占空间的方式,近似恶意循环。

2.2.2开散列

相对于闭散列,开散列不将冲突的值放其他位置,开散列下,每个空间都是一个链表,这个链表里存哈希值相同的元素

结构很清晰,就是利用链式结构。

2.2.2.1哈希文件

这是底层头文件,后面的unordered_map和unordered_set模拟,都是在这个头文件的基础上封装的

#pragma once
#include<vector>
//对于非整数的key,默认采用强转的方式
template<class K>
struct HashFunc {
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
//如果不能通过强制的类型,则单独写个仿函数
//自己写的类,也可以这样。日期类就可以把年月日加起来,中间*权值
//不一定都是成员加起来,像是个人信息,就可以单独把身份证号的ASCII码加起来,中间*权值
//核心就是把非唯一的值尽量拼起来,然后以拼起来的值(中间可以*权值),作为key
//struct HashFuncString {
//	size_t operator()(const string& s) {
//		size_t hash = 0;
//		for (auto e : s)
//		{
//			hash += e;
//			hash *= 131;//权值,否则的话,abcd==dcba,太容易冲突了
//		}
//		return hash;
//	}
//};

//一般来说,string经常用,所以直接特化(库里也是这样的,但自己写的类,还是要自己传仿函数)
//而且库里还有一个仿函数,是用来比较两个key是否相等的仿函数
template<>
struct HashFunc<string> {
	size_t operator()(const string& s) {
		size_t hash = 0;
		for (auto e : s)
		{
			hash += e;
			hash *= 131;//权值,否则的话,abcd==dcba,太容易冲突了
		}
		return hash;
	}
};
namespace openadress {
	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 Hash = HashFunc<K>>
	class HashTable {
	public:
		HashTable(size_t size = 10) {
			_tables.resize(size);
		}
		HashData<K, V>* Find(const K& key)
		{
			Hash hs;
			//线性探测
			size_t hashi = hs(key) % _tables.size();
			while (_tables[hashi]._state != EMPTY)
			{
				if (key == _tables[hashi]._kv.first && _tables[hashi]._state == EXIST)
				{
					return &_tables[hashi];
				}
				hashi++;
				hashi %= _tables.size();
			}
			return nullptr;
		}
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))return false;
			//如果负载因子大于等于0.7要扩容
			if (_n * 10 / _tables.size() >= 7)
			{
				HashTable<K, V, Hash>newHT(_tables.size() * 2);
				//遍历旧表,插入新表
				for (auto& e : _tables)
				{
					if (e._state == EXIST)
					{
						newHT.Insert(e._kv);
					}
				}
				//最后交换,旧表在newHT出了作用域之后,
				//newHT会自动调用析构
				//这里也不用特意写析构,因为默认生成的析构足够了
				//vector会调用vector的析构函数,int类型内置类型
				_tables.swap(newHT._tables);
			}
			Hash hs;
			//线性探测
			size_t hashi = hs(kv.first) % _tables.size();
			while (_tables[hashi]._state == EXIST)
			{
				hashi++;
				hashi %= _tables.size();
			}
			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			++_n;
			return true;
		}
		bool Erase(const K& key)
		{
			HashData<K, V>* ret = Find(key);
			if (ret)
			{
				_n--;
				ret->_state = DELETE;
				return true;
			}
			else
			{
				return false;
			}
		}
	private:
		vector<HashData< K, V>>_tables;
		size_t _n;//实际存储的数据个数
	};
}
namespace hash_bucket {
	//数据量很大的时候,table的每个位置,可以不挂单链表,而是直接挂红黑树。
	//这样就算遇到极端情况,查找效率也很高。
	//从复用角度看,如果不想自己手搓,可以直接让Node
	//里面分别放list(),set(),当前桶里元素个数超过8个,遍历放入set里。
	//然后list.clear(),反之,放入list(),set.clear();
	template<class T>
	struct HashNode {
		HashNode<T>* _next;
		T _data;
		HashNode(const T&data)
			:_next(nullptr)
			, _data(data)
		{

		}
	};

	//前置声明,因为下面迭代器类typedef了哈希表类,但是哈希表类下迭代器类的下面。
	//编译器找不到,也就识别不了迭代器类里的哈希表
	//但单纯两者对换也不行,因为哈希表类里也typedef了迭代器类
	//这样的话,互相引用。解决方法就是把处于下面的哈希表类进行前置声明
	//这样编译器就能识别了
	template<class K, class T, class KeyOfT, class Hash>
	class HashTable;
	
	template<class K, class T, class KeyOfT, class Hash>
	struct _HTIterator {
		size_t hti;
		typedef HashNode<T> Node;
		typedef HashTable<K, T, KeyOfT, Hash> HT;
		typedef _HTIterator<K, T, KeyOfT, Hash> Self;
		HT* _ht;
		Node* _node;
		_HTIterator(Node* node, HT* ht)
			:_node(node)
			, _ht(ht)
		{}
		T& operator*() {
			return _node->_data;
		}
		T* operator->() {
			return &_node->_data;
		}
		Self& operator++() {
			if (_node->_next)
			{
				//当前桶没有走完
				_node = _node->_next;
			}
			else
			{
				//当前桶走完了,找下一个
				KeyOfT kot;
				Hash hs;
				size_t hashi = hs(kot(_node->_data)) % _ht->_tables.size();
				hashi++;
				//找下一个桶
				while (hashi < _ht->_tables.size())
				{
					if (_ht->_tables[hashi])
					{
						_node = _ht->_tables[hashi];
						break;
					}
					hashi++;
				}
				//后面没有桶了
				if (hashi == _ht->_tables.size())
				{
					_node = nullptr;
				}
			}
			return *this;
		}
		bool operator!=(const Self& s)
		{
			return _node != s._node;
		}
	};


	template<class K, class T, class KeyOfT,class Hash>
	class HashTable {
		template<class K, class T, class KeyOfT, class Hash>
		friend struct _HTIterator;
		typedef HashNode<T> Node;
	public:
		typedef _HTIterator<K, T, KeyOfT, Hash> iterator;
		iterator begin() {
			for (size_t i = 0; i < _tables.size(); i++)
			{
				//从左往右,第一个桶
				if (_tables[i])
				{
					return iterator(_tables[i], this);
				}
			}
			return end();
		}
		iterator end() {
			return iterator(nullptr, this);
		}
		//素数大小的容器,没有定论,vs下没有这样
		int GetNextPrimer(size_t val) {
			const int prime[28] = { 53,97,193,389,769,1543,3079,6151,12289,24593,49157,98317,196613,393241,786433,1572869,3145739,6291469,12582917,25165843,50331653,100663319,201326611,402653189,805306457,1610612741,3221225473,4294967291 };
			for (int i = 0; i < 28; i++)
			{
				if (prime[i] > val)return prime[i];
			}
			return prime[27];

		}
		HashTable() {
			_tables.resize(GetNextPrimer(1), nullptr);
			_n = 0;
		}
		~HashTable() {
			//一个个桶找过来,诶个释放
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
		}
		//为了用[]的结构
		pair<iterator,bool> Insert(const T& data)
		{
			KeyOfT kot;
			iterator it = Find(kot(data));
			if (it!=end())
			{
				return make_pair(it,false);
			}
			Hash hs;
			if (_n == _tables.size())
			{
				//这里不复用insert,是vector里的每个地址指向的空间
				//在复用的时候,都要经历new n个节点,析构n个节点。
				//消耗很大
				vector<Node*>newTables(GetNextPrimer(_n), nullptr);
				//遍历旧表,头插新表
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = hs(kot(cur->_data)) % newTables.size();
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;
						cur = next;
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newTables);
			}
			size_t hashi = hs(kot(data)) % _tables.size();
			Node* newnode = new Node(data);
			//头插
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return make_pair(iterator(newnode,this),true);
		}
		iterator Find(const K& key)
		{
			KeyOfT kot;
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				//这里其实还要再配一个比较的仿函数的
				if (kot(cur->_data) == key)
				{
					return iterator(cur, this);
				}
				cur = cur->_next;
			}
			return end();
		}
		iterator Erase(const K& key)
		{
			KeyOfT kot;
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					if (prev)
					{
						prev->_next = cur->_next;
						delete cur;
						--_n;
						if(prev->_next)
						return iterator(prev->_next,this);
						else
						{
							for (size_t i = hashi+1; i < _tables.size(); i++)
							{
								//从左往右,第一个桶
								if (_tables[i])
								{
									return iterator(_tables[i], this);
								}
							}
							return end();
						}
					}
					else
					{
						_tables[hashi] = cur->_next;
						delete cur;
						--_n;
						if(_tables[hashi])
						return iterator(_tables[hashi], this);
						else
						{
							for (size_t i = hashi+1; i < _tables.size(); i++)
							{
								//从左往右,第一个桶
								if (_tables[i])
								{
									return iterator(_tables[i], this);
								}
							}
							return end();
						}
					}
				}
				prev = cur;
				cur = cur->_next;
			}
			return end();
		}
	private:
		//vector<list<pair<K,V>>>_tables;可行,但自己写个也不错
		vector<Node*>_tables;//指针数组
		size_t _n;
	};

}
2.2.2.2unordered_map模拟
#pragma once
#include"newHashTable.h"

namespace bit {
	template<class K, class V,class Hash=HashFunc<K>>
	class unordered_map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};
	public:
		typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::iterator iterator;
		
		iterator begin() {
			return _ht.begin();
		}
		iterator end() {
			return _ht.end();
		}

		pair<iterator, bool> Insert(const pair<K, V>& kv)
		{
			return _ht.Insert(kv);
		}
		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = Insert(make_pair(key, V()));
			return ret.first->second;
		}
		iterator Find(const K& key)
		{
			return _ht.Find(key);
		}
		iterator erase(const K& key)
		{
			return _ht.Erase(key);
		}
	private:
		hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT,Hash>_ht;
	};
}
2.2.2.3unordered_set模拟
#pragma once
#include"newHashTable.h"

namespace bit {
	template<class K,class Hash=HashFunc<K>>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::iterator iterator;

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

		pair<iterator,bool> Insert(const K& key)
		{
			return _ht.Insert(key);
		}
		iterator Find(const K& key)
		{
			return _ht.Find(key);
		}
		iterator erase(const K& key)
		{
			return _ht.Erase(key);
		}
	private:
		hash_bucket::HashTable<K, const K, SetKeyOfT,Hash>_ht;
	};
	void test_set1() {
		unordered_set<int>us;
		for (int i = 0; i < 200; i++)us.Insert(i);
		unordered_set<int>::iterator it = us.begin();
		while (it != us.end())
		{
			cout << (*it) << endl;
			++it;
		}
	}
}

3.位图

位图, 就是每一位用来存放状态,适用于海量数据,数据不重复。一般是查找数据在不在。思想也是用哈希实现。库里也有实现,是bitset。

这里是用的直接定址法。所以开多大取决于数据范围,而不是数据量

位图可以用在什么地方?

1. 快速查找某个数据是否在一个集合中
2. 排序 + 去重
3. 求两个集合的交集、并集等
4. 操作系统中磁盘块标记

接下来是具体的模拟,附带几道变形题

模拟

#pragma once
#include<vector>
#include<cassert>

namespace bit {
	template<size_t N>
	class bitset {
	public:
		//不用担心负数的问题,负数会转成size_t,还是可以做映射的。
		//
		bitset() {
			_bits.resize(N / 32 + 1, 0);
		}
		//把x映射的标记为1
		void set(size_t x)
		{
			assert(x <= N);
			//因为没法开位数组,所以必须开int的话,要以32位为一组,分为存储。
			size_t i = x / 32;
			size_t j = x % 32;
			_bits[i] |= (1 << j);
			//利用或,有1为1,让1左移j位,此时二进制位上,第j位是1,其他为0,然后让这个数跟_bits[i]或以下,这样就能让第i个整形,第j位修改为1
			//或许会疑惑,为什么不是32-j,因为左移是向高位移动,所以不管是小端机还是大端机,都一样。x%32,得到的是从低位到x的距离,整个内部存储,
			//映射不是线性的。
		}
		//把x映射的标记为0
		void reset(size_t x)
		{
			assert(x <= N);
			size_t i = x / 32;
			size_t j = x % 32;

			//跟上面类似,只是这里要让_bits[i]与上一个11111110111111111
			_bits[i] &= ~(1 << j);
		}
		//查找数是不是存在
		bool Test(size_t x)
		{
			assert(x <= N);
			size_t i = x / 32;
			size_t j = x % 32;
			//这里不修改,只探测,返回一个整型值,整型值转bool,非0真,0假。所以只要该二进制位为1,那最后返回就是真,为0就是假。
			return _bits[i] & (1 << j);
		}
	private:
		
		vector<int>_bits;
	};
	void test_bitset() {
		bitset<31>mp;
		mp.set(30);
		mp.set(20);
		mp.set(10);
		for (int i = 1; i <= 30; i++)
		{
			if (mp.Test(i))
			{
				cout <<i<< "在" << endl;
			}
			else
			{
				cout <<i<< "不在" << endl;
			}
		}
		mp.reset(30);
		mp.set(29);
		for (int i = 1; i <= 30; i++)
		{
			if (mp.Test(i))
			{
				cout << i << "在" << endl;
			}
			else
			{
				cout << i << "不在" << endl;
			}
		}
	}


	// 给定100亿个整数,设计算法找到只出现一次的整数?
	template<size_t N>
	class two_bitset {
	public:
		void set(size_t x)
		{
			//00 0次,01 1次,10 2次及以上
			if (_bs1.Test(x) == false && _bs2.Test(x) == false)_bs2.set(x);
			else if (_bs1.Test(x) == false && _bs2.Test(x) == true) {
				_bs1.set(x);
				_bs2.reset(x);
			}
		}
		int test(size_t x)
		{
			if (_bs1.Test(x) == false && _bs2.Test(x) == false)return 0;
			else if (_bs1.Test(x) == false && _bs2.Test(x) == true) {
				return 1;
			}
			else return 2;
		}
	private:
		bitset<N>_bs1;
		bitset<N>_bs2;
	};
	void test_two_bitset() {
		two_bitset<100>mp;
		int a[] = { 2,4,4,56,65,45,56,76,67,76 };
		for (auto e : a)
		{
			mp.set(e);
		}
		for (auto e : a)
		{
			cout << mp.test(e) << endl;
		}
	}
}

应用 

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

  依旧是利用位图,代码不写了,直接写思路。两个位图,两个文件分别存值,然后看是否两个位图里都存在这个数。存在就是交集,否则就不是交集一部分。

注意,不要被100亿数字吓到,整数,这里默认都是int范围的,也就是说位图肯定是可以存的,那这100亿有很多重复的数字,是不用管的,默认去重了。

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

是第一道题的变形,只是00 0次,01 1次, 10 2次 11 3次及以上

对于第一个问题,我们还可以限制内存大小,比如100亿,1G是够的,但是如果只让用512M呢?我们的思路是多批次读取,每次只统计一段范围的值。比如0-2^31-1是512M,2^31-2^32-1也是512M。这样每次读取都只用到512M。

4.布隆过滤器

 主要是对位图的一个小修改,位图对于非整型数据,虽然也能存(通过转换成整型再映射的方式,但是冲突的概率很大),所以布隆的主要思想就是让单个数据映射多个位置,只要多个位置有一个位0,则一定不存在。多个位置全为1,只有很低的概率才是不存在,大概率是存在的。只要让映射的位置变多,冲突的概率就变小了。

模拟

#pragma once
#include<bitset>
#include<string>
struct HashFuncBKDF {
	//BKDR
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto ch : s)
		{
			hash += ch;
			hash *= 131;
		}
		return hash;
	}
};
struct HashFuncAP {
	//AP
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (size_t i = 0; i < s.size(); i++)
		{
			//偶数位
			if ((i & 1) == 0)
			{
				hash ^= ((hash << 7)) ^ (s[i]) ^ (hash >> 3);

			}//奇数位
			else
			{
				hash ^= (~(hash << 11)) ^ (s[i]) ^ (hash >> 5);
			}
		}
		return hash;
	}
};
struct HashFuncDJB {
	//DJB
	size_t operator()(const string& s)
	{
		size_t hash = 5381;
		for (auto ch : s)
		{
			hash = hash * 33 ^ ch;
		}
		return hash;
	}
};
template<size_t N, class K = string, class Hash1 =HashFuncBKDF, class Hash2= HashFuncAP, class Hash3 = HashFuncDJB>
class BloomFilter {
public:
	void Set(const K& key)
	{
		size_t hash1 = Hash1()(key) % M;
		size_t hash2 = Hash2()(key) % M;
		size_t hash3 = Hash3()(key) % M;
		_bs.set(hash1);
		_bs.set(hash2);
		_bs.set(hash3);
	}
	bool Test(const K& key)
	{
		size_t hash1 = Hash1()(key) % M;
		if (_bs.test(hash1) == false)return false;
		size_t hash2 = Hash2()(key) % M;
		if (_bs.test(hash2) == false)return false;
		size_t hash3 = Hash3()(key) % M;
		if (_bs.test(hash3) == false)return false;
		//不存在是准确的
		return true;//存在是有可能误判的,因为可能这3个映射位置都跟别的冲突了
		//这也是为什么说映射位置增加在一定程度上是可以减少误判概率的
	}
	//为什么不实现reset?
	//因为删了某一个,会影响与他有冲突的元素,导致该元素被误判成不存在。
private:
	//这里开多大,知乎上有一个大佬做了分析
	//https://zhuanlan.zhihu.com/p/43263751/
	static const size_t M = 8 * N;
	//为什么要用static?因为就算是常量,在这里也只是类里面的成员的声明,
	//在不实例化前是不占空间的,也就是说不会在常量区里面存放,这样编译器
	//就无法在常量区中找到他,无法找到,就说明这不是个常量,而bitset内部也是数组
	//存储,数组的大小必须是常量,所以只const,是会报错的
	//而static,是不属于单个对象,而是整个类的,就算没有实例化,他也是占据空间的。
	bitset<M> _bs;

};


void TestBloomFilter()
{
	string x[] = { "西瓜","苹果","梨子" };
	BloomFilter<10>bf;
	for (auto& s : x)
	{
		bf.Set(s);
	}
	for (auto& s : x)
	{
		cout << bf.Test(s) << endl;
	}
	for (auto& s : x)
	{
		cout << bf.Test(s+" ") << endl;
	}
}

这里说个题外话,stl里的bitset,一千万级是可能栈溢出的,因为stl里是把数据放在栈上,用的是静态数组,而不是vector,可以实测,sizeof库里的bitset和我上面实现的bitset。

为什么可以测出是在栈上而不是堆上?我也不是很清楚,对这方面可以看下这篇文章

程序员应了解的那些事(9) STL vector:sizeof(vector)分析 + sizeof小结-优快云博客

那实际使用的时候不想手撕怎么办,可以让布隆的成员bitset,以指针的形式存在,然后new一个就好,这样bitset就放在堆上了。

这里总结下布隆过滤器

优点:

1.增加和查询元素的时间复杂度O(K),K是哈希函数的个数,一般比较小。与数据量大小无关

2.哈希函数相互之间没有关系,方便硬件并行运算

3.布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势

4.在能够承受一定的误判时,布隆过滤器比其他数据结构有很大的空间优势

5.数据量很大时,布隆过滤器可以表示全集,其他数据结构不能

6.使用同一组散列函数的布隆过滤器可以进行交、并、差运算

缺陷:

1.有误判率,有个说法叫假阳性(False,Position),只能准确判断元素不在集合中,不能准确判断元素在集合中,(可以建立白名单,存储可能误判的数据)

2.不能获取元素本身

3.一般情况下不能从布隆过滤器中删除元素

4.如果采用计数方式删除,存在计数回绕问题。

应用

对于一些特殊场景,虽然不容忍误判,但是布隆过滤器还是可以做到减少数据库压力的作用。因为布隆过滤器判断不在,那一定是不在,可以直接返回结果,如果判断在,再去数据库进行查找,实现减少数据库压力的作用

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

近似算法,采用布隆过滤器,在,就是在,不在就是不在,因为有误判可能,所以只能是近似算法。

而精确算法:布隆做不到,而用哈希map或者红黑树等,也不现实,因为100亿个查询,就算一个查询只占50字节,那也是相当高的占用空间,所以还是采用哈希的思想,用哈希切分,将大文件差分成小文件,利用哈希函数,映射查询,将大文件拆分下来的文件进行编号,让哈希值做编号,对应查询放入对应的小文件中,因为哈希函数对同一个查询返回的哈希值一定是相同的,所以对于交集,只需要两个大文件A和B的每个Ai和Bi放入内存比较即可(这时候用红黑树、平衡树、哈希表等等,都可以)。具体操作,可以试着把100亿的文件,拆成1000份,也就是hash函数%的数是1000,之所以不拆500份,是因为查询的占据空间不是一样的,有些文件很大,有些文件很小,如果拆成500份,冲突多的话,两个文件会超过1g,就不能放入内存了,这样拆成1000份,就可以让冲突的可能性降低,如果冲突多,可以考虑%1000以上)

但是还是有极端情况,比如某些查询的值想当接近,导致某个文件过大,还是超过了1g,这时候如果对这个文件再次进行哈希切分,效果不大,因为在这个情况下,不管是什么哈希函数,最后还是可能会分到一个文件,还是超过了1g。这个问题其实不用管太多,因为如果是因为查询的值过于接近、冲突过多,导致的Ai文件过大,那直接读入bitset,或者set,map等,都是可以直接读的,因为这些容器基本都是去重的,虽然这个文件可能很大,但是去掉重复,也就那么点。

如果是大于1g,且重复内容不多的文件,那么才可以采取最开始说的,二次哈希切分,换个哈希函数。

总结下,对于冲突多的,采用二次哈希切分。对于重复多的,采用set、map、bitset等可以去重的容器读取文件即可。这也是哈希切分下来,某个文件过大的处理方案
2. 如何扩展BloomFilter使得它支持删除元素的操作

可以用引用计数的方式,这样就可以让布隆支持删除,但是这样空间消耗会大很多,一个标记要开8位。极端情况,消耗跟红黑树之类的差不多,这样就没有意义了

3.给一个超过100G大小的log文件,log中存着ip地址,设计算法找到出现次数最多的ip地址,并且如何找到top k的ip

这题跟上一次的思路是相似的。同样是哈希切分,让相同ip进入相同的文件,一个文件只会是相同的ip和有冲突的ip。这时候用map读文件,虽然没有限制内存,但是map的容量也是有限的,并且极端情况可能出现一个文件很大(因为冲突多而不是重复多,重复多放map里,只是让value++罢了),所以map有几率爆,这时候采取二次处理,即换个哈希函数,再次对该小文件进行哈希切分。而找到出现次数多的ip,只需要让每个文件挨个放进map计数即可,就可以知道答案了。而对于top k的ip,则只需要弄个优先队列的小堆,自己写个仿函数或比较函数即可。

结论:

海量数据问题特征:数据量大,单台机器内存存不下

1.先考虑是否有合适的数据结构可以解决,比如位图、堆、布隆过滤器等等

2.大事化小思路。哈希切分(不能平均切分,就算可以复杂度也很高,效率很慢,有些问题就算平均切分了,也无法解决),切小之后,就能放内存中处理了

5.补充

 开放定址法处理散列表的的冲突时,其平均查找长度高于链接法处理冲突。冲突变多时,查找比较次数就会增加,对平均查找长度影响很大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值