哈希及其哈希思想的应用

news2024/11/23 12:40:32
1. unordered 系列关联式容器
C++98 中, STL 提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 $log_2
N$ ,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好
的查询是,进行很少的比较次数就能够将元素找到,因此在 C++11 中, STL 又提供了 4
unordered 系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是
其底层结构不同,本文中只对 unordered_map unordered_set 进行介绍,
unordered_multimap unordered_multiset 学生可查看文档介绍。
map跟set是有序的,unordered_map和unordered_set是无序的
unordered_map 的文档介绍
1. unordered_map 是存储 <key, value> 键值对的关联式容器,其允许通过 keys 快速的索引到与
其对应的 value
2. unordered_map 中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此
键关联。键和映射值的类型可能不同。
3. 在内部 ,unordered_map 没有对 <kye, value> 按照任何特定的顺序排序 , 为了能在常数范围内
找到 key 所对应的 value unordered_map 将相同哈希值的键值对放在相同的桶中。
4. unordered_map 容器通过 key 访问单个元素要比 map 快,但它通常在遍历元素子集的范围迭
代方面效率较低。
5. unordered_maps 实现了直接访问操作符 (operator[]) ,它允许使用 key 作为参数直接访问
value
6. 它的迭代器至少是前向迭代器。

 

 4. unordered_map的元素访问

               函数声明                                                功能介绍

               operator[]                                              返回与 key 对应的 value ,没有一个默认值
注意:该函数中实际调用哈希桶的插入操作,用参数 key V() 构造一个默认值往底层哈希桶
中插入,如果 key 不在哈希桶中,插入成功,返回 V() ,插入失败,说明 key 已经在哈希桶中,
key 对应的 value 返回。
5. unordered_map 的查询
             函数声明                                                功能介绍
iterator fifind(const K& key)                          返回 key 在哈希桶中的位置
size_t count(const K& key)                          返回哈希桶中关键码为 key 的键值对的个数
注意: unordered_map key 是不能重复的,因此 count 函数的返回值最大为 1

对比map/set的区别:

1.map和set的遍历是有序的,unordered系列是无序的

2.map和set是双向迭代器,unordered系列是单项

基于上面就功能阐述而言,map和set更强大,为什么还要引入unordered系列?

--综合而言,大量数据时,增删查改的效率更优秀,尤其是查找;

2. 底层结构
unordered 系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。
2.1 哈希概念
顺序结构以及平衡树 中,元素关键码与其存储位置之间没有对应的关系,因此在 查找一个元素
时,必须要经过关键码的多次比较 顺序查找时间复杂度为 O(N) ,平衡树中为树的高度,即
O($log_2 N$) ,搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以 不经过任何比较,一次直接从表中得到要搜索的元素
如果构造一种存储结构,通过某种函数 (hashFunc) 使元素的存储位置与它的关键码之间能够建立
一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功
该方式即为哈希 ( 散列 ) 方法, 哈希方法中使用的转换函数称为哈希 ( 散列 ) 函数,构造出来的结构称
为哈希表 (Hash Table)( 或者称散列表 )
例如:数据集合 {1 7 6 4 5 9}
哈希函数设置为: hash(key) = key % capacity ; capacity 为存储元素底层空间总的大小。

 哈希冲突

不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

把具有不同关键码而具有相同哈希地址的数据元素称为 同义词
发生哈希冲突该如何处理呢
哈希函数
引起哈希冲突的一个原因可能是: 哈希函数设计不够合理
哈希函数设计原则
哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 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 种不同的符号在各位上出现的频率不一定
相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只
有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散
列地址。例如:
假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前 7 位都是 相同
的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还
可以对抽取出来的数字进行反转 ( 1234 改成 4321) 、右环位移 ( 1234 改成 4123) 、左环移
位、前两数与后两数叠加 ( 1234 改成 12+34=46) 等方法。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的
若干位分布较均匀的情况
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
  哈希冲突解决
解决哈希冲突 两种常见的方法是: 闭散列 开散列
闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把 key 存放到冲突位置中的 下一个 空位置中去。 那如何寻找下一个空位置
呢?

1. 线性探测
比如 2.1 中的场景,现在需要插入元素 44 ,先通过哈希函数计算哈希地址, hashAddr 4
因此 44 理论上应该插在该位置,但是该位置已经放了值为 4 的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
插入
通过哈希函数获取待插入元素在哈希表中的位置
如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,
使用线性探测找到下一个空位置,插入新元素

 但是如果查找的位置的前一个被删除了,这时候去线性探测,就会出现问题,到了空但是没找到其实是在下一个,但是就没找到退出了;所以我们还得需要一个标志数据

哈希表什么情况下进行扩容?如何扩容?

 负载因子越小冲突概率越低;负载因子越大冲突概率越大;

什么时候扩容?满足一个基准,负载因子到一个基准值就扩容;

基准值越大,冲突越多,效率越低,空间利用率越高;

基准值越小,冲突越小,效率越高,空间利用率越低;

下面是关于线性探测哈希表的写法的实现:

hashtable:

设计中我们会遇见string的取模问题(会通过计算来规避这个问题)(不能完全避免) 

string是不能取模的,上篇文章在红黑树里面string是可以比较大小的;有些值可以取模有些值不能取模,怎么办?unordered_map有一个参数就是解决这个问题的;

hash<key>是一个仿函数,当你给的东西不能取模的时候,要配一个仿函数:转成一个可以取的值,再映射一次。 

这样写又会有这种的冲突问题,而且string作key的频率也很高,是个不容忽视的问题

 解决办法:乘了一个新值131(如果记不清的话随便乘一个值应该也是问题不大的)但是131应该是经过数学计算的最优解;

 下面是线性探测增删查的实现

#pragma once

using namespace std;

enum State
{
	EMPTY,//查找的时候停止的位置
	EXIST,//EXIST此位置已经有元素
	DELETE//查找的时候不停,但是可以继续往里面存值
};
template<class K,class V>
struct HashData
{
	pair<K, V> _kv;
	State _state=EMPTY;
};

template<class K>
struct HashFunc//目标是把string转成一个无符号的整形,之后就可以取模了;
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};


template<>
struct HashFunc<string>
{
	//BKDR算法,大佬针对string类型转换成整形写的算法,也被用于Java和数据库的搜索引擎中;
	size_t operator()(const string& key)
	{
		size_t val = 0;
		for (auto ch : key)
		{
			val *= 131;
			val += ch;
		}
		return val;
	}
};





