「C++」哈希表的实现(unordered系底层)
💻文章目录
- 📄前言
- 哈希表概念
- 哈希函数
- 哈希冲突
- 闭散列
- 开散列
- 📓总结
📄前言
unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构,使其在查找上的时间复杂度几乎减低到了 O ( 1 ) O(1) O(1)。
哈希表概念
顺序结构或者平衡树中,要查找一个元素,必须要经过关键码(查找的数值)的多次比较,顺序表和平衡树最佳的查找时间复杂度都为 O ( l o g 2 N ) O(log2_N) O(log2N)。
哈希,是一种关键码与数值所一一映射的结构,如果能通过某种函数(HashFunc)使元素的存储位置和他的关键码创建一种映射关系,那么在查找时可以通过该函数快速的找到元素,而存储关键码和数值的顺序表就是哈希表。
哈希表的样例
哈希函数
哈希表是通过哈希函数构成的结构,其本质也是数组 。哈希方式中使用的函数也被成为哈希函数,使用哈系函数构成的结构称为哈希表。
常见的哈希函数
- 直接定址法–(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况 - 除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址 - 平方取中法–(了解)
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
哈希冲突
哈希冲突指的是不同关键码通过哈希函数被分配到了同一个哈希地址,哈希冲突是无法避免的,解决冲突的两种常见办法是:闭散列和开散列
闭散列
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置
呢?
-
线性探测
比如2.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,
因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。-
空间标记
采用线性探测时,为了识别哈希表的位置是否为空,可以给表中每个空间一个标记。// 哈希表每个空间给个标记 // EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除 enum State{EMPTY, EXIST, DELETE};
-
插入
- 通过哈希函数获取待插入元素在哈希表中的位置
- 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
-
删除
采用闭散列删除时,采用伪删除法删除一个元素,即把需要删除元素的空间设为DELETE。
-
线性探测的实现
template<class K, class V>
class HashTable
{
enum State{ EMPTY, EXIST, DELETE };
struct Elem //哈希表存储的元素
{
std::pair<K, V> _kv;
State _state;
};
public:
HashTable(size_t capacity = 3)
: _ht(capacity), _size(0), _totalSize(0)
{
for (size_t i = 0; i < capacity; ++i)
_ht[i]._state = EMPTY;
} //初始化元素
void print()
{ //打印元素
for(int i = 0; i < _ht.size(); ++i)
{
if(_ht[i]._state == EXIST)
std::cout << _ht[i]._kv.first << ":" << _ht[i]._kv.second << std::endl;
}
}
// 插入
bool Insert(const std::pair<K, V>& val)
{
if(Find(val.first) != -1)
return false;
CheckCapacity(); //检查是否需要扩容
size_t hashi = HashFunc(val.first);
while(_ht[hashi]._state == EXIST)
{ //存在冲突的情况,找到下一个非空
hashi++;
hashi %= _ht.size();
}
_ht[hashi]._kv = val;
_ht[hashi]._state = EXIST;
++_size;
return true;
}
// 查找
size_t Find(const K& key)
{
size_t hashi = HashFunc(key);
CheckCapacity();
while(_ht[hashi]._state != EMPTY)
{
if(_ht[hashi]._state == EXIST
&& _ht[hashi]._kv.first == key)
return hashi;
hashi++;
hashi %= _ht.size();
}
return -1;
}
// 删除
bool Erase(const K& key)
{
size_t hashi = Find(key);
if(hashi == -1) return false;
_ht[hashi]._state = DELETE; //直接讲元素设为DELETE
--_size;
return true;
}
size_t Size()const
{
return _size;
}
bool Empty() const
{
return _size == 0;
}
void Swap(HashTable<K, V>& ht) //交换节点
{
std::swap(_size, ht._size);
std::swap(_totalSize, ht._totalSize);
_ht.swap(ht._ht);
}
private:
size_t HashFunc(const K& key)
{
return key % _ht.capacity();
}
void CheckCapacity()
{
if(_size * 10 / _ht.size() >= 7)
{
HashTable<K, V> newHT;
newHT._ht.resize(_ht.size() * 2);
for(size_t i = 0; i < _ht.size(); ++i)
{
if(_ht[i]._state == EXIST)
newHT.Insert(_ht[i]._kv);
}
_ht.swap(newHT._ht);
}
}
private:
std::vector<Elem> _ht;
size_t _size;
};
}
开散列
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
开散列哈希表及其迭代器的声明
template <class T>
struct HashBucketNode
{
HashBucketNode<T>* _next; //下一个节点
T _data; //节点的数据
HashBucketNode(const T& data)
:_data(data)
,_next(nullptr)
{}
};
// 为了实现简单,在哈希桶的迭代器类中需要用到hashBucket本身,
// 因为迭代器中用到了哈希桶,所以得先声明。
template<class K, class T, class KeyOfValue, class HF>
class HashBucket;
// 注意:因为哈希桶在底层是单链表结构,所以哈希桶的迭代器不需要--操作
template <class K, class V, class Ref, class Ptr, class KeyOfValue, class HF>
struct HBIterator
{
typedef HashBucket<K, V, KeyOfValue, HF> HBK;
typedef HashBucketNode<V> Node;
typedef HBIterator<K, V, Ref, Ptr, KeyOfValue, HF> Self;
typedef HBIterator<K, V, V&, V*, KeyOfValue, HF> iterator; //这个是为了实现unordered_set所准备的
Node* _node; // 当前迭代器关联的节点
const HBK* _pHt; // 哈希桶--主要是为了找下一个空桶时候方便
size_t _hashi; //当前的未知
HBIterator(const iterator& it)
:_node(it._node)
,_pHt(it._pHt)
,_hashi(it._hashi)
{}
HBIterator(Node* pNode = nullptr, const HBK* pHt = nullptr, size_t hashi = -1)
:_node(pNode)
,_pHt(pHt)
,_hashi(hashi)
{}
Self& operator++(); //C++ unordereded系不支持--操作
Ref operator*();
Ptr operator->();
}
template <class K>
struct HashFuc //将数据转换成int,方便哈希函数取余数
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template <class K, class T, class KeyOfValue, class HF = HashFuc<K>>
class HashBucket //哈系桶
{
template<class Key, class Value, class Ref, class Ptr, class KeyOfT, class Hash> //clang error
friend struct HBIterator; //迭代器需要使用到类的私有成员,所以将其设为友元类
public:
typedef HashBucketNode<T> Node;
typedef HBIterator<K, T, T&, T*, KeyOfValue, HF> iterator;
typedef HBIterator<K, T, const T&, const T*, KeyOfValue, HF> const_iterator;
iterator find(const K& key); //查找
std::pair<iterator, bool> insert(const T& val); //插入
bool erase(const K& key); //删除
private:
void CheckCapacity(); //检查是否需要扩容
private:
HF _hf;
KeyOfValue _kot;
std::vector<Node*> _ht;
size_t _size;
迭代器的实现
Self& operator++()
{
_node = _node->_next;
if(!_node) //如果节点为空,在哈希表探索
{
while(!_node && _hashi < _pHt->_ht.size())
{ //寻找下一个非空节点
_node = _pHt->_ht[++_hashi];
}
}
if(_hashi >= _pHt->_ht.size())
_node = nullptr;
return *this;
}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &operator*();
}
插入实现
iterator find(const K& key)
{
size_t hashi = _hf(key) % _ht.size();
Node* cur = _ht[hashi];
while(cur)
{
if(_kot(cur->_data) == key)
return iterator(cur, this, hashi);
cur = cur->_next;
}
return iterator(cur);
}
std::pair<iterator, bool> insert(const T& val) //插入
{
iterator it = find(_kot(val)); //寻找位置
if (it != end())
{ //节点存在的情况
return std::make_pair(it, false);
}
CheckCapacity(); //检查扩容
size_t hashi = _hf(_kot(val)) % _ht.size();
Node* node = new Node(val);
node->_next = _ht[hashi]; //头插到所在位置
_ht[hashi] = node;
_size++;
return std::make_pair(iterator(node, this, hashi), true);
}
void CheckCapacity()
{
if(_size == _ht.size())
{
std::vector<Node*> newHT;
newHT.resize(_ht.size() * 2, nullptr);
for(size_t i = 0; i < _ht.size(); ++i)
{
Node* node = _ht[i];
while(node)
{
Node* next = node->_next;
size_t hashi = _hf(_kot(node->_data)) % newHT.size();
node->_next = newHT[hashi];
newHT[hashi] = node;
node = next;
}
_ht[i] = nullptr;
}
_ht.swap(newHT);
}
}
删除
bool erase(const K& key) //删除
{
size_t hashi = _hf(key) % _ht.size();
Node* cur = _ht[hashi];
Node* prev = nullptr;
while(cur)
{
if(_kot(cur->_data) == key)
{
if(!prev)
{
_ht[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
--_size;
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
📓总结
优点 | 缺点 | |
---|---|---|
闭散列 | 实现简单 | 容易导致数据堆积 |
开散列 | 存储开销减少 | 如果数据过于集中,会导致查找性能上的损耗 |
📜博客主页:主页
📫我的专栏:C++
📱我的github:github