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

目录

一、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++;//元素+1return 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){//BKDRhash *= 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:15size_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.mzph.cn/pingmian/25226.shtml

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

相关文章

【回调函数】

1.回调函数是什么&#xff1f; 回调函数就是⼀个通过函数指针调用的函数。 如果你把函数的指针&#xff08;地址&#xff09;作为参数传递给另⼀个函数&#xff0c;当这个指针被用来调用其所指向的函数 时&#xff0c;被调用的函数就是回调函数。回调函数不是由该函数的实现方…

【PL理论】(12) F#:模块 | 命名空间 | 异常处理 | 内置异常 |:? | 相互递归函数

&#x1f4ad; 写在前面&#xff1a;本章我们将介绍 F# 的模块&#xff0c;我们前几章讲的列表、集合和映射都是模块。然后我们将介绍 F# 中的异常&#xff0c;以及内置异常&#xff0c;最后再讲解一下相互递归函数。 目录 0x00 F# 模块&#xff08;Module&#xff09; 0x01…

Bootstrap框架集成ECharts教程

最新公司项目要在原有的基础上增加一些饼状图和柱状图来统计一些数据给客户&#xff0c;下面就是集成的一个过程&#xff0c;还是很简单的&#xff0c;分为以下几步 1、引入ECharts的包 2、通过ECharts官网或者菜鸟教程直接拿示例代码过来修修改改直接用就可以了 注意&#xf…

Windows关闭自动更新最有效的方法

按WR打开电脑命令框输入“regedit”进入注册表 依次点击以下几个 右击新建一个“DWORD(32位)值”&#xff0c;命名为”FlightSettingsMaxPauseDays“ 右边选择十进制&#xff0c;左边填写暂停更新的天数 打开windows更新&#xff0c;进入高级选项 选择暂停更新的天数&#xff…

Fortigate防火墙二层接口的几种实现方式

初始配置 FortiGate出厂配置默认地址为192.168.1.99&#xff08;MGMT接口&#xff09;&#xff0c;可以通过https的方式进行web管理&#xff08;默认用户名admin&#xff0c;密码为空&#xff09;&#xff0c;不同型号设备用于管理的接口略有不同。 console接口的配置 防火墙…

java并发控制(猴子摘桃例子)

【问题】 有n个桃子&#xff0c; 猴子A每次固定摘2个&#xff0c;猴子B每次固定摘3个&#xff0c;这2只猴子不断摘桃子直到剩余桃子数量不足以摘&#xff08;必须满足摘桃个数&#xff09;&#xff1b; 【1】 使用AtomicInteger&#xff08;推荐&#xff09; 1&#xff09;利…

iOS--block再学习

block再学习 什么是blockblock是带有自动变量的匿名函数block语法 block的实现block的实质截获自动变量__blcok说明符Block存储域__block变量存储域使用__block变量用结构体成员变量__forwarding的原因 截获对象 什么是block Block时c语言的扩充功能&#xff0c;它允许开发者定…

pytorch之猫狗识别项目

1. 导入资源包 资源包&#xff1a; import torchvision&#xff1a;PyTorch 提供的视觉库&#xff0c;包含了常用的计算机视觉模型架构、数据集以及图像转换工具。 from torchvision import datasets, models&#xff1a;导入 torchvision 中的 datasets 和 models 模块&#…

spring boot +Scheduled 动态定时任务配置

通常情况下我们设定的定时任务都是固定的,有时候需要我们动态的配置定时任务,下面看代码 import com.mybatisflex.core.query.QueryWrapper; import com.yzsec.dsg.web.modules.exportpwd.entity.ExportPwd; import com.yzsec.dsg.web.modules.exportpwd.entity.table.Export…

如何使用GPT-4o函数调用构建一个实时应用程序?