//库里面我们调用unordered_map<make_pair(string,int)>的时候就不需要调用仿函数,这是因为string作为
//常见的类型我们可以用特化
//template<>
//struct HashFunc<string>
//{
//	size_t operator()(const string& key)
//	{
//		size_t val = 0;
//		for (auto ch : key)
//		{
//			val += ch;
//		}
//		return val;
//	}
//};

但是string不支持转,所以我们就针对string写一个
//struct HashFuncString
//{
//	size_t operator()(const string& key)
//	{
//		//怎么把一个string变成整形;
//		//如果用string的首字母来比较他们的ASCII码值,很多单词的首个单词都是相同的,就会一直冲突,这样
//		//算冲突值太大了;
//		size_t val = 0;
//		for (auto ch : key)
//		{
//			val += ch;//去取每个字符的字面,把ASCII码去比较,
//		}
//		return val;
//	}
//};

template<class K,class V,class Hash=HashFunc<K>>//这里给一个缺省类型。
class HashTable
{
public:
	bool Insert(const pair<K, V>& kv)
	{

		if (Find(kv.first))//已经有了就可以不插入了
			return false;

		//if (_size / _table.size() >= 0.7)//基准值给的0.7,java的库里面给的是0.75;
		//只存我们认为的70%(可以改),剩下的30%空间会被浪费
		if (_tables.size()==0||10*_size / _tables.size() >= 7)//扩容
		{
			size_t newSize = _tables.size() == 0 ? 10 : 2 * _tables.size();
			/*vector<HashData<K, V>>newTables;
			newTables.resize(newSize);*/
			HashTable<K, V,Hash> newHT;
			newHT._tables.resize(newSize);//开辟了原表长度的2倍;
			//旧表的数据映射到新表
			for (auto e:_tables)
			{
				newHT.Insert(e._kv);//哈希表对象再去调用insert,之后复用下面的线性探测部分完成拷贝
			}
			_tables.swap(newHT._tables);//交换完之后老的是局部对象,出了作用域会调用vector的析构
		}

		Hash hash;//仿函数的对象建立了;


		size_t hashi = hash(kv.first) % _tables.size();


		//size_t hashi = kv.first % _tables.size();//必须是在size内,如果是capacity的话就会[]越界
		//实际上size应该等于capacity,多了也没用,存不进去,这里不是push_back进去的,而是映射进去的
		
		//这里即使是负数也不会有问题:比如说这里模是操作符,c语言中是这样讲的,当操作符两边的数据不是
		//相同类型的,比如你是一个有符号,会被提升到范围更大的内一个,是无符号;

		
		//线性探测
		while (_tables[hashi]._state==EXIST)//说明这个地方已经有值,得线性探测
		{
			hashi++;
			hashi %= _tables.size();//回到起点的位置
		}
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_size;
		return true;
	}
	HashData<K,V>* Find(const K& key)
	{
		if (_tables.size() == 0)
		{
			return nullptr;
		}
		Hash hash;

		size_t start = hash(key) % _tables.size();
		size_t hashi = start;
		while (_tables[hashi]._state!=EMPTY)//不为空就继续走
		{
			if (_tables[hashi]._state!=DELETE&&_tables[hashi]._kv.first == key)
			{
				return &_tables[hashi];//找到就返回data的地址
			}
			hashi++;
			hashi %= _tables.size();//一直模,防止越界

			if (hashi == start)//没有发生增容,虽然--_size;但是表里面可能全是del,没有空了;(极端情况)
			{
				break;
			}
		}
		return nullptr;
	}

	bool Erase(const K& key)
	{
		HashData<K, V>* ret = Find(key);
		if (ret)
		{
			ret->_state = DELETE;
			--_size;
			return true;
		}
		else
		{
			return false;
		}
	}
	void Print()
	{

		for (size_t i = 0; i < _tables.size(); ++i)
		{
			if (_tables[i]._state == EXIST)
			{
				printf("[%d  %d]", i,_tables[i]._kv.first);
			}
			else
			{
				printf("[%d  *]", i);
			}
		}
		cout << endl;

	}
private:
	vector<HashData<K, V>>_tables;
	size_t _size=0;//数据是间隔放,这个是用来看存储了多少个有效数据
};

void TestHT1()
{
	HashTable<int, int> ht;//哈希表实际上V才是要存的值,K只是它的位置
	int a[] = {1,11,4,15,26,7,44,9};
	//ht.Insert

	for (auto e : a)
	{
		ht.Insert(make_pair(e, e));
	}
	ht.Erase(4);
	ht.Print();
	/*cout << ht.Find(44)->_kv.first << endl;
	cout << ht.Find(4) << endl;*/
	ht.Insert(make_pair(-2, -2));//复数也能被存
	ht.Print();
}
void TestHT2()
{
	string arr[] = { "苹果","西瓜","苹果","西瓜","香蕉","香蕉","苹果","西瓜","香蕉"};

	//HashTable<string,int,HashFuncString>countHT;//隐式的int内些可以通过缺省自己调用计算,这里得用我们写的显示的去传
	
	//HashTable<string, int, HashFunc<string>>countHT;
	HashTable<string, int>countHT;//特化之后可以传也可以不传,传缺省会优先走特化

	for (auto& str : arr)
	{
		auto ptr = countHT.Find(str);
		if (ptr)
		{
			ptr->_kv.second++;
		}
		else
		{
			countHT.Insert(make_pair(str, 1));

		}

	}
}
void TestHT3()
{
	HashFunc<string>hs1;
	cout << hs1("abcd") << endl;
	cout << hs1("bacd") << endl;
	cout << hs1("cadb") << endl;
	cout << hs1("bcad") << endl;

}



极端情况下给1,11,21,31,41,2;

这种就会互相占用位置;

之后我们给了二次探测这种解决方法,二次探测不是探测两次,线性探测是每次hash+i(i>=0),二次探测是+i^2(i>=0),都是先模之后再按这两条规则探测;

 但是还是没有从根本上解决问题,线性是挨着占位,二次探测是跳着占位,这种方式有更好的解决方式,哈希桶(开散列)--拉链法

开散列概念
开散列法又叫链地址法 ( 开链法 ) ,首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中
这里用单链表就够了,能节省一些空间就节省一些空间;

从上图可以看出, 开散列中每个桶中放的都是发生哈希冲突的元素
//这个时间负载的理论上是On,全部冲突,全挂在一条链上;当然这种极限条件几乎不会出现;但是几乎不用考虑最坏,平均下来,每个位置是挂常数个;

