c++|unordered系列关联式容器(unordered_set、unordered_map介绍使用+哈希结构)

news2024/11/26 12:19:11

目录

一、unordered_set的介绍与使用 

1.1unordered_set介绍

1.2unordered_set使用

 2.2.1构造 

 2.2.2容量

 2.2.3修改

二、unordered_map的介绍与使用

2.1unordered_map介绍

2.2unordered_map使用

 2.2.1构造

 2.2.2容量

 2.2.3修改 

 三、底层结构(哈希)

3.1哈希概念

3.2哈希函数(引出哈希冲突)

3.3哈希冲突解决

3.3.1闭散列(开放定址法)

3.3.2开放定址法实现哈希

3.3.3开散列(链地址法)实现哈希

3.3.4探究哈希表的大小

3.3.5开散列和闭散列的比较 


在前面篇章,我们学习过由c++98提供的关联式容器的概念以及一些具体关联式容器,那么在这里的继续延续几个由c++11提供的unordered关联式容器来为学习哈希结构做铺垫。其次, 底层为红黑树结构的一系列关联式容器,在查询时效率可达到log N,但与此相比unordered关联式容器可以达到O(1)效率,这也是我们为什么要学习它的原因。

一、unordered_set的介绍与使用 

1.1unordered_set介绍

1.unordered_set与set一样元素key与value是一一对应的,且是唯一的,set中的key不能在容器中修改(元素总是const),但是可以插入和删除,且元素是去重的。

2.unordered_set与set不一样的是,其底层是由数组实现,数组存储的是<key,key>,且节点值的顺序没有按照特定的顺序排序,为了能在常数范围内找到可以所对应的key,unordered_set将相同哈希值的键值对放在相同的桶中(比较抽象,待后续分析)。

3.所以unordered_set访问单个元素要比set快,但它通常在遍历元素子集的范围迭代方面效率低,因为其底层是个数组,遍历数组的效率那肯定就低咯。

unordered_set第一个参数是key,表示值也表示键,第二个参数代表底层的实现结构,叫做哈希表,本质就是个数组,且数组中的值的顺序是不确定的,但元素是去重的,其他参数暂时不做了解。

1.2unordered_set使用

 2.2.1构造 

函数声明功能介绍
explicit unordered_set ();
构造空unordered_set
template <class InputIterator>
unordered_set ( InputIterator first, InputIterator last,
size_type n = /* see below */
const hasher& hf = hasher(),
const key_equal& eql = key_equal(),
const allocator_type& alloc = allocator_type() );
用[first,last)区间中的元素构造unordered_set
unordered_set(const unordered_set& ust);
拷贝构造

其中n表示:

最小初始存储桶数。
这不是容器中的元素数,而是构造时内部哈希表所需的最小插槽数。
如果未指定此参数,则构造函数会自动确定此参数(以取决于特定库实现的方式)。

#include <iostream>
#include <unordered_set>
using namespace std;


