哈希表
- [1. 关联式容器的对比](#1. 关联式容器的对比)
- [2. 哈希结构](#2. 哈希结构)
-
- [2.1 概念](#2.1 概念)
- [2.2 哈希冲突](#2.2 哈希冲突)
- [2.3 哈希函数](#2.3 哈希函数)
-
- [2.3.1 哈希函数设计原则](#2.3.1 哈希函数设计原则)
- [2.3.2 常见的哈希函数](#2.3.2 常见的哈希函数)
- [2.4 解决哈希冲突的方法](#2.4 解决哈希冲突的方法)
-
- [2.4.1 闭散列](#2.4.1 闭散列)
-
- [2.4.1.1 线性探测](#2.4.1.1 线性探测)
- [2.4.1.2 二次探测](#2.4.1.2 二次探测)
- [2.4.2 开散列](#2.4.2 开散列)
- [2.4.3 负载因子](#2.4.3 负载因子)
- [2.4.4 开散列与闭散列的比较](#2.4.4 开散列与闭散列的比较)
- [2.5 模拟实现](#2.5 模拟实现)
-
- [2.5.1 非整形类型转化为整数](#2.5.1 非整形类型转化为整数)
- [2.5.2 哈希表---闭散列模拟实现](#2.5.2 哈希表—闭散列模拟实现)
-
- [2.5.2.1 哈希表结构的定义](#2.5.2.1 哈希表结构的定义)
- [2.5.2.2 查询](#2.5.2.2 查询)
- [2.5.2.3 插入](#2.5.2.3 插入)
- [2.5.2.4 删除](#2.5.2.4 删除)
- [2.5.3 哈希桶---开散列模拟实现](#2.5.3 哈希桶—开散列模拟实现)
-
- [2.5.3.1 哈希表结构的定义](#2.5.3.1 哈希表结构的定义)
- [2.5.3.2 查询](#2.5.3.2 查询)
- [2.5.3.3 插入](#2.5.3.3 插入)
- [2.5.3.4 删除](#2.5.3.4 删除)
- [3. 哈希表封装unorered_map、unordered_set](#3. 哈希表封装unorered_map、unordered_set)
-
- [3.1 哈希表的模拟实现](#3.1 哈希表的模拟实现)
-
- [3.1.1 迭代器](#3.1.1 迭代器)
-
- [3.1.1.1 构造函数](#3.1.1.1 构造函数)
- [3.1.1.2 begin()+end()](#3.1.1.2 begin()+end())
- [3.1.1.3 operator++()](#3.1.1.3 operator++())
- [3.1.1.4 operator*()](#3.1.1.4 operator*())
- [3.1.1.5 operator->()](#3.1.1.5 operator->())
- [3.1.1.6 operator==()](#3.1.1.6 operator==())
- [3.1.1.7 operator!=()](#3.1.1.7 operator!=())
- [3.1.2 insert()](#3.1.2 insert())
- [3.1.3 完整代码](#3.1.3 完整代码)
- [4.1 unorered_set的模拟实现](#4.1 unorered_set的模拟实现)
-
- [4.1.1 完整代码](#4.1.1 完整代码)
- [5.1 unorered_map的模拟实现](#5.1 unorered_map的模拟实现)
-
- [5.1.1 operator[]](#5.1.1 operator[])
- [5.1.2 完整代码](#5.1.2 完整代码)
1. 关联式容器的对比
- 在C++98中,STL提供了底层为红黑树的关联式容器,如:set、map等,查询效率为O(longn),即最差情况下需要比较红黑树的高度次,当节点数目很多时,需要比较的次数多,查询效率低。在C++11中,STL提供了4个unordered系列的关联式容器,如:unordered_set、unordered_map等,底层结构为哈希表,通过存储值和存储位置的映射的关联关系,查询效率可以达到常数级别,即:O(1)。
- unordered_map、unordered_set底层结构为哈希结构,其容器内元素的顺序是无序的,单向迭代器。map、set底层结构为红黑树结构,其其容器内元素的顺序是有序的,双向迭代器。两者的成员函数使用方式基本相同、都有multi版本、key是唯一的。
- 在性能上差别:对于插入、删除操作,如果容器中元素重复值很多 或者 元素重复值不是很多时,map、set效率低于unordered_map、unordered_set。如果容器中元素无重复值时,map、set效率优于unordered_map、unordered_set。因为unordered系列容器存在哈希冲突、扩容有消耗,导致效率降低。
2. 哈希结构
2.1 概念
概念:存储值和存储位置的映射的关联关系。
-
顺序结构和平衡树,关键码和存储位置未建立关联关系,因此在查找元素时,关键码需要进行多次比较。顺序结构的查找效率为O(n),平衡树的查找效率为高度次O(longn)。搜索效率取决于在搜索过程中关键码比较的次数。
-
哈希方法的思想:在元素关键码k和元素存储位置p之间建立起一一映射关联关系H,使得p = H(k),H为哈希函数。因此在查找时,通过哈希函数(hash(key) = key%size=存储位置)的转化,使得查询效率达到常数级别,即:O(1)。
-
哈希方法中,使用的转换函数称为哈希函数(散列函数),构造出来的结构称为哈希表(散列表)。
2.2 哈希冲突
概念:不同的关键码通过同一哈希函数计算出相同的存储位置,称为哈希冲突或者哈希碰撞。把具有不同关键码而具有相同存储位置的数据元素称为"同义词"。
问:为什么会引起哈希冲突妮?
答:引起哈希冲突的一个原因可能是:哈希函数设计不够合理。
2.3 哈希函数
2.3.1 哈希函数设计原则
-
哈希函数的定义域必须包含需存储的全部关键码。若哈希表中有n个地址,其值域必须在[0, n-1]。
-
哈希函数计算出的存储位置能够均匀分布在空间内。
-
哈希函数比较简单。
2.3.2 常见的哈希函数
- 直接定址法
此处哈希函数一般为线性函数,hash(key) = A*key + B。
优点:计算简单,不会产生冲突。
缺点:需要事先知道数据的分布范围情况,若分布不连续,会造成内存空间大量浪费。
适用场景:查找数据范围比较小且连续的情况。
- 除留余数法
hash(key) = key % size。
优点:计算简单,适用于数据范围较广,是一种常见的哈希函数。
缺点:哈希冲突。 适用场景:整数的求模运算(int->存储位置)。
💡Tips:哈希函数设计的越精妙,哈希冲突效率就越低,搜索效率就越高,但仍无法避免哈希冲突。
2.4 解决哈希冲突的方法
2.4.1 闭散列
概念:闭散列,也称为开放定址法。若哈希表中未被装满,说明哈希表中还存在"空位置", 就可以把key放到冲突位置的下一个"空位置"处。
缺点:空间利用率较低,这也是哈希的缺陷。
- 问:如何找下一个空位置妮?
- 答:线性探测、二次探测等。
2.4.1.1 线性探测
概念:从冲突位置开始,依次往后进行探测,若探测到哈希表结尾位置,从头开始探测,直到找到下一个空位置为止。hash(key) = key%size + i(i>=0)。
缺点:一旦发生哈希冲突,所以的冲突堆积在一起,就会造成"数据堆积",各个数据相互影响,如同"恶性循环",即:每个关键码占据了可利用的空位置,使得找寻某个关键码时,不能直接找到,需要进行多次比较,搜索效率降低。
2.4.1.2 二次探测
概念:hash(key) = key%size + i^2(i>=0)。
2.4.2 开散列
概念:开散列,也称为开散列拉链法(开链法)、开散列哈希桶。首先先通过哈希函数计算出key在哈希表中的地址,将具有相同关键码key的元素归于同一个子集中,每个子集代表着一个桶,桶中的元素通过单链表连接起来,链表的头节点就存在哈希表中。
2.4.3 负载因子
概念:负载因子:也称为载荷因子,n = 实际存储的数据个数 / size。
负载因子越大,哈希冲突就越高,搜索效率就越低,空间利用率就越高。
对于闭散列,负载因子要控制在0.7~0.8以下,超过0.8,cpu缓存不命中按照指数曲线上升。
2.4.4 开散列与闭散列的比较
-
应用链地址法处理哈希冲突时,要增加链接的指针,似乎增加了存储开销。而开地址法必须保持大量空间空闲,以确保搜索效率,如:二次探测的负载因子<=0.7,而表所占的空间比指针大,所以使用开地址法反而比链地址法更节省空间。
-
闭散列负载因子到了0.7就需要扩容了,开散列负载因子到1就需要扩容了。
2.5 模拟实现
2.5.1 非整形类型转化为整数
-
哈希结构中,计算key所对应的存储位置需要用到求模运算%,但%两操作符必须为整形,对于string、自定义类型不适用,所以我们需要显示写一个仿函数,此仿函数的功能是将任意数据类型转化为整形。只需确保相同的key值转化为整形是唯一的。
-
仿函数hash作用:将各种数据类型K -> int->存储位置, eg : string、Data、Person -> int->存储值。 K类型有两种情况:本身为size_t或者可以强制类型(相近类型)转化为size_t、string或者其他自定义类型。hash默认情况下为第一种,若想要显示控制转化过程,需要自己手动写手动传。
cpp
template<class K>
struct HashFunc
{
size_t operator()(const K& key) //size_t、可强制类型转化为size_t
{
return (size_t)key;
}
};
cpp
//全特化,特化后若为显示传模板参数,默认情况下会去调用全特化,必须需要一个模板作为基础
template<> //因为string为常见类型,所以将其进行特化
{
size_t operator()(const string& s)
{
size_t hashi = 0;
for (auto& e : s)
{
hashi += e;
hashi *= 131; //BKDRhash算法,哈希冲突小,将字符串转化为整形
}
return hashi;
}
};
- BKDRHash算法是由Brian Kernighan与Dennis Ritchie的《The C Programming Language》一书被展示而得名,是一种简单快捷的hash算法,也是Java目前采用的字符串的Hash算法(累乘因子为31)。BKDRHash算法的原理是将字符串转化为一个整数,然后通过一系列位运算和取模运算,将整数压缩到一个固定范围内的值,最终得到哈希值。
cpp
struct HashFunPerson //Person
{
size_t operator()(const Person& p)
{
size_t hashi = 0;
hashi += p._id;
hashi *= 131;
return hashi;
}
};
cpp
struct Person
{
string _name;
size_t _id; //身份证号码,唯一
size_t age;
};
struct HashFunPerson //Person
{
size_t operator()(const Person& p)
{
size_t hashi = 0;
hashi += p._id;
hashi *= 131;
return hashi;
}
};
cpp
struct Data
{
size_t _year;
size_t _month;
size_t _day;
};
struct HashFunDate //Data
{
size_t operator()(const Data& d)
{
size_t hashi = 0;
hashi += d._year;
hashi *= 131;
hashi += d._month;
hashi *= 131;
hashi += d._day;
hashi *= 131;
return hashi;
}
};
2.5.2 哈希表---闭散列模拟实现
2.5.2.1 哈希表结构的定义
cpp
enum State //状态
{
Empty,
Exist,
Delete
};
template<class K, class V>
struct HashData{
pair<K, V> _kv; //数据值
State _state = Empty; //状态
};
/*注意:仿函数hash作用:将各种数据类型K -> int->存储位置, eg : string、Data、Person -> int->存储值
K类型有两种情况:本身为size_t或者可以强制类型(相近类型)转化为size_t、string或者其他自定义类型
hash默认情况下为第一种,若想要显示控制转化过程,需要自己手动写手动传*/
template<class K, class V, class Hash = HashFunc<K>>
class HashTable{
public:
HashTable(size_t n = 10) //构造函数
{
_table.resize(n);
}
private:
vector<HashData<K, V>> _table; //哈希表的物理结构为数组 ------》指针数组
size_t _n = 0; //实际存储的数据个数
};
2.5.2.2 查询
cpp
HashData<K, V>* find(const K& key) //查找
{
Hash hs; //仿函数类创建对象,对象调用operator()进行转化为整形
size_t hashi = hs(key) % _table.size(); //key在哈希表中的位置
while (_table[hashi]._state != Empty) //直到遇到空,查找结束
{
if (_table[hashi]._kv.first == key && _table[hashi]._state == Exist)
return &_table[hashi]; //找到了
hashi++; //存在和删除状态继续线性向后查找
hashi = hashi % _table.size(); //走到了哈希表的结尾,从头查找
}
return nullptr; //找不到
}
2.5.2.3 插入
cpp
bool insert(const pair<K, V>& kv) //插入
{
Hash hs;
if (find(kv.first)) return false; //unordered_map、unordered_set中key不能重复、
if ((double)_n / _table.size() >= 0.7) //负载因子>=0.7,进行扩容
{
//1.创建新表vector<HashData<K, V>> newtable(_table.size()*2);
//2.遍历旧表,重新映射到新表 -> 冗余
//3.新旧表交换_table.swap(newtable);
HashTable<K, V, Hash> newHT(_table.size() * 2);
for (auto& e : _table) //遍历旧表,重新映射到新表
{
if (e._state == Exist)
newHT.insert(e._kv); //递归,实现代码复用
}
_table.swap(newHT._table);
}
//关键码与存储位置的映射
size_t hashi = hs(kv.first) % _table.size(); //注意:%左右操作符必须为整数
while (_table[hashi]._state == Exist) //发生哈希冲突,线性往后找"空",进行插入
{
hashi++;
hashi = hashi % _table.size();
}
_table[hashi]._kv = kv;
_table[hashi]._state = Exist;
_n++;
return true;
}
2.5.2.4 删除
cpp
bool erase(const K& key) //删除 不能直接删除该位置上的值,会影响其他关键码的删除,用状态来标记
{
//查找到了,再进行删除
HashData<K, V>* ret = find(key);
if (ret)
{
ret->_state = Delete;
_n--;
return true;
}
else
return false;
}
2.5.3 哈希桶---开散列模拟实现
2.5.3.1 哈希表结构的定义
cpp
template<class K, class V>
struct HashNode {
typedef HashNode<K, V> Node;
Node* _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{ //开散列拉链法、开散列哈希桶
public:
typedef HashNode<K, V> Node;
HashTable() //构造函数
{
_table.resize(10, nullptr);
}
~HashTable() //析构函数 ------》 对象中进行了资源申请,需要显示写
{
for (int i = 0; i < _table.size(); i++)
{
Node* next = nullptr;
Node* cur = _table[i];
while (cur) //释放单链表
{
next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
private: //vector<list<K, V>> _table结构也可以,但带头双向循环链表空间开销大
vector<Node*> _table; //哈希表,里面存储的是单链表的头节点
size_t _n = 0; //实际存储的数据个数
};
2.5.3.2 查询
cpp
Node* find(const K& key) //查找
{
Hash hs;
size_t hashi = hs(key) % _table.size();
Node* cur = _table[hashi]; //key所对应的桶
while (cur) //遍历桶中的单链表
{
if (cur->_kv.first == key)
return cur; //找到了
cur = cur->_next;
}
return nullptr; //没找到
}
- 先使用哈希函数将key转化为哈希地址,找到对应的桶后,从前往后遍历单链表,直到单链表为空 或者 找到了。
2.5.3.3 插入
cpp
bool insert(const pair<K, V>& kv) //插入 本质:开空间,添加新节点
{
if (find(kv.first)) return false; //key是唯一的
//最好情况:每个桶下面只有一个元素
if (_n == _table.size()) //扩容
{
vector<Node*> _newtable(_n * 2, nullptr); //开辟新的哈希表
//取出旧表中每个桶中的单链表上节点,重新计算(映射),链接到新表中,头插
for (size_t i = 0; i < _table.size(); i++)
{
Node* next = nullptr;
Node* cur = _table[i];
while (cur)
{
next = cur->_next;
cur->_next = _newtable[i];
_newtable[i] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(_newtable); //新旧表交换
}
Hash hs;
size_t hashi = hs(kv.first) % _table.size(); //起始size=10
Node* newnode = new Node(kv);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
_n++;
return true;
}
-
先使用哈希函数将key转化为哈希地址,找到对应的桶后,头插。若表中存在待插入元素,则插入失败,因为unordered_map、unordered_set中的key是唯一的。
-
开散列扩容:存储的元素个数等于桶的个数时,需要扩容。因为哈希表中桶的个数是一定的,随着元素的不断的插入,桶中的元素个数不断增加,再极端情况下,一个桶中有很多很多节点,导致查找性能降低,最好的情况是每个桶中只挂一个节点,每插入一次,就会发生哈希冲突,此时需要进行扩容。
2.5.3.4 删除
cpp
bool erase(const K& key) //删除
{
Hash hs;
size_t hashi = hs(key) % _table.size();
Node* prev = nullptr; //单链表的删除需要记录前一个节点,否则会找不到当前节点的下一个
Node* cur = _table[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
if (prev != nullptr) //中间以及后面位置的删除
prev->_next = cur->_next;
else //头删
_table[hashi] = cur->_next;
delete cur; //删除
cur = nullptr;
_n--;
return true;
}
prev = cur;
cur = cur->_next;
}
return false; //key不存在
}
- 先使用哈希函数将key转化为哈希地址,找到对应的桶后,从前往后遍历单链表,直到单链表为空 或者找到了,找到了才能进行删除,但不能直接使用find函数,因为find函数只返回表中要删除元素的地址,而单链表的删除需要记录删除节点的前一个节点,让前一个结点指向删除节点的下一个节点。
3. 哈希表封装unorered_map、unordered_set
- unordered_map、unordered_set共用同一个哈希表,所以将哈希表变为模板。因为与map、set用法上基本上无区别,所以两者的实现基本相同。
3.1 哈希表的模拟实现
3.1.1 迭代器
cpp
template<class K, class T, class KeyOfT, class Hash> // 注意 : 前置声明
class HashTable; //类模板的声明必须带上模板参数
template<class K, class T, class KeyOfT, class Hash>
struct Hs_iterator { //迭代器
typedef HashNode<T> Node;
typedef HashTable<K, T, KeyOfT, Hash> HT;
typedef Hs_iterator<K, T, KeyOfT, Hash> Self;
Node* _node; //节点的地址 -》可实现*、!=、==、->
HT* _hs; //对象的地址 -》可实现++,为了找到下一个非空桶方便
};
3.1.1.1 构造函数
cpp
Hs_iterator(Node* node, HT* hs) //构造函数
:_node(node)
,_hs(hs)
{ }
3.1.1.2 begin()+end()
💡iterator begin( ) ;
- 功能:返回哈希表中第一个非空桶的第一个节点的迭代器
cpp
iterator begin() //哈希表中第一个非空桶的第一个位置
{
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i])
{
return iterator(_table[i], this);
}
}
return end(); //哈希表中全为空桶
}
💡iterator end( ) ;
- 功能:返回空指针。
cpp
iterator end()
{
return iterator(nullptr, this);
}
3.1.1.3 operator++()
cpp
//对于++,走到某个桶的结束位置,需要找下一个非空桶,此时需要借助_table数组
Self& operator++()
{
KeyOfT kt;
Hash ht;
if (_node->_next) //当前桶还有数据,直接取当前迭代器所指向节点的下一个节点
_node = _node->_next;
else //当前桶走完了,需要找下一个非空桶的第一个节点
{
size_t hashi = ht(kt(_node->_kv)) % _hs->_table.size();
hashi++;
while (hashi < _hs->_table.size())
{
if (_hs->_table[hashi]) //找到了
{
_node = _hs->_table[hashi];
break;
}
hashi++;
}
if (hashi == _hs->_table.size()) //不存在下一个非空桶
_node = nullptr;
}
return *this;
}
- 如果当前桶还有数据,直接取当前迭代器所指向节点的下一个节点 ; 如果当前桶走完了,需要找下一个非空桶的第一个节点。此时需要借助_table。
3.1.1.4 operator*()
cpp
T& operator*() //解引用
{
return _node->_kv;
}
3.1.1.5 operator->()
cpp
T* operator->() //结构体指针
{
return &_node->_kv;
}
3.1.1.6 operator==()
cpp
bool operator==(const Self& it)
{
return _node == it._node;
}
3.1.1.7 operator!=()
cpp
bool operator!=(const Self& it)
{
return _node != it._node;
}
3.1.2 insert()
💡pair<iterator,bool> insert(const T& kv) ;
- 功能:向红黑树中插入data。
- insert返回值为pair<iterator, bool>,若key(unordered_set的key、unordered_map的pair的first)在哈希表中存在,因为键值key不能重复,所以pair::first指向在哈希表中与key值相等节点的迭代器,pair::second为false。若key在哈希表中不存在,pair::first指向在哈希表中新插入节点的迭代器,pair::second为true。insert相当于查找。
cpp
pair<iterator, bool> insert(const T& kv) //插入 本质:开空间,添加新节点
{
KeyOfT kot;
iterator ret = find(kot(kv));
if (ret != end()) return make_pair(ret, false); //key是唯一的 插入失败,返回该节点在哈希表的位置(迭代器)
//最好情况:每个桶下面只有一个元素
if(_n == _table.size()) //负载因子为1,扩容
{
vector<Node*> _newtable(_n * 2, nullptr); //开辟新的哈希表
// 取出旧表中每个桶中的单链表上节点,重新计算(映射),链接到新表中,头插
for (size_t i = 0; i < _table.size(); i++)
{
Node* next = nullptr;
Node* cur = _table[i];
while (cur)
{
next = cur->_next;
cur->_next = _newtable[i];
_newtable[i] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(_newtable); //新旧表交换
}
Hash ht;
size_t hashi = ht(kot(kv)) % _table.size(); //起始size=10
Node* newnode = new Node(kv);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
_n++;
return make_pair(iterator(newnode, this), true);
}
3.1.3 完整代码
cpp
/*注意:仿函数hash作用:将各种数据类型K -> int->存储位置, eg : string、Data、Person -> int->存储值
K类型有两种情况:本身为size_t或者可以强制类型(相近类型)转化为size_t、string或者其他自定义类型
hash默认情况下为第一种,若想要显示控制转化过程,需要自己手动写手动传*/
template<class K, class T, class KeyOfT, class Hash> //KeyOfT:取出T中的第一个值
class HashTable { //开散列拉链法、开散列哈希桶
public:
//在迭代器++中,使用了private成员_table,在类外不能访问私有成员,若想被访问,则设置成友元
template<class K, class T, class KeyOfT, class Hash>
friend struct Hs_iterator; //将类模板设置为友元类,需要加上模板参数
typedef HashNode<T> Node;
typedef HashTable<K, T, KeyOfT, Hash> HT;
typedef Hs_iterator<K, T, KeyOfT, Hash> iterator;
iterator begin() //哈希表中第一个非空桶的第一个位置
{
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i])
{
return iterator(_table[i], this);
}
}
return end(); //哈希表中全为空桶
}
iterator end()
{
return iterator(nullptr, this);
}
HashTable() //构造函数
{
_table.resize(10, nullptr);
}
HashTable(const HT& ht) //拷贝构造函数
{
_table.resize(10, nullptr); //开空间
for (int i = 0; i < ht.size(); i++) //拷贝数据
{
if (ht._table[i])
insert(ht._table[i]->_kv);
}
}
HT& operator=(HT ht) //赋值运算符重载
{
_table.swap(ht._table);
swap(_n, ht._n);
return *this;
}
~HashTable() //析构函数 ------》 对象中进行了资源申请,需要显示写
{
for (int i = 0; i < _table.size(); i++)
{
Node* next = nullptr;
Node* cur = _table[i];
while (cur) //释放单链表
{
next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
iterator find(const K& key) //查找
{
Hash ht;
KeyOfT kot;
size_t hashi = ht(key) % _table.size();
Node* cur = _table[hashi]; //key所对应的桶
while (cur) //遍历桶中的单链表
{
if (kot(cur->_kv) == key)
return iterator(cur, this); //找到了
cur = cur->_next;
}
return end(); //没找到
}
pair<iterator, bool> insert(const T& kv) //插入 本质:开空间,添加新节点
{
KeyOfT kot;
iterator ret = find(kot(kv));
if (ret != end()) return make_pair(ret, false); //key是唯一的 插入失败,返回该节点在哈希表的位置(迭代器)
//最好情况:每个桶下面只有一个元素
if(_n == _table.size()) //负载因子为1,扩容
{
vector<Node*> _newtable(_n * 2, nullptr); //开辟新的哈希表
// 取出旧表中每个桶中的单链表上节点,重新计算(映射),链接到新表中,头插
for (size_t i = 0; i < _table.size(); i++)
{
Node* next = nullptr;
Node* cur = _table[i];
while (cur)
{
next = cur->_next;
cur->_next = _newtable[i];
_newtable[i] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(_newtable); //新旧表交换
}
Hash ht;
size_t hashi = ht(kot(kv)) % _table.size(); //起始size=10
Node* newnode = new Node(kv);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
_n++;
return make_pair(iterator(newnode, this), true);
}
bool erase(const K& key) //删除
{
Hash _ht;
KeyOfT kot;
size_t hashi = _ht(key) % _table.size();
Node* prev = nullptr; //单链表的删除需要记录前一个节点
Node* cur = _table[hashi];
while (cur)
{
if (kot(cur->_kv) == key)
{
if (prev != nullptr) //中间以及后面位置的删除
prev->_next = cur->_next;
else //头删
_table[hashi] = cur->_next;
delete cur; //删除
cur = nullptr;
_n--;
return true;
}
prev = cur;
cur = cur->_next;
}
return false; //key不存在
}
private:
vector<Node*> _table; //哈希表,里面存储的是单链表的头节点 ------》指针数组
size_t _n = 0; //实际存储的数据个数
};
4.1 unorered_set的模拟实现
4.1.1 完整代码
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"HashTable.h"
namespace zzx {
template<class K, class Hash = HashFunc<K>>
class unordered_set {
public:
struct SetKeyOfT {
const K& operator()(const K& key)
{
return key;
}
};
//typename作用:区分类域中的静态成员变量和typedef类型
typedef typename HashTable<K, K, SetKeyOfT, Hash>::iterator iterator;
iterator find(const K& key)
{
return _hs.find(key);
}
pair<iterator, bool> insert(const K& key)
{
return _hs.insert(key);
}
bool erase(const K& key)
{
return _hs.erase(key);
}
iterator begin()
{
return _hs.begin();
}
iterator end()
{
return _hs.end();
}
private:
HashTable<K, K, SetKeyOfT, Hash> _hs;
};
}
5.1 unorered_map的模拟实现
5.1.1 operator[]
💡V& operator[ ](const K& key) ;
-
功能:访问与key相对应的value值。即可读又可写。
-
原理:operator[ ]底层是通过调用insert( )将键值队插入到unordered_map中。如果key存在,插入失败,insert返回与unordered_map中key值相同元素的迭代器。如果key不存在,插入成功,insert返回在unordered_map中新插入元素的迭代器。operator[ ]最后返回与key值相对应的value值的引用。
-
operator[ ] 具有插入、查找、插入+修改、查找+修改功能。
cpp
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
5.1.2 完整代码
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"HashTable.h"
namespace zzx {
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map {
public:
struct MapKeyOfT {
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
typedef typename HashTable<K, pair<K, V>, MapKeyOfT, Hash>::iterator iterator;
iterator find(const K& key)
{
return _hs.find(key);
}
pair<iterator, bool> insert(const pair<K, V>& kv)
{
return _hs.insert(kv);
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
bool erase(const K& key)
{
return _hs.erase(key);
}
iterator begin()
{
return _hs.begin();
}
iterator end()
{
return _hs.end();
}
private:
HashTable<K, pair<K, V>, MapKeyOfT, Hash> _hs;
};
}