namespace HashBucket
{
    template<class K,class V>
	struct HashNode
	{
		pair<K, V> _kv;
		//这里删除直接就把结点干掉,不需要状态了;
		HashNode<K, V>* _next;


		HashNode(const pair<K,V>&kv)
			:_kv(kv)
			,_next(nullptr)
		{}
	};
	template<class K,class V, class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:

		~HashTable()
		{
			//vector不会释放自己的桶
			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;
			}
		}
			

		inline size_t __stl_next_prime(size_t n)
		{
			static const size_t __stl_num_primes = 28;
			static const size_t __stl_prime_list[__stl_num_primes] =
			{
				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 (size_t i = 0; i < __stl_num_primes; i++)
			{
				if (__stl_prime_list[i] > n)
				{
					return __stl_prime_list[i];
				}
			}

			return -1;
		}

		bool insert(const pair<K, V>& kv)
		{
			//去重
			if (Find(kv.first))//如果已经有了要插入值的first,先find之后就不插入了;
			{
				return false;
			}
			//扩容
			//库里面是负载因子到1就扩容
			if (_size == _tables.size())
			{
				//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;

				//我们不能像上面闭散列那种写法去复用insert,因为闭散列,数据不是像这样在结点上可以被拿下来
                //只需要拷贝,开散列这里得移动,insert会开辟新结点;所以采取新的方式

				vector<Node*> newTables;//自己去挂
				//newTables.resize(newSize, nullptr);

				newTables.resize(__stl_next_prime(_tables.size()),nullptr);
				Hash hash;

				//旧表中的节点移动映射到新表
				for (size_t i = 0; i < _tables.size(); ++i)
				{
					Node* cur = _tables[i];//这是取table的数据就是一个一个的指针
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = hash(cur->_kv.first) % newTables.size();//这时候的位置要模新表
						//算出在新表的位置,之后头插进去;
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;

						cur = next;
					}

					_tables[i] = nullptr;
				}
				_tables.swap(newTables);
 			}

			Hash hash;

			size_t hashi = hash(kv.first) % _tables.size();
			//模出来就可以开始挂了;严格意义上头插尾插无所谓,但是multi版本肯定是头插的,这里我们也头插
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;

			++_size;

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

		//删除这里我们不能用find先找,再去删除,这里相当于链表的删除,我们这里设计的是单链表,直接find
		//会找不到被删的前一个结点,导致删除无法正常完成;
		bool Erase(const K& key)
		{
			if (_tables.size() == 0)
				return nullptr;

			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];//算出位置,走到桶
			Node* prev = nullptr;
			while (cur)
			{
				//找到当前位置还得找到它的前一个
				if (cur->_kv.first == key)//找到了
				{
					//这里会有两种情况,第一种是cur是链表的第一个结点,那么这时prev就是nullptr
					//这种情况就单独处理,用指针数组指向cur的next;

					//1.头删
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;

					}
					//2.中间删
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;

					_size--;
					return true;
				}
				prev = cur;//没找到就先跟新prev;之后再++
				cur = cur->_next;
			}


			return false;
		}
		size_t Size()
		{
			return _size;
		}
		size_t BucketNum()//桶的数量(有多少个桶被用了)
		{
			size_t num = 0;
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i] != nullptr)
				{
					++num;
				}
			}
			return num;
		}
		size_t TableSize()//表的长度
		{
			return _tables.size();
		}

		size_t MaxBucketLenth()
		{
			size_t Maxlen = 0;
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				size_t len = 0;
				Node* cur = _tables[i];
				while (cur)
				{
					++len;
					cur = cur->_next;
				}
				
				if (len > Maxlen)
				{
					Maxlen = len;
				}
				

				
			}
			return Maxlen;
		}
	private:
		//表不存值,它是一个指针数组;
		vector<Node*> _tables;
		size_t _size=0;//存储有效数据的个数
	};


	void TestHT1()
	{
		HashTable<int, int> ht;//哈希表实际上V才是要存的值,K只是它的位置
		int a[] = { 1,11,4,15,26,7,44,9 };
		for (auto e : a)
		{
			ht.insert(make_pair(e, e));
		}
		ht.insert(make_pair(22, 22));
		
	}

	void TestHT2()
	{
		string arr[] = { "苹果","西瓜","苹果","西瓜","香蕉","香蕉","苹果","西瓜","香蕉" };


		HashTable<string, int>countHT;

		for (auto& str : arr)
		{
			auto ptr = countHT.Find(str);
			if (ptr)
			{
				ptr->_kv.second++;
			}
			else
			{
				countHT.insert(make_pair(str, 1));

			}

		}
	}
}

也有人说哈希如果扩容开的倍数如果每次都是素数,冲突会降低,我们看库里对这里的实现是写了一个素数表,规定好的,最后resize的时候是去调用这个表,因为如果刚开始给表长+1我们正常扩容的逻辑是乘2,但是乘2之后肯定就不是素数了,素数(只能有1和它本身两个因子)。所以还是比较麻烦的,用素数表这种方法就得到解决了;

最大的接近整形的最大值42亿; 

这里有一个语法特例,整形的const可以给缺省值,其他的不行,会语法报错;

哈希要说有什么缺点也就是插入慢,搜索还是很快的,如果挂的链表再某些极限情况下是很长的,我们可以改成挂红黑树来提升效率;哈希插入慢的原因是要重新计算位置;对比红黑树的插入,红黑树的消耗在于变色,和旋转,但是哈希比vector还麻烦重新计算再挂值;

有了开散列的实现,我们下来就可以模拟实现unordered_set, unordered_map;

我们搭了一个基础的架子,剩下的就是去写迭代器,上层的unordered_map和unordered_set只需要调用我们底层哈希桶的结构,但是需要略微的修改,对模板参数的修改,为了达到了两个结构使用一个底层,跟之前前面红黑树的封装是一样的,在哈希桶的底层的把第二个原来的k改成T因为这个参数是未知的;

我们只需要这个来自定义是set的k还是map的v来决定它是map的kv还是set的k。当然第一个k这个也不能删除,它要用于搜索;

把哈希结点也给一个模板参数,它需要用来上层unordered_map和unordered_set的node来实例化它,所有也给一个参数,并且我们把仿函数的缺省也给上层,因为下层也不确定它到底是什么,所以需要上层的确认; 

下来我们来写它的迭代器,这里是无序,不像之前红黑树,红黑树是二叉树,走中序就有序,这里是不需要排序的只需要按部就班的去走即可;

namespace HashBucket
{
    template<class T>
	struct HashNode
	{
		//pair<K, V> _kv;
		T _data;