本教程介绍了如何使用OpenAI最新的LLM GPT-4o通过函数调用将实时数据引入LLM。 我们在LLM函数调用指南(详见https://thenewstack.io/a-comprehensive-guide-to-function-calling-in-llms/)中讨论了如何将实时数据引入聊天机器人和代理。现在&#xff0c;我们将通过将来自Fligh…

bat脚本简介

一、bat脚本 概念定义 BAT 批处理是一种在 Windows 系统中用于将一系列命令组合成一个可执行文件&#xff08;.bat 文件&#xff09;的脚本技术。 允许用户将多个操作命令按顺序编写在一起。形成一个自动化执行的流程。批处理文件可以包含各种系统命令和程序调用。 如文件操作…

中国现在最厉害的书法家颜廷利:东方伟大思想家哲学家教育家

中国书法界名人颜廷利教授&#xff0c;一位在21世纪东方哲学、科学界及当代中国教育领域内具有深远影响力的泰斗级人物&#xff0c;不仅以其深厚的国学修为和对易经姓名学的独到见解著称&#xff0c;还因其选择在济南市历城区的龙泉大街以及天桥区的凤凰山庄与泉星小区等地设立…

如何在隔离环境中设置 LocalAI 以实现 GPU 驱动的文本嵌入

作者&#xff1a;来自 Elastic Valeriy Khakhutskyy 你是否想在 Elasticsearch 向量数据库之上构建 RAG 应用程序&#xff1f;你是否需要对大量数据使用语义搜索&#xff1f;你是否需要在隔离环境中本地运行&#xff1f;本文将向你展示如何操作。 Elasticsearch 提供了多种方法…

多曝光融合算法(三)cv2.createAlignMTB()多曝光图像融合的像素匹配问题

文章目录 1.cv2.createAlignMTB() 主要是计算2张图像的位移&#xff0c;假设位移移动不大2.多曝光图像的aline算法&#xff1a;median thresold bitmap原理讲解3.图像拼接算法stitch4.多曝光融合工具箱 1.cv2.createAlignMTB() 主要是计算2张图像的位移&#xff0c;假设位移移动…

Python中猴子补丁是什么,如何使用

1、猴子补丁奇遇记 &#x1f412; 在Python的世界深处&#xff0c;隐藏着一种神秘而又强大的技巧——猴子补丁&#xff08;Monkey Patching&#xff09;。这是一项允许你在程序运行时动态修改对象&#xff08;如模块、类或函数&#xff09;的行为的技术。它得名于其“快速修补…

问题排查: Goalng Defer 带来的性能损耗

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 本作品 (李兆龙 博文, 由 李兆龙 创作)&#xff0c;由 李兆龙 确认&#xff0c;转载请注明版权。 文章目录 引言问题背景结论 引言 性能优化之路道阻且长&#xff0c;因为脱敏规定&#xff0c;…

vite常识性报错解决方案

1.导入路径不能以“.ts”扩展名结束。考虑改为导入“xxx.js” 原因&#xff1a;当你尝试从一个以 .ts 结尾的路径导入文件时&#xff0c;ESLint 可能会报告这个错误&#xff0c;因为它期望导入的是 JavaScript 文件&#xff08;.js 或 .jsx&#xff09;而不是 TypeScript 文件&…

coap-emqx:使用libcoap与emqx通信

# emqx开启CoAP网关 请参考【https://blog.csdn.net/chenhz2284/article/details/139562749?spm1001.2014.3001.5502】 # 写一个emqx的客户端程序&#xff0c;不断地往topic【server/1】发消息 【pom.xml】 <dependency><groupId>org.springframework.boot<…

开源与新质生产力

在这个信息技术迅猛发展的时代&#xff0c;全球范围内的产业都在经历着深刻的变革。在这样的背景下&#xff0c;“新质生产力”的概念引起了广泛的讨论。无论是已经成为或正努力转型成为新质生产力的企业&#xff0c;都在寻求新的增长动力和竞争优势。作为一名长期从事开源领域…

Linux用户和用户组的管理

目录 前言一、系统环境二、Linux用户组的管理2.1 新增用户组2.2 删除用户组2.3 修改用户组2.4 查看用户组 三、Linux用户的管理3.1 新增用户3.2 删除用户3.3 修改用户3.4 查看用户3.5 用户口令&#xff08;密码&#xff09;的管理 总结 前言 本篇文章介绍如何在Linux系统上实现…