【C++和数据结构】模拟实现哈希表和unordered_set与unordered_map
目录
一、哈希的概念与方法
1、哈希概念
2、常用的两个哈希函数
二、闭散列的实现
1、基本结构:
2、两种增容思路 和 插入
闭散列的增容:
哈希表的插入:
3、查找
4、删除
三、开散列的实现
1、基本结构
2、仿函数Hash
3、迭代器实现
4、增容和插入
5、查找
6、删除
7、Clear和析构函数
四、哈希表模拟实现unordered_set和unordered_map
看这篇文章之前你需要对哈希表有一定了解,本文主讲代码实现
一、哈希的概念与方法
1、哈希概念
哈希表和数组区别:哈希表是在数组的基础上按 映射关系放的比如我们身份证号就是个映射关系,比如看身份证号的前几位,就能知道你是哪个省的,后几位就能看出你的生日
注:
哈希:映射方式的算法
哈希表:使用哈希建立的数据结构
2、常用的两个哈希函数
注:由于除留余数法是目前很好的哈希函数,本文讲解用的全是除留余数法,故不用直接定址法
取关键字的某个线性函数为散列地址: Hash ( Key ) = A*Key + B优点:简单、均匀缺点:需要事先知道关键字的分布情况使用场景:适合查找比较小且连续的情况面试题: 字符串中第一个只出现一次字符![]()
设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数: Hash(key) = key% p(p<=m), 将关键码转换成哈希地址直接定址法是是 没有哈希冲突的,每个值都映射了一个唯一位置,但为什么要引入除留余数法,因为若数据分布不均匀,那么我们给每个值映射一个位置,可能会造成巨大的空间浪费。而除留余数法,不再是给每个值映射一个位置,是在限定大小的空间中将我们的值映射进去。 index = key % 空间大小。![]()
除留余数法带来的问题:不同的值可能会映射到相同位置上去,导致哈希冲突。哈希冲突越多整体而言效率越低。
除留余数法的使用:
如何解决哈希冲突呢?
解决哈希冲突两种常见的方法是:闭散列和开散列
1、闭散列——开放定址法(当前位置冲突了,则按规则再给你找个位置)
a、线性探测 b、二次探测
2、开散列——拉链法(哈希桶)(重点)
二、闭散列的实现
先用哈希表的查找来引出基本结构的设计
可见,删除会影响查找,难道不像上面那样考虑,直接整个表查找一下有没有21可行吗?不行,哈希表就是为了效率而生的,所以必须按照上面那个规则来走。
注:哈希表的代码在HashTable.h中实现
1、基本结构:
这里的哈希表实现完全可以写为KV模型的,但是我们这里是为了模拟实现unordered_set与unordered_map,故我们用K,T
K就是按照关键字K来查找等,unordered_set对于T,会传入K
而unordered_map对于T,会传入pair<K,V>,故用T来表示对两种结构的通用,其次KeyOfT表示的是K类型的,因为我们后续的比较都是用K类型的来直接比较,所以我们要取出T中的K,这一点利用仿函数来实现,(因为两种结构所传入的T不同)
哈希表中存放两个变量,为什么要用vector?首要原因是哈希表的本质就是数组,而vector符合是动态数组这一点,其次是方便,vector是自定义类型,它支持的resize等操作在我们实现哈希表过程中非常好用,并且它的析构也不用我们管
enum State
{EMPTY, //空EXIST, //存在DELETE //删除
};template<class T>
struct HashData
{T _data;State _state; //代表数据状态HashData():_state(EMPTY),_data(0){}
};template<class K, class T, class KeyOfT>
class HashTable
{typedef HashData<T> HashData;
private:vector<HashData> _tables; //哈希数组size_t _num = 0; //表中存了多少个有效个数,不等于容量
};
代码中HashTable不写构造函数是因为_num初始给0了,而vector是自定义类型,它本身就有构造函数,所以没必要写构造函数
2、两种增容思路 和 插入
闭散列的增容:
负载因子衡量哈希表满的程度,哈希表不敢让表太满,表一满,那对于哈希表插入等操作的效率是非常低的,所以引入负载因子
增容分两种思路:
1、传统思路
- a、开2倍大小的新表
- b、遍历旧表的数据,重新计算在新表中位置
- c、释放旧表
增容不能直接将旧表中的数据拷贝到新表中就完事了,而应该重新映射,若不重新映射,由于表的大小会变,旧表中的数据到新表中的数据会改变,故要重新映射
2、简便思路
创建一个新的哈希表,利用哈希表已实现的Insert操作直接把原哈希表中每个数据插入到新哈希表中,注意临界条件,如果是初次增容只会resize开辟空间,详细操作见代码
哈希表的插入:
规则:a、线性探测(挨着往后找,直到找到空位置)
b、二次探测(按 i^2,跳跃着往后找,直到找到空位置)
线性探测:
我们先计算出该数据映射到的位置,如果映射的位置已有数据,则继续挨着往后找,直到找到一个空位置(EMPTY)或一个被删除过的位置(DELETE),我们一定能找到一个位置的,因为闭散列的数组我们引入了负载因子,我们不可能让数组满了的。
其次要注意闭散列是一个数组结构,走到尾部还没找到一个位置就要回数组头部去找,针对此操作代码可以用以下两种写法(index代表位置):
//法一、 if (index == _tables.capacity()) {index = 0; } //法二、 index %= _tables.capacity();
二次探测:
线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,会导致一片一片的冲突,因此二次探测为了避免该问题,其让冲突的数据相对分散了,不会导致连片冲突,效率更高
代码如下:
bool Insert(const T& data)
{KeyOfT koft;//1、增容:第一次插入或者负载因子>=0.7就要增容if (_tables.capacity() == 0 || _num * 10 / _tables.capacity() == 7){//A、增容——传统思路//vector<HashData> newtables;//size_t newcapacity = _tables.capacity() == 0 ? 10 : _tables.capacity() * 2;//newtables.resize(newcapacity);//开空间+自动初始化为0把旧空间数据拷贝到新空间中//for (size_t i = 0; i < _tables.capacity(); ++i)//{// if (_tables[i]._state == EXIST)// {// size_t index = koft(_tables[i]._data) % newtables.capacity();// while (newtables[index]._state == EXIST)// {// index++;// if (index == newtables.capacity())// {// index = 0;//走到尾了就要返回头找位置// }// }// newtables[index] = _tables[i];// }//}//_tables.swap(newtables);//B、增容——简便思路HashTable<K, T, KeyOfT> newht;size_t newcapacity = _tables.capacity() == 0 ? 10 : _tables.capacity() * 2;newht._tables.resize(newcapacity);for (size_t i = 0; i < _tables.capacity(); ++i){if (_tables[i]._state == EXIST){newht.Insert(_tables[i]._data);//把原哈希表中每个数据利用Insert都插入到新哈希表中}}_tables.swap(newht._tables);//交换两者的vector}//1、线性探测//size_t index = koft(data) % _tables.capacity();//计算出要映射的位置//while (_tables[index]._state == EXIST)//{// if (koft(_tables[index]._data) == koft(data))// {// return false;//如果存在相同的数据// }// ++index;// if (index == _tables.capacity())// {// index = 0;// }//}//2、二次探测size_t start = koft(data) % _tables.capacity();size_t index = start;int i = 1;while (_tables[index]._state == EXIST){if (koft(_tables[index]._data) == koft(data)){return false;}index = start + i * i;++i;index %= _tables.capacity();}//插入数据_tables[index]._data = data;_tables[index]._state = EXIST;//用状态表示该位置已有数据++_num; //有效数据个数++return true;
}
问题解释和知识回顾(理解代码):
1、vector的resize
resize会开空间+初始化,而初始化什么值,你不传,他就会默认用这个类型的默认值,比如你vector中存的是int,那就默认初始化为0,你vector中存的是指针,默认初始化为nullptr,你vector中存的是string,默认初始化为空串
2、
增容完这个操作什么意思?符合现代写法
1、交换的是vector,那_tables不就是增容完的vector吗,那我后续操作还可以用_tables
2、newht的_tables就被换得了原vector的空间,而newht是个局部变量,出作用域就销毁,正好把我原vector的空间释放了
3、
KeyOfT koft;
这是什么?KeyOfT是模板参数,而它是为了取unordered_set和unordered_map中的key值的,本质上是利用仿函数实现了这一点,故你定义一个仿函数对象koft,利用koft调用仿函数operator()即可取出T类型数据中K类型的key值
4、
_num * 10 / _tables.capacity() == 7
判断负载因子为什么要这么写,因为整数相除不能得到0.7,其实转为double也是个方法,但是不推荐,直接*10不就行了嘛
3、查找
查找操作开头就已解释过
HashData* Find(const K& key)
{KeyOfT koft;size_t index = key % _tables.capacity();while(_tables[index]._state != EMPTY){//只要是存在和删除状态就要持续往下找if (koft(_tables[index]._data) == key){if (_tables[index]._state == EXIST)return &_tables[index];//值相等且为存在状态elsereturn nullptr;//值相等但为删除状态,说明被删除了}++index;//没找到继续往后找index %= _tables.capacity();}return nullptr;
}
4、删除
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索 。比如删除元素 4 ,如果直接删除掉, 44 查找起来可能会受影响。因此 线性探测采用标记的伪删除法来删除一个元素 。
bool Erase(const K& key){HashData* ret = Find(key);if (ret){ret->_state = DELETE;//用状态代表删除状态--_num; //--有效元素个数return true;}else{return false;}}
测试开散列代码:
template<class K>
struct SetKeyOfT
{const K& operator()(const K& key){return key;}
};void TestCloseHash()
{CLOSEHASH::HashTable<int, int, SetKeyOfT<int>> ht;ht.Insert(2);ht.Insert(4);ht.Insert(14);ht.Insert(24);ht.Insert(26);ht.Insert(16);ht.Erase(14);ht.Erase(2);CLOSEHASH::HashData<int>* data = ht.Find(4);
}
用线性探测测试代码:
用二次探测测试代码:
因为闭散列没有开散列好,所以这里闭散列简单实现下即可,对于更进一步的迭代器、和实现unordered_set和unordered_map等等操作我们都是用开散列实现,开散列才是重中之重
三、开散列的实现
开散列法又叫链地址法 ( 开链法 ) ,首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中 。这里的开散列就像个数组和链表的融合体,但开散列本质还是个数组
1、基本结构
template<class T> struct HashNode {T _data;HashNode<T>* _next;HashNode(const T& data):_data(data), _next(nullptr){} }; template<class K, class T, class KeyOfT, class Hash> class HashTable {typedef HashNode<T> Node;private:vector<Node*> _tables; //使用vector一定要包含std库,不然使用不了size_t _num = 0; //有效元素的个数,不是容量 };
细品:(_tables)表是一个指针数组,是这个数组中存了第一个节点的地址
2、仿函数Hash
为什么模板参数多了个Hash?
它是个仿函数,他可以针对string、自定义类型等不能取模的类型,让他们能支持取模运算,因为对于开散列也涉及取模运算,它要求数据的映射位置
若用string类型就无法取模,如果key的类型是一个结构体呢,是不是也不能取模,也就是进来的key不一定能取模
因为string不支持直接取模,故再写一个仿函数_Hash,然后内部使用_Hash即可,用仿函数来支持取模运算,默认情况下使用_Hash<K>,但是遇到string我们需要显式地传_HashString
那么_HashString的思路是什么?拿字符串的第一个字母的ASCII码取模可以吗?不好,因为若这些字符串的第一个字母相同,就容易产生冲突,故我们把所有字母ASCII码值加起来(因为哈希表是取模后映射位置,都用ASCII码来计算,就会有对应的映射位置)
那怎么使用这个仿函数呢?
在HashTable中写个HashFunc直接把数据转换成能取模的,因为这个操作需要频繁使用
size_t HashFunc(const K& key) {Hash hash;return hash(key);//利用仿函数把key值转为整形 }
存在的问题:
因为不同的字符串,相加后可能是相同的,那放在同一位置会引起哈希冲突,故要把字符串转成整形等,用字符串哈希算法来算,比如出色的BKDR Hash算法,字符串每次乘以131即可,则字符串算出来的值就不那么容易冲突了(也可以乘以31,1313,13131,131313..),但是无论如何一定会有可能发生冲突,因为字符串在不限定长度的情况下是无限长的,我们要做的就是降低冲突概率
应用BKDR后,就会发现不冲突了,它大大的降低冲突性
那对于结构体想取模怎么办?比如一个人的信息是一个结构体,那我们就可以找人的信息中有代表项的东西,比如人的电话号码,是个字符串,就能唯一的代表人。用身份证也行。那如果这个人不能用电话号码和身份证等,我们用名字作为代表项码?不好,名字相同的人很多,很容易产生冲突,故我们可以用名字+另一个代表项,比如名字+家乡地址作为代表项,算出映射位置
注:
因为string类型会经常去做key,我们干脆让string作为默认的仿函数,一个模板类型要对一个参数作特殊处理就要用到模板特化,这下即使不传参数,也能处理好string类型的哈希表使用,就是在于它变成默认支持的了
template<class K> struct _Hash {const K& operator()(const K& key){return key;//可以取模的直接返回即可} };//特化 template<> struct _Hash<string> {size_t operator()(const string& key){//运用BKDR Hash算法size_t hash = 0;for (size_t i = 0; i < key.size(); ++i){hash *= 131; //BKDRhash += key[i];//字符串中每个字母ASCII码值相加}return hash;} };template<class K, class T, class KeyOfT, class Hash = _Hash<K>> class HashTable
注意:上面模板参数class Hash = _Hash<K>表示默认会调用_Hash<K>,那这个默认一个是string可以默认调用,另一个就是能正常取模的类型可以正常调用,也就是你调用HashTable时不传这个Hash参数就可以帮你自动调用这个默认的,你若不用模板特化的话string类型的是不会帮你默认调用的(但我们说了我们的哈希表是为了给unordered_set和unordered_map使用的,也就是上层使用的,你下层哈希表的实现就不用写为class Hash = _Hash<K>了,这一点在模拟实现unordered_set和unordered_map时,它们俩会写的,也就是模拟实现时这里HashTable的参数只会写为class Hash即可)
如果我不想在创建对象时还要传仿函数(说的是取模的仿函数),但是对于string还能正常调用到它的仿函数,那就可以用模板特化,一个模板类型要对一个参数作特殊处理就要用到模板特化,当K是string类型的时候,就会调用_Hash的模板特化,正常处理string取模问题
但是再来一个类型你也可以写成模板特化(但它变成默认的模板参数前提应该是这个类型在某个场景下会被频繁使用),但如果不是频繁使用的,就没必要整成默认的,直接单独写一个仿函数,显示传参数即可
3、迭代器实现
关于闭散列的哈希表的迭代器,就和list迭代器的思路相仿(建议先把list的迭代器的模拟实现看明白),不过在此基础上还要额外考虑
这里最需要说明的是operator++,其他的操作容易理解
哈希桶的迭代器operator++思路:
迭代器不需要按照插入顺序迭代,底层实现也没有考虑,但是我们可以思考一下如果按插入的顺序迭代遍历要如何实现 :
为了我们能找到下一个桶,若我们使用vector,但我就一个迭代器,怎么用vector呢
我怎么知道我在哪个桶呢?可以再计算下此时的映射位置,即可以通过桶中的值,计算我是哪个桶。
如果这里直接传vector,那HashFunc就调用不到了,所以干脆给个哈希表,并且是哈希表指针,就能用HashFunc
总结:
利用哈希表,也就是在迭代器中再创建个哈希表对象的指针,利用哈希表找下一个桶,因为我哈希表里面有vector啊,vector里面存的是头指针,利用这个头指针不就能找到下一个桶了
注:哈希表迭代器不支持--操作,因为它是单向迭代器,也就是没有rbegin和rend等等
//前置声明:为了让哈希表的迭代器能用哈希表
template<class K, class T, class KeyOfT, class Hash>
class HashTable; template<class K, class T, class KeyOfT, class Hash>
struct __HashTableIterator
{typedef __HashTableIterator<K, T, KeyOfT, Hash> Self;typedef HashTable<K, T, KeyOfT, Hash> HT;typedef HashNode<T> Node;Node* _node;//迭代器中存的是节点指针HT* _pht;//对象的指针__HashTableIterator(Node* node, HT* pht):_node(node),_pht(pht){}T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}Self operator++(){if (_node->_next){//如果还能在一个桶中,就直接在一个桶中往后走(单链表)_node = _node->_next;}else{// 如果一个桶走完了,要往下找到下一个桶继续遍历KeyOfT koft;//先计算我当前是在哪个桶size_t i = _pht->HashFunc(koft(_node->_data)) % _pht->_tables.capacity();++i;//下一个桶for (; i < _pht->_tables.capacity(); ++i){ //找不为空的桶Node* cur = _pht->_tables[i];if (cur){ //如果这个桶不为空_node = cur;return *this;//迭代器++返回的是迭代器本身}}_node = nullptr;//如果没有找到有数据的桶,则指针置为空,与end()相符return *this;}}bool operator!=(const Self& s){return _node != s._node;}
};
哈希表中的begin()和end()实现:
typedef __HashTableIterator<K, T, KeyOfT, Hash> iterator;//begin()返回第一个不为空的桶的第一个节点iterator begin(){for (size_t i = 0; i < _tables.capacity(); ++i){if (_tables[i]){return iterator(_tables[i], this);//找到了则构造匿名对象返回}}return end();//每个桶中都没找到则返回end()}iterator end(){return iterator(nullptr, this);}
4、增容和插入
增容就涉及重新映射元素位置的问题,那我们就取出原哈希表中每个桶中的元素,然后重新计算它在新表中的映射位置,那放到新表中有冲突的元素怎么办?头插即可,和插入的思路一样,或者说增容的思路就包含了插入的思路。我们会一个一个桶往后走,那每一个桶都是一个单链表,遍历这个单链表用while,不断计算桶中每个元素在新表中的映射位置,遍历桶我们相当于遍历_tables
插入的关键是这个冲突的值是尾插还是头插到对应位置呢?按理说,冲突的数据的顺序是无所谓的,故头插尾插都可以,也就是达到的效果一样,但是尾插还要先找到尾再插入,故这里用头插实现
假设表容量capacity为10
pair<iterator, bool> Insert(const T& data)
{KeyOfT koft;//1、判断是否增容if (_tables.capacity() == _num){ //开散列的实现平衡因子为1就增容且第一次插入也会增容size_t newcapacity = _tables.capacity() == 0 ? 10 : _tables.capacity() * 2;vector<Node*> newtables;newtables.resize(newcapacity, nullptr);//给新的vector开新空间+初始化//重新计算旧表的数据在新表中的映射位置for (size_t i = 0; i < _tables.capacity(); ++i){ //如果是第一次的增容不会进for循环的,故不用担忧表的初始数据是否为nullptr//哈希表中每一个桶都是一个单链表,故考察单链表的头插Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t index = HashFunc(koft(cur->_data)) % newtables.capacity();//重新计算映射位置//头插cur->_next = newtables[index];newtables[index] = cur;cur = next;}_tables[i] = nullptr;//映射到新表后置为空}_tables.swap(newtables);//新旧表的vector交换}size_t index = HashFunc(koft(data)) % _tables.capacity();//计算新的映射位置//1、先查找这个元素是否在哈希表中Node* cur = _tables[index];//知道映射位置就能确定是哪个桶while (cur){if (koft(cur->_data) == koft(data))return make_pair(iterator(cur, this), false);//找到相同数据则插入失败elsecur = cur->_next;}//2、头插到这个桶中Node* newnode = new Node(data);//开辟新节点//头插newnode->_next = _tables[index];_tables[index] = newnode;++_num;//哈希表中有效元素个数++return make_pair(iterator(newnode, this), false);
}
1、Insert的返回值底层实现就是pair<iterator, bool>
插入数据:
若要插入的数据已存在于哈希表中,则返回已存在的数据的位置,并返回false,即代码中的make_pair(iterator(newnode, this), false),这里用的是iterator的构造函数,因为构造函数初始化列表有一个哈希表指针,而this就是我本身的哈希表指针( HashTable<K, T, KeyOfT, Hash>*类型的)
若要插入的数据不存在于哈希表中,则返回这个要插入数据的位置,并返回true,即代码中的make_pair(iterator(cur, this), true);
5、查找
Node* Find(const K& key)
{KeyOfT koft;size_t index = HashFunc(key) % _tables.capacity();//先计算映射位置Node* cur = _tables[index];while (cur){if (koft(cur->_data) == key)return cur;elsecur = cur->_next;}return nullptr;//走遍桶都没找到则返回空
}
6、删除
开散列的删除,就相当于单链表的删除,故要找前一个节点,而删除的前一步是找到你要删除的数据在不在
bool Erase(const K& key)
{assert(_tables.capacity() > 0);//有空间才能删KeyOfT koft;size_t index = HashFunc(key) % _tables.capacity();Node* cur = _tables[index];Node* prev = nullptr;//记录cur的前一位置while (cur){if (koft(cur->_data) == key){if (prev == nullptr){ //如果是头删_tables[index] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--_num;return true;}else{prev = cur;cur = cur->_next;}}return false;//要删除数据根本不存在
}
7、Clear和析构函数
虽然哈希表不用我们写构造函数,但是因为哈希表中每个节点都是动态开辟的,故我们要释放下每个节点,遍历每个桶,每个桶都是单链表,即考察单链表的节点释放
~HashTable(){Clear();//vector不用我们释放,因为它是自定义类型,哈希表要清理时,vector也会自动清理}void Clear(){for (size_t i = 0; i < _tables.capacity(); ++i){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;//清空完数据后置为nullptr}}
遗留的问题:
当大量的数据冲突,这些哈希冲突的数据就会挂在同一个链式桶中 ,查找时效率就会降低,所以开散列-哈希桶也是要控制哈希冲突的。如何控制呢?通过控制负载因子,不过这里就把空间利用率提高一些,负载因子也可以高一些,一般开散列把负载因子控制到1,会比较好一些
四、哈希表模拟实现unordered_set和unordered_map
对于unordered_set和unordered_map第二个模板参数是个通过哈希来比较的(我们实现的是把这个参数放在第四个模板参数),万一我的unordered_set的模板参数K是个结构体类型,我不能直接取模就直接完蛋了,所以要传一个仿函数给哈希表
总结:这个默认的模板参数是利用上层模拟实现用的,你哈希表的底层实现就写为class Hash即可,我上层使用的时候会直接传给你
MyUnordered_set.h中实现unordered_set的:
#pragma once
#include"HashTable.h"
using namespace OPENHASH;namespace mz
{using OPENHASH::_Hash;template<class K, class Hash = _Hash<K>>class unordered_set{struct SetKOfT{const K& operator()(const K& k){return k;}};public://加typename是告诉编译器这是个类型,你先让我过,等实例化了再去找//因为模板没实例化它是不接受你用模板里面的一个类型,故要用typename typedef typename HashTable<K, K, SetKOfT, Hash>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}pair<iterator, bool> insert(const K& k){return _ht.Insert(k);}private:HashTable<K, K, SetKOfT, Hash> _ht;};void test_unordered_set(){unordered_set<int> s;s.insert(1);s.insert(5);s.insert(4);s.insert(2);unordered_set<int>::iterator it = s.begin();while (it != s.end()){cout << *it << " ";++it;}cout << endl;}
}
MyUnordered_map.h中实现unordered_map的:
#pragma once
#include"HashTable.h"
using namespace OPENHASH;namespace mz
{using OPENHASH::_Hash;template<class K, class V, class Hash = _Hash<K>>//一般模板参数都是由上一层来控制的class unordered_map{struct MapKOfT {const K& operator()(const pair<K, V>& kv){return kv.first;}};public:typedef typename HashTable<K, pair<K,V>, MapKOfT, 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){//unordered_map的operator[]是给key值返回v的引用//底层实现用的是哈希表的Insert来实现,在介绍使用时讲过这点pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));return ret.first->second;}private:HashTable<K, pair<K, V>, MapKOfT, Hash> _ht;//底层是个哈希表};void test_unordered_map(){unordered_map<string, string> dict;dict.insert(make_pair("factual", "真实的"));dict.insert(make_pair("fringe", "侵犯"));dict.insert(make_pair("intermittent", "间歇的"));dict["prerequisite"] = "先决条件";dict["reduce to"] = "处于";//unordered_map<string, string>::iterator it = dict.begin();auto it = dict.begin();while (it != dict.end()){cout << it->first << ":" << it->second << endl;++it;}cout << endl;}
}
遗留的问题:
很明显我们实现的unordered_map和unordered_set排出来的数据并不是有序的,但std库里面会自动排序的
思路:再建立一个链表存储数据,尾插,迭代器遍历时,遍历链表,就可以保持遍历有序,那么主要问题是 遍历时保持插入的顺序,这样结构会复杂,但迭代器会变简单,定义两个指针,一个next指针用来挂桶,另一个prev指针用来遍历
这个知识了解思路即可
进一步改进:
如果表的大小是一个素数,冲突率会有一定的降低,素数是只能被1和本身所整除的,那怎么保证是个素数呢?我们可以提供一个素数表,数字后面加ul,表示无符号的长整形,即unsigned long,这个素数表从头到尾基本上每个数据都是前一个的2倍,而最大的数接近整数最大值,【注:这个表是STL中的,其经过研究具有一定的优越性】
怎么用这个表?
在开辟表大小时调用这个素数表,从头遍历这个素数表,找到比我当前要开辟的大小大的即可
完整源码:
http://t.csdnimg.cn/XTY7Y
相关文章:

【C++和数据结构】模拟实现哈希表和unordered_set与unordered_map
目录 一、哈希的概念与方法 1、哈希概念 2、常用的两个哈希函数 二、闭散列的实现 1、基本结构: 2、两种增容思路 和 插入 闭散列的增容: 哈希表的插入: 3、查找 4、删除 三、开散列的实现 1、基本结构 2、仿函数Hash 3、迭代器…...
十四天学会C++之第五天:类的详细讨论
1. 友元函数和友元类 什么是友元函数和友元类,它们的作用。如何声明和使用友元函数和友元类,访问类的私有成员。 友元函数(Friend Functions) 友元函数是一种特殊的函数,它被允许访问类的私有成员。这意味着即使成员…...

字典树学习笔记
trie 树,即字典树,是一种可以实现 O ( S ) O(S) O(S) 的预处理( S S S 为所有字符串的长度和), O ( N ) O(N) O(N)( N N N 为查询的字符串的长度)的查询的数据结构。 举个栗子,对于…...

web各个指标理解
QPS : 单位时间得请求次数 TPS :单位时间得事务数 并发 : QPS *单位响应时间 pv :进入一个网站,又单击打开该网站的其他页面,每打开一个页面就 增加一个PV,甚至在同一页面每刷新一次也多一个PV 二八定律:百…...
Java后端开发(七)-- 在gitee上部署远程仓库,通过idea上传本地代码(用idea2022版本开发)
目录 1. 在Gitee上创建gitee远程仓库 2.在打开idea,再打开您要上传的idea代码,先创建 本地git仓库...

Go语言入门心法(十二): GORM映射框架
Go语言入门心法(一): 基础语法 Go语言入门心法(二): 结构体 Go语言入门心法(三): 接口 Go语言入门心法(四): 异常体系 Go语言入门心法(五): 函数 Go语言入门心法(六): HTTP面向客户端|服务端编程 Go语言入门心法(七): 并发与通道 Go语言入门心法(八): mysql驱动安装报错o…...

Ubuntu更新镜像源切换
概述 用ubuntu用apt命令,自动安装或更新包的时候,默认的镜像源服务器非常卡,很不方便。切换到国内的镜像源,下载更新非常快。为防止以后忘记,本文以国内服务器阿里巴巴的为例简单描述。 版本 Ubuntu23.10 找到更新…...

“一键合并剪辑,轻松添加片头——全新的视频编辑工具让你成为视频制作达人“
在日常生活中,我们时常会遇到需要制作视频的情况。但面对繁琐的视频剪辑和合并,你是否感到无从下手?今天,我们为你带来一款全新的视频编辑工具,让你轻松成为视频制作达人! 首先我们要进入好简单批量智剪主页…...

1.3 矩阵
一、向量与矩阵 下面是三个向量 u \boldsymbol u u、 v \boldsymbol v v、 w \boldsymbol w w: u [ 1 − 1 0 ] v [ 0 1 − 1 ] w [ 0 0 1 ] \boldsymbol u\begin{bmatrix}\,\,\,\,1\\-1\\\,\,\,\,0\end{bmatrix}\kern 10pt\boldsymbol v\begin{bmatrix}\,\,\,…...
阿里云-AnalyticDB【分析型数据库】总结介绍
一、背景 随着企业IT和互联网系统的发展,产生了越来越多的数据。数据量的积累带来了质的飞跃,使得数据应用从业务系统的一部分演变得愈发独立。物流、交通、新零售等越来越多的行业需要通过OLAP做到精细化运营,从而调控生产规则、运营效率、企…...

数二思维导图
高数上 第一章:函数、极限、连续 函数 函数的单调性、周期性、奇偶性复合函数 极限 求直接代入型的极限求∞∞型的极限用等价无穷小代换求00型的极限用洛必达法则求00型或∞∞型的极限求∞•0型的极限求幂指函数的极限函数的左右极限及需要求左右极限的情形极限的…...

ESXI6.5安装教程
设置从IPMI Virtual Disk 3000启动,出现如下界面: 默认选择第一项,回车安装 安装程序正在检测服务器硬件信息,如果不满足系统安装条件会跳出错误提示。 检测完成之后会出现下面界面 回车 按F11 这里列出了服务器硬盘信息&#…...
2023-9-25 美团售后服务系统后端一面【2024秋招】
1 实习 1.1 讲讲你做的一个需求,为什么这么做之类的 答: 1.2 什么是接线 1.3 什么的初始接线,和权威接线 答:初始接线是现状,权威是规划中的 1.4 为什么要做比较呢? 答:运维人员需要查看…...

YOLOv5改进实战 | GSConv + SlimNeck双剑合璧,进一步提升YOLO!
前言 轻量化网络设计是一种针对移动设备等资源受限环境的深度学习模型设计方法。下面是一些常见的轻量化网络设计方法: 网络剪枝:移除神经网络中冗余的连接和参数,以达到模型压缩和加速的目的。分组卷积:将卷积操作分解为若干个较小的卷积操作,并将它们分别作用于输入的不…...
Redis之zset在异步队列上的应用
当遇到并发的客户端请求时,为了缓解服务端的处理压力,当请求对响应的处理的实时性要求不高时,可以实现一个异步的请求消息队列。 一种实现策略是使用redis的zset,将消息的到期处理时间作为score,然后用多个线程去轮训…...
day4:Node.js 核心库
day4:Node.js 核心库 文章目录 day4:Node.js 核心库常用工具模块util 模块Moment 模块Lodash 模块web模块文件模块path 模块常用工具模块 Node.js有许多常用的工具,以下是一些常见的: util: 是一个Node.js 核心模块,提供常用函数的集合,用于弥补核心 JavaScript 的功能…...
PHP非对称与对称双向加密解密的方式
目录 RSA非对称加密解密: 什么是RSA非对称加密解密解析: 解析: 为什么使用: 有什么优点: DEMO: AES、DES、3DES等对称加密解密: 解析: 为什么使用: 有什么优点: DEMO: RSA非对称加密解密: 什么是RSA非对称加密解密解析: 解析: RSA非对称加密…...

C++之struct匿名结构体实例(二百四十四)
简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 人生格言: 人生…...

npm publish发布到在线仓库时,提示:Scope not found
当npm publish发布时,控制台提示:Scope not found,具体错误信息如下: npm notice npm ERR! code E404 npm ERR! 404 Not Found - PUT https://registry.npmjs.org/xxx%2fxxx - Scope not found npm ERR! 404 npm ERR! 404 xxx/xx…...

AWS Lambda 操作 RDS 示例
实现目标 创建一个 Lambda 接收调用时传入的数据, 写入 RDS 数据库 Post 表存储文章信息. 表结构如下: idtitlecontentcreate_date1我是标题我是正文内容2023-10-21 15:20:00 AWS 资源准备 RDS 控制台创建 MySQL 实例, 不允许 Public access (后面 Lambda 需要通过 VPC 访问…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能
下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能,包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...

(二)TensorRT-LLM | 模型导出(v0.20.0rc3)
0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述,后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作,其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...
TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案
一、TRS收益互换的本质与业务逻辑 (一)概念解析 TRS(Total Return Swap)收益互换是一种金融衍生工具,指交易双方约定在未来一定期限内,基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...

tauri项目,如何在rust端读取电脑环境变量
如果想在前端通过调用来获取环境变量的值,可以通过标准的依赖: std::env::var(name).ok() 想在前端通过调用来获取,可以写一个command函数: #[tauri::command] pub fn get_env_var(name: String) -> Result<String, Stri…...
Python竞赛环境搭建全攻略
Python环境搭建竞赛技术文章大纲 竞赛背景与意义 竞赛的目的与价值Python在竞赛中的应用场景环境搭建对竞赛效率的影响 竞赛环境需求分析 常见竞赛类型(算法、数据分析、机器学习等)不同竞赛对Python版本及库的要求硬件与操作系统的兼容性问题 Pyth…...

Qt的学习(一)
1.什么是Qt Qt特指用来进行桌面应用开发(电脑上写的程序)涉及到的一套技术Qt无法开发网页前端,也不能开发移动应用。 客户端开发的重要任务:编写和用户交互的界面。一般来说和用户交互的界面,有两种典型风格&…...

密码学基础——SM4算法
博客主页:christine-rr-CSDN博客 专栏主页:密码学 📌 【今日更新】📌 对称密码算法——SM4 目录 一、国密SM系列算法概述 二、SM4算法 2.1算法背景 2.2算法特点 2.3 基本部件 2.3.1 S盒 2.3.2 非线性变换 编辑…...

【大模型】RankRAG:基于大模型的上下文排序与检索增强生成的统一框架
文章目录 A 论文出处B 背景B.1 背景介绍B.2 问题提出B.3 创新点 C 模型结构C.1 指令微调阶段C.2 排名与生成的总和指令微调阶段C.3 RankRAG推理:检索-重排-生成 D 实验设计E 个人总结 A 论文出处 论文题目:RankRAG:Unifying Context Ranking…...