		//这里删除直接就把结点干掉,不需要状态了;
		HashNode<T>* _next;


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

	//代码由上到下,这里得用到哈希表,但是哈希表的实现在下面,所以得在这里声明:
	//前置声明
	template<class K, class T, class Hash, class KeyOfT >
	class HashTable;

	template<class K, class T, class Hash, class KeyOfT >
	struct __HashIterator
	{
		typedef HashNode<T> Node;
		typedef HashTable<K, T, Hash, KeyOfT> HT;
		typedef __HashIterator<K, T, Hash, KeyOfT> Self;

		Node* _node;
		HT* _pht;

		__HashIterator(Node*node,HT*pht)
			:_node(node)
			,_pht(pht)
		{}

		T& operator*()
		{
			return _node->_data;
		}
		T* operator->()
		{
			return &_node->_data;
		}
		//单项迭代器:因为是单链表,不支持--;想要支持--,这里下层就需要一个双向链表
		Self& operator++()
		{
			if (_node->_next != nullptr)
			{
				//在当前桶里面迭代
				_node = _node->_next;
			}
			else
			{
				//找下一个桶;先算自己当前在哪个桶里面
				Hash hash;
				KeyOfT kot;
				//先得把结点里面的data的key取出来,在把这个key用hash转成可以取模的整形值,再去模table的
				//size;
				size_t i = hash(kot(_node->_data)) % _pht->_tables.size();
				//从下一个桶去找;
				++i;
				for (; i < _pht->_tables.size(); i++)
				{
					if (_pht->_tables[i] != nullptr)
					{
						_node = _pht->_tables[i];
						break;
					}
				}

				//说明后面没有数据的桶了
				if (i == _pht->_tables.size())
				{
					_node = nullptr;//返回空作为结尾
				}
				return *this;
			}
		}

		//bool operator !=(Self& s)const
		//{
		//	return _node != s._node;//用结点的指针比较
		//}
		
		//报错的原因是,你在使用 != 运算符比较itr和end()时,要求右操作数也要能够接受类型为
		//HashBucket::__HashIterator<K, T, Hash, KeyOfT>的参数。根据报错信息,可以推测右操作数的类型
		//是HashBucket::__HashIterator<K, T, Hash, KeyOfT>,而在你的代码中,end()函数返回的迭代器对象
		//是常量类型的迭代器



		//operator!=运算符被定义为成员函数,而且该函数使用了const修饰符。这意味着该成员函数可以在const
		//对象上被调用,并且在函数内部不会修改对象的状态。当你使用 != 运算符进行比较操作时,如果其中一个
		//操作数是const类型的对象,编译器将选择匹配的const成员函数进行调用。因此,当你在代码中使用 != 
		//运算符时,编译器会自动选择匹配的const版本的运算符函数进行调用。通过为iterator结构体中的
		//operator != 函数添加const修饰符,你为const对象提供了一个适当的重载函数,从而解决了编译器无法
		//匹配的问题,并消除了错误。
		bool operator!=(const Self& s) const
		{
			return _node != s._node;
		}


		bool operator==(const Self& s)const
		{
			return _node == s._node;//用结点的指针比较
		}
	};



	//template<class K,class V, class Hash = HashFunc<K>>
	template<class K, class T, class Hash,class KeyOfT >

	class HashTable
	{
		typedef HashNode<T> Node;
	public:

		//这里迭代器跟哈希表相互调用,但是这里的tables是私有成员,只能让迭代器做哈希的友元;
		//模板友元的声明得把类型标上;
		template<class K, class T, class Hash, class KeyOfT >
		friend struct __HashIterator;

		typedef __HashIterator<K, T, Hash, KeyOfT> iterator;

		iterator begin()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i])//一个一个去找,如果第i个桶不为空,则构造它的迭代器返回;
				{
					return iterator(_tables[i],this);//第二个参数this就是哈希表的指针
				}
			}
			return end();//一个桶都有的话;
		}

		iterator end()
		{
			return iterator(nullptr, this);//给个空的匿名对象的构造
		}

		

		~HashTable()
		{
			//vector不会释放自己的桶
			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;
			}
		}
			

		inline size_t __stl_next_prime(size_t n)
		{
			static const size_t __stl_num_primes = 28;
			static const size_t __stl_prime_list[__stl_num_primes] =
			{
				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 (size_t i = 0; i < __stl_num_primes; i++)
			{
				if (__stl_prime_list[i] > n)
				{
					return __stl_prime_list[i];
				}
			}

			return -1;
		}

		//bool insert(const T& data)
		pair<iterator,bool> insert(const T& data)

		{
			
			Hash hash;
			KeyOfT kot;
			//去重
			//if (Find(kot(data)))//如果已经有了要插入值的first,先find之后就不插入了;
			//{
			//	return false;
			//}
			
			iterator ret = Find(kot(data));
			if (ret != end())
			{
				return make_pair(ret, false);//插入失败了,返回已经有的迭代器;
			}
			//扩容
			//库里面是负载因子到1就扩容
			if (_size == _tables.size())
			{
				//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;

				//我们不能像上面闭散列那种写法去复用insert,因为闭散列,数据不是像这样在结点上可以被拿下来
				//只需要拷贝,开散列这里得移动,insert会开辟新结点;所以采取新的方式

				vector<Node*> newTables;//自己去挂
				//newTables.resize(newSize, nullptr);

				newTables.resize(__stl_next_prime(_tables.size()), nullptr);

				//旧表中的节点移动映射到新表
				for (size_t i = 0; i < _tables.size(); ++i)
				{
					Node* cur = _tables[i];//这是取table的数据就是一个一个的指针
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = hash(kot(cur->_data)) % newTables.size();//这时候的位置要模新表
						//算出在新表的位置,之后头插进去;
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;

						cur = next;
					}

					_tables[i] = nullptr;
				}
				_tables.swap(newTables);
			}
		

			

			size_t hashi = hash(kot(data))% _tables.size();
			//模出来就可以开始挂了;严格意义上头插尾插无所谓,但是multi版本肯定是头插的,这里我们也头插
			Node* newnode = new Node(data);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;

			++_size;

			//return true;
			return make_pair(iterator(newnode, this), true);

		}

		
		//Node* Find(const K& key)//不能只要第二个参数,find需要第一个k,map,find的时候只能用k找
		iterator Find(const K& key)//不能只要第二个参数,find需要第一个k,map,find的时候只能用k找
		{
			if (_tables.size() == 0)
				return end();


			Hash hash;
			KeyOfT kot;//所有比较的地方都得加这个仿函数
			size_t hashi = hash(key )% _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)//找到了
				{
					//return cur;
					return iterator(cur, this);
				}

				cur = cur->_next;
			}
			return end();
		}

		//删除这里我们不能用find先找,再去删除,这里相当于链表的删除,我们这里设计的是单链表,直接find
		//会找不到被删的前一个结点,导致删除无法正常完成;
		bool Erase(const K& key)
		{
			if (_tables.size() == 0)
				return false;

			Hash hash;
			KeyOfT kot;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];//算出位置,走到桶
			Node* prev = nullptr;
			while (cur)
			{
				//找到当前位置还得找到它的前一个
				if (kot(cur->_data) == key)//找到了
				{
					//这里会有两种情况,第一种是cur是链表的第一个结点,那么这时prev就是nullptr
					//这种情况就单独处理,用指针数组指向cur的next;

					//1.头删
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;

					}
					//2.中间删
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;

					_size--;
					return true;
				}
				prev = cur;//没找到就先跟新prev;之后再++
				cur = cur->_next;
			}


			return false;
		}
		size_t Size()
		{
			return _size;
		}
		size_t BucketNum()//桶的数量(有多少个桶被用了)
		{
			size_t num = 0;
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i] != nullptr)
				{
					++num;
				}
			}
			return num;
		}
		size_t TableSize()//表的长度
		{
			return _tables.size();
		}

		size_t MaxBucketLenth()
		{
			size_t Maxlen = 0;
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				size_t len = 0;
				Node* cur = _tables[i];
				while (cur)
				{
					++len;
					cur = cur->_next;
				}
				
				if (len > Maxlen)
				{
					Maxlen = len;
				}
				

				
			}
			return Maxlen;
		}
	private:
		//表不存值,它是一个指针数组;
		vector<Node*> _tables;
		size_t _size=0;//存储有效数据的个数
	};


	
}