int main()
{
	int arr[] = { 2,34,6,6,56,8,321,9,4,88 };

	unordered_set<int> us(arr, arr + sizeof(arr) / sizeof(arr[0]));//迭代器区间构造

	for (auto e : us)
	{
		cout << e << " ";
	}
	cout << endl;

	unordered_set<int> us1(us);//拷贝构造
	for (auto e : us1)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

 输出结果:

2.2.2容量

函数声明功能介绍
bool empty() const;
检测unordered_set是否为空,空返回true,否则返回false
size_type size() const;
返回unordered_set中有效元素的个数
size_type max_size() const;
返回unordered_set能够存储的最大容量
#include <iostream>
#include <unordered_set>
using namespace std;


int main()
{
	int arr[] = { 2,34,6,6,56,8,321,9,4,88 };

	unordered_set<int> us(arr, arr + sizeof(arr) / sizeof(arr[0]));//迭代器区间构造

	for (auto e : us)
	{
		cout << e << " ";
	}
	cout << endl;

	cout << boolalpha << us.empty() << endl;//bool值形式打印
	cout << us.size() << endl;
	cout << us.max_size() << endl;

	return 0;
}

 输出结果:

 2.2.3修改

函数声明功能介绍
pair<iterator,bool> insert(const value_type& x)在unordered_set中插入元素
void erase(iterator position)删除迭代器所指向position位置上的元素
size_type erase(const key_type& x)删除unordered_set中值为x的元素,返回删除的元素的个数
void erase(const_iterator first,const_iterator last)删除迭代器区间[first,last)中的元素
void swap(set& ust)交换两个unordered_set中的元素
void clear()将unordered_set中的元素清空
iterator find(const key_type& x) const返回unordered_set中值为x的元素的位置,未找到,返回end()
size_type count(const key_type& x) const返回set中值为x的元素个数,因为set会去重,所以要么返回1,要么返回0
#include <iostream>
#include <unordered_set>
using namespace std;


int main()
{
	int arr[] = { 2,34,6,6,8,9,4 };

	unordered_set<int> s(arr, arr + sizeof(arr) / sizeof(arr[0]));//迭代器区间构造

	unordered_set<int>::iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;

	it = s.find(6);//返回元素的当前位置
	cout << *it << endl;
	cout << s.count(6) << endl;

	size_t num = s.erase(6);
	cout << num << endl;

	s.erase(s.begin());//删除迭代器当前位置元素
	it = s.begin();

	while (it != s.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;

	s.erase(s.begin(), s.end());//删除迭代器区间中的元素

	return 0;
}

 输出结果:

二、unordered_map的介绍与使用

2.1unordered_map介绍

1.unordered_map与map一样也是存储<key,value>键值对的关联式容器,通过key找到value,且元素是去重的。

2.unordered_map与map不一样的是,其底层是由数组实现,数组存储的是<key,value>,且节点值的顺序没有按照特定的顺序排序,为了能在常数范围内找到可以所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中(比较抽象,待后续分析)。

3.所以unordered_map通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率低,因为其底层是个数组,遍历数组的效率那肯定就低咯。

unordered_map容器的第一个参数是key,代表键,第二个参数代表key对应的value,第三个参数代表底层的实现结构,叫做哈希表,本质就是个数组,且数组中的值的顺序是不确定的,但元素是去重的,其他参数暂时不做了解。

 2.2unordered_map使用

对于unordered_map的接口使用,大部分跟map的使用是差不多的,所以我就直接使用,不做过多的解释。

 2.2.1构造

函数声明功能介绍
explicit unordered_map ();
构造空unordered_map
template <class InputIterator>
unordered_map (InputIterator first, InputIterator last,
const hasher& hf = hasher(),
const key_equal& eql = key_equal(),
const allocator_type& alloc = allocator_type());
用[first,last)区间中的元素构造unordered_map
unordered_map (const unorderedmap& x);
拷贝构造
unordered_map& operator= (const unordered_map& x);
unordered_map& operator=
(initializer_list<value_type> il);

通过对象进行初始化。

通过初始化列表进行初始化,C++11的用法

#include <iostream>
#include <unordered_map>
using namespace std;

int main()
{
	unordered_map<int, int> unmap{ {8,1},{3,2},{20,3}, {4,4} };//通过初始化列表进行初始化
	for (auto e : unmap)
	{
		cout << e.first << ":" << e.second << endl;
	}
	cout << endl;

	unordered_map<int, int> unmap2(unmap);
	for (auto& e : unmap2)
	{
		cout << e.first << ":" << e.second << endl;
	}
	cout << endl;

	unordered_map<int, int> unmap3(unmap.begin(), unmap.end());
	for (auto& e : unmap)
	{
		cout << e.first << ":" << e.second << endl;
	}

	return 0;
}

输出结果:

通过结果也可以看出,其值顺序是不确定的,至于为什么是这样,由其底层结构性质决定,不是三言两语就能解决的,待后续叙述。 

  2.2.2容量

函数声明功能简介
bool empty() const;
检测unordered_map中元素是否为空,是,返回true,否,返回false
size_type size() const;
返回unordered_map中有效元素的个数
mapped_type& operator[] (const key_type& k);

返回key对应的value。(支持插入,修改。若key不存在,则插入value,并返回。若存在,修改value,并返回)。

mapped_type& at (const key_type& k);

const mapped_type& at (const key_type& k) const;
返回key对应的value。(支持修改,不支持插入。若key不存在,抛异常。若存在,修改value,并返回)
#include <iostream>
#include <unordered_map>
using namespace std;

int main()
{
	unordered_map<int, int> unmap{ {8,1},{3,2},{20,3}, {4,4} };//通过初始化列表进行初始化
	for (auto e : unmap)
	{
		cout << e.first << ":" << e.second << endl;
	}
	

	cout << unmap.size() << endl;
	cout << unmap.empty() << boolalpha << endl;

	unmap[8] = 8;//修改
	unmap[3] = 3;
	unmap[20] = 20;
	unmap[4] = 5;
	unmap[7] = 7;//插入
	//unmap.at(6) = 3;抛异常
	cout << endl;
	for (auto& e : unmap)
	{
		cout << e.first << ":" << e.second << endl;
	}
	cout << endl;


	return 0;
}

输出结果:

2.2.3修改 

函数声明功能介绍
pair<iterator,bool> insert(const value_type& x)在unordered_map中插入键值对x

void erase(iterator position)

size_type erase(const key_type& x)

void erase(iterator first,iterator last)

删除position位置上的元素

删除键值为x的元素

删除[first,last)区间中的元素

void swap(unordered_map& ump)交换两个unordered_map中的元素
void clear()将map中的元素清空

iterator find(const key_type& x)

const_iterator find(const key_type& x) const

返回key在哈希桶的迭代器位置,未找到返回end()

size_type count(const key_type& x)返回哈希桶中关键码为key的键值对的个数,因为key是唯一的,所以该函数的返回值要么为0,要么为1,因此也可以用该函数来检测一个key是否在哈希桶中

#include <iostream>
#include <unordered_map>
using namespace std;

int main()
{
	int arr[] = { 10,2,3,4,5,5,3,3,8 };
	unordered_map<int, int> unmap;

	for (auto e : arr)
	{
		unmap.insert(make_pair(e, e));
	}

	for (auto& e : unmap)
	{
		cout << e.first << ":" << e.second << endl;
	}

	unmap.erase(8);
	unordered_map<int, int>::iterator it = unmap.begin();
	it = unmap.find(3);
	cout << it->first << ":" << it->second << endl;
	it = unmap.find(1);//返回end()位置迭代器
	if (it == unmap.end())
	{
		cout << "未查询到该元素" << endl;
	}
	

	unordered_map<int, int> umap;
	umap.swap(unmap);
	cout << "unmap whether is empty():" << boolalpha << unmap.empty() << endl;
	cout << "实际3的元素有几个:" << umap.count(3) << endl;
	for (auto& e : umap)
	{
		cout << e.first << ":" << e.second << endl;
	}
	
	return 0;
}

输出结果: 

当然他们的使用还有一些其他的接口,我们暂且放一放,接下来学习它的底层结构。 

 三、底层结构(哈希)

3.1哈希概念

对于顺序结构,查询单个元素的时间复杂度是O(N),在平衡树中,查询单个元素的时间复杂度是O(log N),这两种时间复杂度的结果取决于搜索过程中元素的比较次数。但是对于理想程度来说,肯定是查询时间越小越好,那么为了达到此结果,哈希结构能够满足,可以不经过任何比较,一次直接从表中取到要搜索的元素。

哈希结构:构造一种存储结构,通过某种函数使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。说白了就是存储的位置和存储的值的映射关系,只要满足映射关系就称为哈希结构。

当向该结构中:

  • 插入元素

        根据待插入元素的关键码(key),以此函数计算出该元素的存储位置并按此位置进行存放

  • 搜索元素

        对元素的关键码进行同样的计算,把求得的函数值当作元素的存储位置,在结构中按此位置            取元素比较,如关键码相等,则搜索成功

 在该哈希结构中使用的函数称为哈希(散列)函数,构造出来的结构称为哈希表或者称为散列表,接下来对如何通过哈希函数来构造一张哈希表。

3.2哈希函数(引出哈希冲突)

 1.直接定址法

取关键字的某个线性函数为散列地址:hash(key) = A*key + B。如图:

由此可看出该方法的优点是简单,均匀,但是缺点就是需事先知道A,B关键字的分布情况,且极其的浪费空间。

那么它的场景就适合查找比较小且连续的情况

2.除留余数法

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数 p作为除数,按照哈希函数:hash(key) = key % p(p<=m),将关键码转换成哈希地址。如图:

由上图可知该哈希函数也能实现哈希结构,不用去找关键字,但是导致的结果就是,会出现同一个地址内出现两个元素,我们把不同关键字通过相同哈希函数计算出相同的哈希地址称为哈希冲突,把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。而引起哈希冲突的原因就是哈希函数设计不够合理。

还有一些哈希函数,做个了解:3.平放取中法 4.折叠法 5.随机数法 6.数学分析法

为了使得哈希函数设计合理,须符合以下原则:

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

设计哈希函数的目的就是为了减少哈希冲突,但是不能够完全避免哈希冲突。

注意:

要区分哈希结构和哈希表。

哈希/散列:K和V的映射关系

哈希表/散列表:K和存储位置的映射关系

3.3哈希冲突解决

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

3.3.1闭散列(开放定址法)

也叫开放定址法,当发生哈希冲突时,如果哈希表未装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个”空位置中去。什么意思呢,又该如何找寻下一个位置,这里提供了两种方式

1.线性探测解决哈希冲突

通过线性探测来实现插入、查找、删除等操作。 

插入:

通过哈希函数获取待插入元素在哈希表中的位置(hash(key) = i % 表的大小)

如果在该位置中没有元素则直接插入新元素,如果该位置中已有元素,则线性往后找寻空位置进行插入元素,如图:

其中9和44发生了哈希冲突,由于9是先插入的新元素,那么44就会遵循线性探测原则,继续往后寻找空位置插入,最先符合空位置的是3地址,如果3地址处也有元素,那么就在5插入,以此类推。当往后的位置都插满时,这时再发生哈希冲突时,就需要去前面寻找空位置插入。

注意:

在一定的空间,随着元素的增多,哈希冲突的概率会不断增大,所以,当达到一定的元素时,需要进行扩容,对于扩容,引入了负载因子/载荷因子的概念,负载因子 = 实际存进去的元素个数/表的大小,一般负载因子会控制在0.7到0.8,由实验表明,当负载因子超过0.8,在查表时CPU缓存不命中按照指数曲线上升。当达到该负载因子时,进行扩容,在进行扩容后,需要对原来的元素在新的空间里重新映射,这从另一方面,也说明其元素顺序是无序的。由于负载因子的控制,元素个数始终保持不会超过7/10,例如:容量为10,元素个数不会超过7个,如果达到7个,会进行相应扩容,元素个数不变,但容量变大了,依然小于7/10。所以在该容量的空间中就必然会出现空位置。

查找:

要查找的元素可能因为哈希冲突,而填到了后面的空位置,所以在进行查找时,需从i = key%capacity位置开始依次线性往后查询,如果找到了,返回该位置,如果该元素确实不存在,当往后查找时,若遇到空位置,则说明元素不存在(因为发生冲突时,插入就是往后线性插入,在查找时为空位置,说明就没有在该位置插入元素),当往后查找的位置都不为空,但是没有该元素时,也要跑到前面需寻找该元素。由于空间中必然会出现空位置,结果就是要么找到元素,返回该元素位置,要么遇到空结束返回空

删除:

采用闭散列删除元素时,不能够直接找到该元素就删除,因为可能会影响其他元素的查询。比如删除9,那么在查询44时,是从44 % 7 = 2位置开始查找,而2位置为空,就会返回该元素不存在的信息,所以为了不影响其他元素查找,线性探测采用标记的伪删除法来删除一个元素。

用三个标记代表该位置的状态,enum State{ EMPTY,EXSIT,DELETE},EMPTY代表该位置是空,EXIST带表该位置有元素,DELETE代表该位置元素已经删除,所以当要查找元素,只需判断该位置的状态是否继续往后查找。

线性探测优点:实现非常简单

线性探测缺点:一旦发生哈希冲突,所有与之冲突都会连在一起,造成数据堆积的效果。也就是关键码占据了可利用的空位置,使得寻找关键码需要多次比较,导致效率降低

2.二次探测解决哈希冲突

已知哈希冲突的缺点,为了避免挨个方式往后查找,二次探测就能够代替,理解起来也很简单,那就不是挨个查找,而是以平放的形式跳跃查找插入。如图:

 4和44发生冲突,4是先插入,为了避免冲突,需要往后寻找空位插入,线性探测是挨个往后查找空位,而二次探测是以平放形式跳跃查找,假设变量i(i>=0),第一次跳跃为1^2 = 1,跳跃一个位置即4+1 = 5 ,发现该位置不为空,那么第二次跳跃,2^2 = 4,4+4 = 8,该位置为空,那么44就插入到8位置。

二次探测优点:实现非常简单

二次探测缺点:与线性探测一样,一旦发生哈希冲突,所有与之冲突都会连在一起,造成数据堆积的效果。也就是关键码占据了可利用的空位置,使得寻找关键码需要多次比较,导致效率降低

接下来通过开放定址法实现哈希结构,这里只提供线性探测方式。

3.3.2开放定址法实现哈希

//HashTable.h

#include <iostream>
#include <vector>
#include <string>
using namespace std;

template<class K>
struct HashFunc
{

	size_t operator()(const K& key)
	{
		return (size_t)key;
	}

};

//特化--目的可以比较字符串
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto e : key)
		{
			//BKDR--一种哈希算法
			hash *= 31;
			hash += e;
		}
		return hash;
	}
};