unordered_map

#pragma once

#include"HashTable.h"
namespace lrx
{
	template<class K,class V, class Hash = HashFunc<K>>//把缺省的套在这一层,平常不用底层的哈希
	class unordered_map
	{
		//这里实现mapkeyofT让我们知道这是一个pair
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};
	public:
		//区分不出来这是静态变量还是类型,静态变量也可以用类域去取;
		typedef typename HashBucket::HashTable<K, pair<K, V>, Hash, MapKeyOfT>::iterator iterator;

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

		iterator end()
		{
			return _ht.end();
		}
		
		//bool Insert(const pair<K, V>& kv)
		pair<iterator,bool> Insert(const pair<K, V>& kv)
		{
			return _ht.insert(kv);//直接套一层外壳
		}

		V& operator[](const K& key)
		{
			pair<iterator, bool>ret = _ht.insert(make_pair(key, V()));
			return ret.first->second;//ret.first是迭代器,之后再取它的->second
		}
	private:
		//跟之前红黑树底层实现map和set一样,由第二个参数决定我们这里存什么;
		HashBucket::HashTable<K, pair<K,V>, Hash,MapKeyOfT> _ht;
	};

	void test_map()
	{
		unordered_map<string, string> dict;
		dict.Insert(make_pair("sort","排序"));
		dict.Insert(make_pair("string", "字符串"));
		dict.Insert(make_pair("left", "左"));
		dict.Insert(make_pair("right", "右"));
		for (auto& ch : dict)
		{
			cout << ch.first << " " << ch.second << endl;
		}


		unordered_map<string,string>::iterator u1 = dict.begin();
		while (u1 != dict.end())
		{
			cout << u1->first << ":" << u1->second << endl;
			++u1;
		}
		
		
	}
	void test01()
	{
		unordered_map<string, int>s1;
		string arr[] = { "苹果","西瓜","苹果","橘子","西瓜" };
		for (auto& ch : arr)
		{
			s1[ch]++;//ch在就++,不在就插入并且返回0;
		}
		for (auto& kv : s1)
		{
			cout << kv.first << ":" << kv.second << endl;
		}

	}
}

unordered_set

#pragma once
#include"HashTable.h"
namespace lrx
{
	template<class K, class Hash = HashFunc<K>>//把缺省的套在这一层,平常不用底层的哈希
	class unordered_set
	{
	
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:

		typedef typename HashBucket::HashTable<K, K, Hash, SetKeyOfT>::iterator iterator;
		
		iterator begin()
		{
			return _ht.begin();
		}

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

		//bool Insert(const K& key)
		pair<iterator, bool> Insert(const K&key)
		{
			return _ht.insert(key);//直接套一层外壳
		}
	private:
		//跟之前红黑树底层实现map和set一样,由第二个参数决定我们这里存什么;
		HashBucket::HashTable<K, K, Hash, SetKeyOfT> _ht;
	};
	void TestSet()
	{
		unordered_set<int> h1;
		h1.Insert(1);
		h1.Insert(3);
		h1.Insert(2);
		h1.Insert(5);
	}
	void TestIterator()
	{
		unordered_set<int> h1;
		h1.Insert(1);
		h1.Insert(3);
		h1.Insert(5);
		h1.Insert(2);
		h1.Insert(9);

		
	}
}

set库里面还多了一个仿函数,就是我们在比较的时候我们自己写的==,库里面给的仿函数,可以自己写,针对不同类型的不同需求;

这里就有一个问题:一个类型K去做set和unordered_set的模板参数有什么要求?

set:1.要求能比较大小,要求支持小于比较,要想要大于比较,换一下参数顺序即可;(库里是默认less小于比较);2.或者显示提供比较的仿函数

unordered_set:1.K类型对象可以转换成整形取模或者支持提供转成整形的仿函数 

 2.K类型对象可以支持等于比较,(set小于往左走,大于往右走,间接的支持了等于)或者提供等于比较的仿函数;

总之哈希映射是一种思想,我们上面主要介绍了直接定值法和除留取余法;

下面介绍两个哈希的应用

4.1 位图

4.1.1 位图概念
1. 面试题(K的模型:判断在还是不在)
40 亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在
40 亿个数中。(40亿大概就是16gb,内存中几乎是存不下的)
1. 遍历,时间复杂度 O(N)(太慢)
2. 排序 (O(NlogN)) ,利用二分查找 : logN(数据太大,只能放在磁盘文件上,不好支持二分查找,效率慢)
3.搜索树喝哈希表(都不太行,内存中存不下)
位图解决(用的直接定址法,就没有哈希冲突了)
一个位映射标记值,1就是在,0就是不在;
数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一
个二进制比特位来代表数据是否存在的信息,如果二进制比特位为 1 ,代表存在,为 0
代表不存在。比如:

2. 位图概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用
来判断某个数据存不存在的。

具体步骤如下:

  1. 首先,通过将 x 除以 8,确定位所在的字符索引 i。这将确定 _bits 中的哪个字符存储了该位。
  2. 接着,通过将 x 对 8 取模,确定位在字符中的偏移量 j。这将确定 _bits[i] 字符中的哪个位被操作。
  3. 最后,使用位操作符 |=1 左移 j 位得到的掩码与 _bits[i] 进行按位或操作。这将将 _bits[i] 字符对应位置上的位标记为 1,而其他位保持不变。

该过程的目的是将指定位置 x 的位设置为 1,而其他位保持不变。通过使用位操作符和位掩码,可以轻松实现这一目标。

需要注意的是,根据你提供的代码,bit_set 类的 _bits 成员变量是一个 vector<char> 类型,用于存储位集合的实际位。每个 char 元素都包含 8 个位,因此 _bits 向量的大小是根据 N 的值计算得出的。在 set() 函数中,通过对 _bits[i] 执行按位或操作,可以将指定位置 x 的位设置为 1。

#pragma once
#include<iostream>
#include<vector>
using namespace std;
namespace lrx
{
	template<size_t N>
	class bit_set
	{
	public:
		bit_set()
		{
			_bits.resize(N/8+1, 0);//如果要开10个位,10/8==1,9会越界,+1防止类似情况出现;
		}
		//或等
		void set(size_t x)//把x这个位置变成1
		{
			//先除8确认在哪个char
			size_t i = x / 8;
			//之后在模8看是第几个char
			size_t j = x % 8;
			//左移右移是升阶降阶,而非正常的方向
			//把char对应的这个位标记为1,其他位不动,
			_bits[i] |= (1 << j);//1或0=1,0或0=0,1或1=1;0与其他位或不会改变;这里可能原本就是1;
			//(1 << j)是将1左移j位;
		}
		//与等
		void reset(size_t x)//把值干掉(其他位不变,把第j位变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:
		vector<char> _bits;
	};
	void test_bit_set()
	{
		bit_set<100> bs1;
		bs1.set(8);
		bs1.set(9);
		bs1.set(20);

		cout << bs1.test(8) << endl;
		cout << bs1.test(9) << endl;
		cout << bs1.test(20) << endl;


	}
	
}

 至此有了位图的实现上面的问题得到了解决

 

如果出现这种情况怎么解决,这是一个kv的模型,库里面已经有位图了,我们只需要开两个位图,表示三种情况即可,之前四十亿个数据里面找大约要开512mb的内存,现在开两个位图就是1g的内存;

三个位图就可以表示出8种出现次数的状态;

	template<size_t N>
	class two_bit_set
	{
	public:
		void set(size_t x)
		{
			bool inset1 = _bs1.test(x);
			bool inset2 = _bs2.test(x);

			//00
			if (inset1 ==false && inset2==false)
			{
				//->01
				_bs2.set(x);
			}
			else if (inset1 == false && inset2 == true)
			{
				//->10
				_bs1.set(x);
				_bs2.reset(x);
			}
		}

		void print_once()
		{
			for (int i = 0; i < N; i++)
			{
				//01
				if (_bs1.test(i) == false && _bs2.test(i) == true)
				{
					cout << i << endl;
				}
			}
			cout << endl;
		}
		void print_twice()
		{
			for (int i = 0; i < N; i++)
			{
				if (_bs1.test(i) == true && _bs2.test(i)==false)
				{
					cout << i << " ";
				}
			}
			cout << endl;
		}
	private:
		bit_set<N> _bs1;
		bit_set<N> _bs2;
	};
	void test_two_bit_set()
	{
		int arr[] = { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 
        2, 3, 3, 4, 2,3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 
		4, 4, 5, 2, 3,3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 
		3, 4, 2, 3, 3,4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 
		6, 2, 3, 3, 4,3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6,
		4, 5, 5, 6, 5,6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2,
		3, 3, 4, 3, 4,4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4,
		4, 5, 4, 5, 5,6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3,
		4, 3, 4, 4, 5,3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6,
		5, 6, 6, 7, 3,4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5,
		6, 6, 7, 5, 6,6, 7, 6, 7, 7, 8 };
		two_bit_set<256>tbs1;
		for (auto ch : arr)
		{
			tbs1.set(ch);
		}
		tbs1.print_once();
		tbs1.print_twice();

	}

	
}

位图也可以达到排序加去重的目的

优点:快,节省空间;没有冲突(直接定值法)

缺点只能用于计算整数;但是如果出现字符串这种类型并且数据量很大这种情况怎么办?

--我们引出布隆过滤器这个结构解决这个问题;

布隆过滤器
如果字符串来了我们给字符串*某个值,之后参照上面的位图的设计思想来设计的话,会出现下面的问题:字符串的范围是无限的,不像整型是42亿9千万这么多,所有会存在相同字面出现冲突;

存在误判:

在:是不准确的,存在误判的

不在:准确的,不存在误判

所以上面的思想是需要改进的;

首先要明确的一点是误判是无法去掉的,但是考研优化,降低误判率;

--每个值多去映射几个位;

只要有一个位置没有冲突,就会达到降低误判率,

理论而言:一个值映射的位越多,误判概率就会越低,但是你映射的过多就会消耗太多的空间,布隆过滤器的优势就是空间小,如果大肆的去开空间一直去映射,它的优势性就会降低;

在黑名单查找的情况下就可以用布隆过滤器:以此来提升效率

下面是对于布隆过滤器的实现,布隆过滤器在c++(stl)标准库里面没有;

 公式计算出来插入一个值要开4.2个位置;

下面的对于布隆过滤器的简单实现

//下面三个算法是借鉴了别人写的高性能算法:

struct HashBKDR//目标是把string转成一个无符号的整形,之后就可以取模了;
{
   //BKDR算法
   size_t operator()(const string & key)
   {
      size_t val = 0;
      for (auto ch : key)
      {
         val *= 131;
         val += ch;
      }
       return val;
   }
    
};


struct HashAP
{
    size_t operator()(const string& key)
    {
        size_t hash = 0;
        size_t ch;
        for (size_t i = 0; i<key.size(); i++)
        {
            if ((i & i) == 0)
            {
                hash ^= ((hash << 7) ^ key[i] ^ (hash >> 3));
            }
            else
            {
                hash ^= (~((hash << 11) ^ key[i]^ (hash >> 5)));
            }
        }
        return hash;
    }
};