//开放地址法
namespace open_address
{
    //用枚举存放该表的状态
	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);//给哈希表空间大小初始化为10
		}
        //查找元素
		HashData<K, V>* Find(const K& key)
		{
			Hash hs;
			int hashi = hs(key) % _tables.size();//获取元素的对应哈希表中的下标
			while (_tables[hashi]._state == EXIST)//判断该位置的状态
			{
				if (_tables[hashi]._kv.first == key)//是否有冲突,否,直接返回该位置
				{
					return &_tables[hashi];
				}
				++hashi;//是,往后线性探测查询
				hashi %= _tables.size();//若到达末尾还未查询到,则往前回绕查找
			}
			return nullptr;//查询完还未找到,返回空
		}
        
        //插入
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))//是否已存在该值
				return false;


			//扩容
			/*if ((double)n / _tables.size() >= 0.7)*/
			if (n * 10 / _tables.size() >= 7)//到达负载因子就扩容
			{
				HashTable<K, V> newt(_tables.size() * 2);//每次扩2倍
				//遍历旧表,插入新表
				for (auto& e : _tables)
				{
					if (e._state == EXIST)
					{
						newt.Insert(e._kv);//递归插入到新表
					}
				}

				//浅拷贝交换
				_tables.swap(newt._tables);//交换两个表的指针指向
			}

			//插入元素
			Hash hs;

			int hashi = hs(kv.first) % _tables.size();//获取新表下标
			while (_tables[hashi]._state == EXIST)//判断该位置状态
			{
				++hashi;//若有冲突,线性往后探测,找待插入位置
				hashi %= _tables.size();//若查找到末尾,则往前继续探测待插入位置
			}
			_tables[hashi]._kv = kv;//插入元素
			_tables[hashi]._state = EXIST;//给该位置附一个状态
			n++;//元素+1

			return true;//插入成功,返回true
		}

        //删除
		bool Erase(const K& key)
		{
			if (Find(key) == nullptr)//要删元素不存在
				return false;

			/*int hashi = hash(key) % _tables.size();
			_tables[hashi]._state = DELETE;*/

			HashData<K, V>* ret = Find(key);//返回查询元素位置所在
			if (ret)
			{
				ret->_state = DELETE;//更改要删除元素状态,所以实则并没有将该元素从数组中删掉,在输出该数组内容时,也是通过判断每个位置的状态来进行打印
				--n;//元素-1
			}

            
			return true;
		}


		void Print()
		{
			for (int i = 0; i < _tables.size(); i++)
			{
				if (_tables[i]._state == EXIST)//通过判断该位置状态来进行输出
				{
					cout << _tables[i]._kv.first << ":" << _tables[i]._kv.second << endl;
				}
			}
		}
	private:
		vector<HashData<K, V>> _tables;//哈希表本质就是个数组,每个位置存储的是一个结构体,结构体存储的才是真正的值。
		size_t n = 0;//记录元素个数
	};
	void TestHT1()
	{
		HashTable<int, int> ht;
		int a[] = { 4,14,24,34,5,7,1 };
		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		ht.Insert(make_pair(3, 3));
		ht.Insert(make_pair(3, 3));
		ht.Insert(make_pair(-3, -3));
		ht.Print();
		cout << endl;
		ht.Erase(3);
		ht.Print();
		cout << endl;
		if (ht.Find(3))
		{
			cout << "3存在" << endl;
		}
		else
		{
			cout << "3不存在" << endl;
		}

		ht.Insert(make_pair(3, 3));
		ht.Insert(make_pair(23, 3));
		ht.Print();
	}
}

 测试:

//test.cpp

#include "HashTable.h"

int main()
{

	open_address::TestHT1();
	return 0;
}

 输出结果:

 除了开放定址法,还有一种链地址法来解决哈希冲突

3.3.3开散列(链地址法)实现哈希

也叫链地址法(开链法),对关键码集合通过散列函数(哈希函数)计算散列地址,与闭散列不同的是,具有相同地址的关键码归于同一子集和,每一个子集和称为一个桶,每个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

示意图: 

话不多少,直接来实现开散列,以kv版参照。

//HashTable.h
#include <iostream>
#include <vector>
#include <string>
#include <set>
#include <unordered_set>
using namespace std;

template<class K>
struct HashFunc
{

	size_t operator()(const K& key)
	{
		return (size_t)key;
	}

};

//特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto e : key)
		{
			//BKDR
			hash *= 31;
			hash += e;
		}
		return hash;
	}
};

//哈希桶/链地址法
namespace Hash_Bucket
{
	//存放节点值
	template<class K, class V>
	struct HashNode
	{
		HashNode* _next;
		pair<K, V> _kv;

		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(size_t size = 10)
		{
			_tables.resize(size);//默认给10
		}

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

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;

			Hash hf;

			// 负载因子最大到1,目的是增加空间利用率,尽量使每个桶挂更多的元素
			//if (_n*10 / _tables.size() == 7)
			if (_n == _tables.size())
			{
				//size_t newSize = _tables.size() * 2;
				//HashTable<K, V> newHT;
				//newHT._tables.resize(newSize);

				 遍历旧表
				//for (size_t i = 0; i < _tables.size(); i++)
				//{
				//	Node* cur = _tables[i];
				//	while(cur)
				//	{
				//		newHT.Insert(cur->_kv);
				//		cur = cur->_next;
				//	}
				//}

				//_tables.swap(newHT._tables);//浅拷贝,而链表是一个深拷贝存在,所以此法不行

				vector<Node*> newTables;
				newTables.resize(_tables.size() * 2, nullptr);
				// 遍历旧表
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;

						// 挪动到映射的新表
						size_t hashi = hf(cur->_kv.first) % newTables.size();
						cur->_next = newTables[i];
						newTables[i] = cur;

						cur = next;
					}

					_tables[i] = nullptr;
				}

				_tables.swap(newTables);
			}

			size_t hashi = hf(kv.first) % _tables.size();//获取待插入位置
			Node* newnode = new Node(kv);

			// 头插
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;

			return true;
		}

		Node* Find(const K& key)
		{
			Hash hf;
			size_t hashi = hf(key) % _tables.size();//查找该元素位置
			Node* cur = _tables[hashi];
			while (cur)//遍历链表
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}

				cur = cur->_next;
			}

			return NULL;
		}


		bool Erase(const K& key)
		{
			Hash hf;
			size_t hashi = hf(key) % _tables.size();//待删除位置
			Node* prev = nullptr;//记录cur的前驱节点
			Node* cur = _tables[hashi];
			while (cur)//若cur为空(不管一来为空,还是查找完为空),说明没有该元素,若cur不为空,遍历链表查找该元素
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)//说明第一个元素就是要删除元素,将第一个元素置为空
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;//删除当前节点,并将前驱节点和后驱节点链接
					}
					delete cur;

					return true;
				}

				//未找到该元素,就继续往后迭代查找,直到末尾
				prev = cur;
				cur = cur->_next;
			}

			//未查找到要删除元素
			return false;
		}




		void Some()
		{
			size_t bucketSize = 0;
			size_t maxBucketLen = 0;
			size_t sum = 0;
			double averageBucketLen = 0;

			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				if (cur)
				{
					++bucketSize;
				}

				size_t bucketLen = 0;
				while (cur)
				{
					++bucketLen;
					cur = cur->_next;
				}

				sum += bucketLen;

				if (bucketLen > maxBucketLen)
				{
					maxBucketLen = bucketLen;
				}
			}

			averageBucketLen = (double)sum / (double)bucketSize;

			printf("all bucketSize:%d\n", _tables.size());
			printf("bucketSize:%d\n", bucketSize);
			printf("maxBucketLen:%d\n", maxBucketLen);
			printf("averageBucketLen:%lf\n\n", averageBucketLen);
		}
	private:
		vector<Node*> _tables;
		size_t _n = 0;
	};

	void TestHT1()
	{
		HashTable<int, int> ht;
		int a[] = { 4,14,24,34,5,7,1,15,25,3 };
		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		ht.Insert(make_pair(13, 13));

		cout << ht.Find(4) << endl;
		ht.Erase(4);
		cout << ht.Find(4) << endl;
	}

	void TestHT2()
	{
		string arr[] = { "香蕉", "甜瓜","苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
		HashTable<string, int> ht;
		for (auto& e : arr)
		{
			//auto ret = ht.Find(e);
			HashNode<string, int>* ret = ht.Find(e);
			if (ret)
			{
				ret->_kv.second++;
			}
			else
			{
				ht.Insert(make_pair(e, 1));
			}
		}
	}

	void TestHT3()
	{
		const size_t N = 1000000;

		unordered_set<int> us;
		set<int> s;
		HashTable<int, int> ht;

		vector<int> v;
		v.reserve(N);
		srand(time(0));
		for (size_t i = 0; i < N; ++i)
		{
			//v.push_back(rand()); // N比较大时,重复值比较多
			v.push_back(rand() + i); // 重复值相对少
			//v.push_back(i); // 没有重复,有序
		}

		// 21:15
		size_t begin1 = clock();
		for (auto e : v)
		{
			s.insert(e);
		}
		size_t end1 = clock();
		cout << "set insert:" << end1 - begin1 << endl;

		size_t begin2 = clock();
		for (auto e : v)
		{
			us.insert(e);
		}
		size_t end2 = clock();
		cout << "unordered_set insert:" << end2 - begin2 << endl;

		size_t begin10 = clock();
		for (auto e : v)
		{
			ht.Insert(make_pair(e, e));
		}
		size_t end10 = clock();
		cout << "HashTbale insert:" << end10 - begin10 << endl << endl;


		size_t begin3 = clock();
		for (auto e : v)
		{
			s.find(e);
		}
		size_t end3 = clock();
		cout << "set find:" << end3 - begin3 << endl;

		size_t begin4 = clock();
		for (auto e : v)
		{
			us.find(e);
		}
		size_t end4 = clock();
		cout << "unordered_set find:" << end4 - begin4 << endl;

		size_t begin11 = clock();
		for (auto e : v)
		{
			ht.Find(e);
		}
		size_t end11 = clock();
		cout << "HashTable find:" << end11 - begin11 << endl << endl;

		cout << "插入数据个数:" << us.size() << endl << endl;
		ht.Some();

		size_t begin5 = clock();
		for (auto e : v)
		{
			s.erase(e);
		}
		size_t end5 = clock();
		cout << "set erase:" << end5 - begin5 << endl;

		size_t begin6 = clock();
		for (auto e : v)
		{
			us.erase(e);
		}
		size_t end6 = clock();
		cout << "unordered_set erase:" << end6 - begin6 << endl;

		size_t begin12 = clock();
		for (auto e : v)
		{
			ht.Erase(e);
		}
		size_t end12 = clock();
		cout << "HashTable Erase:" << end12 - begin12 << endl << endl;
	}
}