struct HashDJB
{
    size_t operator()(const string& Key)
    {
        size_t hash = 5381;
        for(auto ch:Key)
        {
            hash += (hash << 5) + ch;
        }
        return hash;
    }
};
//N表示准备要映射N个值

template<size_t N,class K =string,class Hash1=HashBKDR,class Hash2=HashAP,class Hash3=HashDJB>
//任何值都可以被转成一个整形,所以给一个模板参数
class BloomFilter
{
public:
    void Set(const K&key)//把对应的比特位set成1;
    {
        //直接去算位置
        size_t hash1 = Hash1()(key)%(_radio * N);//Hash1是一个仿函数
        //用除留余数法再缩小一下,因为例如开了五千的空间,但是字符串转成整形个数可能是八千,所以得除一下
        _bits.set(hash1);

        size_t hash2 = Hash2()(key) %( _radio * N);//Hash1是一个仿函数
        _bits.set(hash2);

        size_t hash3 = Hash3()(key) % (_radio * N);//Hash1是一个仿函数
        _bits.set(hash3);

    }
    bool Test(const K& key)//不是看某一个位,而是看三个位
    {
        size_t hash1 = Hash1()(key) % (_radio * N);
        if (!_bits.test(hash1))//如果是真不能说明在,假就一定不在,有一个位为0就得return false
            return false;//不在一定准确
        size_t hash2 = Hash2()(key) % (_radio * N);
        if (!_bits.test(hash2))
            return false;//不在一定准确
        size_t hash3 = Hash3()(key) % (_radio * N);
        if (!_bits.test(hash3))
            return false;//不在一定准确
        
        return true;//这里的在可能存在误判

    }
private:
    const static size_t _radio = 5;
    bitset<_radio* N>_bits;

    //bitset<_radio* N>*_bits=new std::bitset<_radio*N>;//这里调用一个匿名对象,让它自己
    //调用它的构造函数初始化

};
void TestBloomFilter1()
{
    BloomFilter<10>BF1;
    string arr[] = { "苹果","西瓜","苹果","橘子","西瓜" };
    for (auto& ch : arr)
    {
        BF1.Set(ch);
    }
    for (auto& str : arr)
    {
        cout<<str<<": " << BF1.Test(str) << endl;//判断它们在不在
    }
    cout << endl;
    string arr2[] = { "苹果1","西瓜22222","11苹果","橘子","西瓜" };
    for (auto& str : arr2)
    {
        cout <<str<<": " << BF1.Test(str) << endl;//判断它们在不在
    }
   
}

 库里面位图有些小瑕疵,如果这里你用库里面的ratio并且这个值给的很大的话,这里是我们定义的变量是在栈区开的,如果ratio开的很大,这个位图开的值就会很大,在很大的情况下就有可能把栈撑爆了

堆区的空间大于栈区,所以我们可以把这个位图定义在堆区,这里就把这里定义成一个指针;

 

 

由于是结构体指针,调用函数的时候得用->; 

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给 k 个计
数器 (k 个哈希函数计算出的哈希地址 ) 加一,删除元素时,给 k 个计数器减一,通过多占用几倍存储
空间的代价来增加删除操作。

这样空间消耗增多,优势削弱了;

下面是应用:

给两个文件,分别有100亿个query,我们只有1g内存,如何找到两个文件的交集?

1G大概是10亿字节,1m是100万字节;(数量级,非精确的)

这里就大约是300g,假设两个文件叫A和B

布隆这里也用不了,布隆有误判,这里的情景不允许出现错误;

用哈希切分:

如果切成300个小文件,对应一找,每个就是1g,内存可以使用;但是这样太慢了,a部分和b都得找一遍;

这样可以用Hash(),无论是BKDR还是DP算法都可以,依次读取文件A的查询(query),把它转成整形,i=Hash(query)%1000,这个query就进入编号为A i的小文件,从A0---A999;

另外一遍也是,B被切成1000,也是从B0---B999

之后编号相同的小文件,可以一起找交集,先放到内存中set里面去个重,之后再判断在不在,在就是交集;

我们把Query通过hash(例如BKDR)转成了整形,之后去模。这里的核心:相同的query,一定进入相同编号的小文件;

等加入到内存中,可以用哈希等数据结构解决;因为A和B是用同一个哈希函数转出来的,值肯定相同,模完之后的i肯定也相同,之后哈希进入的Ai,和Bi,不会存在A1和B2的值相同,这是哈希保证的;

这种算法的本质叫做哈希切分;(相当于相同的查询,进入到相同编号的文件,等于把他们的范围缩小了,进行了多次比较,最后对应小文件找交集)如果切分完,某个子文件很大,进不去内存怎么办?--把我们设计的算法设计成递归,在对这里切一次,换一个哈希函数,这次就得具体算一次文件大小,当成一个子问题处理;

给一个超过100G大小的log fifile, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
与上题条件相同,如何找到top KIP

跟上面同样的思路,注意建小堆; 

哈希切割的本质不是平均切!!!它是让相同的ip进入相同的小文件,这个ip不可能在不同文件,之后再建立堆,这些问题就会解决

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/663753.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ubuntu20.04,samba服务器搭建。

0.前言 推荐个电视剧吧&#xff0c;百看不厌&#xff0c;《雍正王朝》。 这篇博客没什么技术含量&#xff0c;纯粹的表现一下我的勤劳。 1.Ubuntu 安装 终端输入 $ sudo apt install samba samba-common 配置需要共享的目录 # 新建目录&#xff08;自定义&#xff09;&am…

CarFramework打造无缝用户体验:提升汽车信息娱乐和控制

CarFramework框架解析 CarFramework&#xff08;汽车框架&#xff09;是Android Automotive平台上的一个关键框架&#xff0c;它提供了专门针对车辆应用程序开发的功能和工具。CarFramework通过提供一组API和服务&#xff0c;简化了与车辆硬件和车辆特定功能的交互。 CarFram…

被ChatGPT骗了!再用ChatGPT可要小心了。

被ChatGPT戏耍的周末 1. 被ChatGPT戏耍全过程2. 拆穿ChatGPT的把戏3. AIGC与内容安全 1. 被ChatGPT戏耍全过程 电动垂直起降飞行器&#xff08;eVTOL&#xff0c;Electric Vertical Takeoff and Landing&#xff09;技术越来越成熟&#xff0c;为了解下相关产品我周末打开了Cha…

Android Java判断密码强度 强度显示