测试:

//test.cpp
#include "HashTable.h"

int main()
{

	Hash_Bucket::TestHT3();
	return 0;
}

 输出结果:

 

 3.3.4探究哈希表的大小

有研究表面,哈希表的大小最好是一个素数,这样的话能够提供哈希结构的效率。

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

			return __stl_prime_list[__stl_num_primes - 1];
		}

该方法也是STL库中获取素数的方式。

  •  将素数放在一个数组中,两个素数之间的关系接近二倍。
  •  当需要进行扩容时,就从数组中寻找一个比当前素数大的素数作为新的容量。

上面数组中存放了28个素数,最大的素数约等于2^32,约等于哈希表有4GB个数据,每个数据是一个指针,占4个字节大小,假设插入的数据量是整形最大值,那么算下来插入的数据总大小为16GB,超出了哈希表的大小,但是需要考虑每个桶长度,插入这么多个数据,通过除留余数法,必有重复值,每个桶挂的数据也是大量的,所以,总体算下来,哈希表是够用的。

那么在初始化时就可以这样定义,以数组第一个素数作为容量大小:

扩容时从数组中查询下一个素数

3.3.5开散列和闭散列的比较 

开散列每个节点中多了一个指针,看起来比闭散列增加了存储开销,但是它空间利用率高,负载因子大于1的时候才会扩容。

闭散列必须保持大量的空闲空间以确保搜索的效率,二次探测甚至要求负载因子必须小于等于0.7,由于其扩容次数比开散列多,所以其表项所占的空间比哈希桶大的多。

  • 所以在空间利用率上,哈希桶比闭散列更有优势。

在搜索上效率上,无论是开散列还是闭散列,都是通过哈希函数直接映射到待查找位置,然后再查询常数次,所以这两种方式的时间复杂度都是O(1),比红黑树的查找效率高的多。

但相比开散列,闭散列的负载因子越大,哈希碰撞的概率就越大,扩容消耗越大,查询次数越多。

  • 所以,综合考虑,哈希结构的底层也采用了哈希桶结构。

 补充:

在极端情况下,哈希桶的单链表可能会很长,此时可以将该桶改成红黑树结构来提高效率。

通过设置一个链表长度,当单链表的长度超过这个值时,就将桶改成树结构,如AVL树或者红黑树等等。

下一章节,将使用哈希结构封装unordered_set和unordered_map

end~

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

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

相关文章

函数的概念及图像

注&#xff1a; 判断两函数是否相同&#xff0c;只看定义域和对应法则。 1. 函数的定义 一般的&#xff0c;在一个变化过程中有两个变量 x&#xff0c;y。如果对于x在某个变化范围内的每一个确定值&#xff0c;按照某个对应法则&#xff0c;都有唯一确定的值y和他对应。那么y就…

ChatGPT为啥不用Websocket而是EventSource?

点击下方“JavaEdge”&#xff0c;选择“设为星标” 第一时间关注技术干货&#xff01; 免责声明~ 任何文章不要过度深思&#xff01; 万事万物都经不起审视&#xff0c;因为世上没有同样的成长环境&#xff0c;也没有同样的认知水平&#xff0c;更「没有适用于所有人的解决方案…

统计信号处理基础 习题解答10-10

题目 在本题中&#xff0c;我们讨论再生PDF。回顾前面 其中分母与无关。如果选择一个&#xff0c;使得它与相乘时&#xff0c;我们得到与相同形式的PDF&#xff0c;那么后验PDF 将有和相同的形式。例10.1的高斯PDF正是这样的一种情况。现在假设在条件下的的PDF是指数形式&…

Codeforces Round 949 (Div. 2) A~D

A. Turtle and Piggy Are Playing a Game &#xff08;思维&#xff09; 题意&#xff1a; 给出一个整数 x x x &#xff0c;使得 l ≤ x ≤ r l \le x \le r l≤x≤r &#xff0c;其中 l , r l, r l,r 为给定值。同时保证 2 l ≤ r 2l \le r 2l≤r 。 执行以下操作&…

python如何输入回车

Python默认遇到回车的时候&#xff0c;输入结束。所以我们需要更改这个提示符&#xff0c;在遇到空行的时候&#xff0c;输入才结束。 raw_input就是从标注输入读取输入&#xff0c;输入的是什么就是什么。 文档解释&#xff1a; The function then reads a line from input,…

quick4 - hackmyvm

简介 靶机名称&#xff1a;quick4 难度&#xff1a;简单 靶场地址&#xff1a;https://hackmyvm.eu/machines/machine.php?vmQuick4 本地环境 虚拟机&#xff1a;vitual box 靶场IP&#xff08;quick4&#xff09;&#xff1a;192.168.56.104 跳板机IP(windows 11)&…

leetcode:不同的二叉树