1&#xff0c;正则表达式 密码强度的正则表达式 public static final String WEAK_PATTERN "(^(?.*[0-9])(?.*[a-z])[0-9a-z]{6,18}$)|(^(?.*[0-9])(?.*[A-Z])[0-9A-Z]{6,18}$)";//全数字字母&#xff08;大/小&#xff09;public static final String MEDIUM_…

61、基于51单片机无线蓝牙音乐喷泉控制系统设计(程序+原理图+PCB源文件+参考论文+参考PPT+元器件清单等)

方案选择 单片机的选择 方案一&#xff1a;AT89C52是美国ATMEL公司生产的低电压&#xff0c;高性能CMOS型8位单片机&#xff0c;器件采用ATMEL公司的高密度、非易失性存储技术生产&#xff0c;兼容标准MCS-51指令系统&#xff0c;片内置通用8位中央处理器(CPU)和Flash存储单元…

MySql基础教程(一):创建删除选择数据库

MySql基础教程(一)&#xff1a;创建删除选择数据库 1、创建数据库 创建一个名为 CSDN 的数据库 CREATE DATABASE CSDN;执行成功&#xff0c;数据库CSDN创建成功。 2、删除数据库 删除名为 CSDN 的数据库 DROP DATABASE CSDN;执行成功&#xff0c;CSDN数据库删除成功。 3…

【Java技术专题】「入门到精通系列教程」零基础带你认识网络请求工具鼻祖之HttpClient开发实战指南(执行请求篇)

零基础带你认识网络请求工具鼻祖之HttpClient开发实战指南 前言HttpClient的作用HttpClient可以实现什么&#xff1f;HttpClient无法实现什么&#xff1f; HttpClient的使用基础执行请求HTTP 请求查询字符串也可以从独立的参数中来生成 HTTP 响应HTTP 实体实体类型重复实体使用…

重新审视长时间序列预测:关于线性映射的调查

摘要 近年来&#xff0c;长时间序列预测得到了极大的关注。虽然有各种专门的设计来捕捉时间依耐性&#xff0c;但以前的研究表明&#xff0c;与其他复杂的架构相比&#xff0c;单一的线性层可以实现有竞争力的预测性能。在本文中&#xff0c;我们彻底调查了最近提出的方法内在…

微信小程序开发21__Echarts的应用

Echarts 是一个使用JS实现的开源可视化库&#xff0c; 其官网是 https://echarts.apache.org . 它提供了常规的折线图、柱状图、散点图、饼图、K线图等&#xff0c; 还支持图与图之间的混搭。 Echarts 的微信小程序版本的Github为 https://github.com/ecomfe/echarts-for-…

MySQL数据库日志管理、备份与恢复

目录 一、MySQL 日志管理 二、数据备份的重要性 造成数据丢失的原因 三、数据库备份的分类 1 、从物理与逻辑的角度 &#xff08;1&#xff09;备份划分 &#xff08;2&#xff09; 物理备份方法 2、 从数据库的备份策略角度 四、常见的备份方法 1、物理冷备 2、专用备…

天气热了,三叔请你吃瓜

目录 前言预备知识画一个完整的西瓜加些纹路切西瓜参考资料前言 六月份转眼就过去一半了,气温逐渐走高,每次经过小区门口的时候都会被那家水果店铺的吆喝声吸引住,“正宗南汇8424西瓜只要3元一斤啦”,每每都想冲进店里抱一个回来,可碍于囊中羞涩,只好作罢,晚餐过后闲暇…

Postman | 一分钟掌握Pre-request Script | 外部库的使用

简介 Postman内部提供了13种外部库&#xff0c;可以直接在前置请求脚本和后置请求脚本中使用&#xff0c;如果需要其他外部库&#xff0c;只需要使用如下方式引入即可&#xff1a; require(moduleName:String):function → * 一些内部库使用的例子 postman可用的外部库官网列…

[第一章 web入门]afr

afr_1 题目开头已经提示说是任意文件读取漏洞 所以这里还需要复习一下php伪协议php://filter 的作用 读取源代码并进行base64编码输出&#xff0c;不然传入的参数会直接当做php代码执行就看不到源代码内容了。php://filter即使在allow_url_fopen和allow_url_include双off情况下…

Day21 实战篇——Jmeter接口测试之案例实战——添加线程组、添加HTTP请求、获取所有学生接口

Day21 实战篇——Jmeter接口测试之案例实战——添加线程组、添加HTTP请求、获取所有学生接口 文章目录 Day21 实战篇——Jmeter接口测试之案例实战——添加线程组、添加HTTP请求、获取所有学生接口1、添加线程组2、添加HTTP请求3、获取所有学生接口3.1 Jmeter察看结果树中乱码:…

03_运行时数据区

目录 一、概述二、线程1、JVM系统线程 一、概述 二、线程 1、JVM系统线程

调用万维易源API实现天气预测

目录 1. 作者介绍2. 关于理论方面的知识介绍2.1 天气预测简介2.2 预测方法 3. 代码实现3.1 需要安装的包3.2 部分代码3.3 实验结果 1. 作者介绍 房庚晨&#xff0c;男&#xff0c;西安工程大学电子信息学院&#xff0c;22级研究生 研究方向&#xff1a;机器视觉与人工智能 电子…

【数据库四】MySQL备份与恢复

MySQL备份与恢复 1.数据库备份的分类1.1 数据备份的重要性1.2 数据库备份的分类1.3 常见的备份方法 2.MySQL完全备份与恢复2.1 MySQL完全备份2.2 数据库完全备份分类2.3 MySQL物理冷备份及恢复2.4 数据迁移DST2.5 mysqldump进行逻辑备份2.5.1 mysqldump备份数据库2.5.2 mysqldu…

基于卡尔曼滤波进行四旋翼动力学建模(SimulinkMatlab)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

下载routeview网站上的BGP更新数据

需求 给出起始时间和终止时间&#xff0c;从routeview网站上&#xff0c;下载在这两者之间的所有数据到本机&#xff0c;以便于进行后续的分析工作。 例如&#xff1a;2022.2.23 - 2022.2.26 主要流程 主流程在遍历每个月中实现 当月份小于10时&#xff0c;前面加‘0’&am…

算力军备竞赛白热化 “卖铲人”联想集团竞争力如何?

继微软通过OpenAI推出GPT系列、谷歌推出Bard和PaLM-E2之后&#xff0c;国内AI大模型也呈百家争鸣态势&#xff0c;年初至今&#xff0c;国内科技巨头几乎都发布了自研AI大模型产品&#xff0c;AI竞赛全面升级的背后&#xff0c;是全球科技巨头们对算力的争夺&#xff0c;作为算…