class Solution { public:int numTrees(int n) {vector<int> dp(n1);dp[0] 1;dp[1] 1;for(int i 2;i < n;i){for(int j 1;j < i;j) // 当根节点为j时{dp[i] dp[j-1] * dp[i-j];}}return dp[n];} }; /* dp[i] i个不同的数组成的二叉搜索数的个数假设 i 5当根…

零基础非科班也能掌握的C语言知识19 动态内存管理

动态内存管理 1.为什么要有动态内存分配2.malloc和free2.1 malloc2.2 free 3.calloc和realloc3.1 calloc3.2realloc 4.常见的动态内存的错误4.1对NULL指针的解引用操作4.2对动态开辟空间的越界访问4.3对非动态内存开辟的空间free4.4使用free释放⼀块动态开辟内存的⼀部分4.5对同…

kNN算法-概述

所谓kNN算法就是K-nearest neigbor algorithm。这是似乎是最简单的监督机器学习算法。在训练阶段&#xff0c;kNN算法存储了标签训练样本数据。简单地说&#xff0c;就是调用训练方法时传递给它的标签训练样本会被它存储起来。 kNN算法也叫lazy learning algorithm懒惰学习算法…

分享不用会员免费听歌的软件,可听付费,支持随听随下!

今天来点特别的&#xff0c;给你们带来几款全网免费听歌的神器&#xff0c;让你们的音乐之旅不再有障碍&#xff01; 现在&#xff0c;找好听的歌越来越像寻宝一样&#xff0c;动不动就得掏腰包。不过别担心&#xff0c;阿星今天就来分享几款好用的免费听歌app&#xff0c;电脑…

Linux——PXE整体流程

1.自己安装一个CentOS 8的服务器 1&#xff09;手动安装 虚拟硬件配置&#xff1a;2核CPU&#xff0c;4G内存&#xff0c;100G硬盘 2个网卡&#xff08;一个通外网&#xff0c;一个内部使用&#xff09; 软件安装&#xff1a;Server GUI 磁盘分区&#xff1a;使用逻辑卷&#…

实践分享:如何用小程序里的小组件做应用开发?

随着移动互联网的快速发展&#xff0c;小程序等轻量级应用平台日益成为用户获取信息和服务的重要渠道。而小组件也在其中扮演了至关重要的角色&#xff0c;不仅能够提升用户的交互体验&#xff0c;还能帮助开发者高效地构建功能丰富、界面美观的小程序。 本文中&#xff0c;我们…

【Uniapp】uniapp微信小程序定义图片地址全局变量

错误写法&#xff1a; main.js Vue.prototype.$imgUrl 图片地址这么写之后 就发现压根不起作用&#xff1b;获取到的是undefined 正确写法&#xff1a; 返回函数&#xff0c;后面可以拼上OSS图片完整路径 Vue.prototype.$imgUrl (url) > {return ("https://地址…

jmeter性能优化之mysql配置

一、连接数据库和grafana 准备:连接好数据库和启动grafana并导入mysql模板 大批量注册、登录、下单等,还有过节像618,双11和数据库交互非常庞大,都会存在数据库的某一张表里面,当用户在登录或者查询某一个界面时,量少的话体现不出来,量很大的时候一定会有卡的现象, 性…

SpringBoot整合RabbitMQ实现消息延迟队列

环境依赖 SpringBoot 3.1.0 JDK 17 前期准备 安装MQ: liunxdockerrabbitmq安装延迟队列插件 实例 实现延迟队列的一种方式是在 RabbitMQ 中使用消息延迟插件&#xff0c;这个插件可以让你在消息发送时设置一个延迟时间&#xff0c;超过这个时间后消息才会被消费者接收到…

期刊影响因子、分区如何查询

查询期刊影响因子、分区等信息就不得不说到的数据库JCI(Journal Citation Indicator)。 JCR 是一个综合性、多学科的期刊分析与评价报告&#xff0c;它客观地统计Web of Science收录期刊所刊载论文的数量、论文参考文献的数量、论文的被引用次数等原始数据&#xff0c;再应用文…

数据库(29)——子查询

概念 SQL语句中嵌套SELECT语句&#xff0c;称为嵌套查询&#xff0c;又称子查询。 SELECT * FROM t1 WHERE column1 (SELECT column1 FROM t2); 子查询外部语句可以是INSERT/UPDATE/DELETE/SELECT的任何一个。 标量子查询 子查询返回的结果是单个值&#xff08;数字&#xff…

基于xml的Spring应用(理解spring注入)

目录 问题&#xff1a; 传统Javaweb开发的困惑? 问题&#xff1a; IOC、DI和AOP的思想提出 问题&#xff1a; Spring框架的诞生 1. BeanFactory快速入门 2. ApplicationContext快速入门 3. BeanFactory和ApplicationContext的关系 基于xml的Spring应用 1. SpringBean的…

c# 开发的wpf程序闪退,无法用try catch捕获异常

之前开发的一个程序是c#wpf开发&#xff0c;基于.net framework 4.6.1的&#xff0c;一切都是正常的&#xff0c;但是在我重新装了win11后在程序logo出现后直接闪退&#xff0c;报错 返回值为 -1073740791 (0xc0000409)&#xff0c;而且定位到代码时发现是&#xff0c; publi…

AI助教时代:通义千问,让学习效率翻倍?

全文预计1100字左右&#xff0c;预计阅读需要5分钟。 关注AI的朋友知道&#xff0c;在今年5月份以及6月份的开端&#xff0c;AI行业可谓是风生水起&#xff0c;给了我们太多的惊喜和震撼&#xff01;国内外各家公司纷纷拿出自己憋了一年的产品一决雌雄。 国内有文心一言、通义千…