本篇建立在已经熟悉map/set模拟实现和开散列的模拟实现的基础上,关于开散列,可以看上一篇文章:
【数据结构】哈希——闭散列/开散列模拟实现(C++)-CSDN博客本文对比分析了C++中unordered_map/unordered_set与map/set的主要区别。底层实现上,前者采用哈希表(O(1)时间复杂度),后者使用红黑树(O(logN))。重点探讨了哈希冲突的两种解决方案:闭散列(开放定址法)和开散列(哈希桶)。闭散列通过线性探测或二次探测解决冲突,但存在效率问题;开散列则通过链表存储冲突元素,效率更高。文章还提供了哈希表的模拟实现代码,包括插入、查找、删除等操作,并比较了两种容器在百万级数据下的性能差异。
https://blog.csdn.net/suimingtao/article/details/149002209
【数据结构】C++的map/set模拟实现(红黑树作底层)-CSDN博客文章浏览阅读777次,点赞11次,收藏26次。本文介绍了基于红黑树实现C++中的map和set容器。通过复用单一红黑树结构,分别处理K模型(set)和KV模型(map)。重点内容包括:1) 红黑树改造,引入KeyOfValue仿函数解决不同类型值比较问题;2) 迭代器实现,包括++/--操作的中序遍历逻辑;3) map的[]运算符重载,通过insert返回pair 实现。相比传统双树实现方式,这种高级复用方案减少了代码冗余,同时保持了高效性能,插入操作时间复杂度为O(logN)。https://blog.csdn.net/suimingtao/article/details/148928079
迭代器实现
在实现unordered_map/unordered_set之前,先将开散列的迭代器实现一下
迭代器的结构
对于迭代器来说,也需要传KOfT,Hash参数,还需要一个该哈希表的指针(即下面的_pht),关于原因稍候早说
template<class K,class T,class KOfT,class Hash,class Ref,class Ptr>
struct HashIterator
{
typedef HashNode<T> Node;
typedef HashTable<K, T, KOfT, Hash> HT;
typedef __HashIterator<K, T, KOfT, Hash,Ref,Ptr> Self;//迭代器本身
HashIterator(Node* node,HT* pht)//默认构造
:_node(node)
,_pht(pht)
{ }
private:
Node* _node;//每个桶中的节点
HT* _pht;//当一个桶走完时,为了可以找到下一个桶,需要该哈希表的指针
};
迭代器的解引用
迭代器中主要存的还是一个节点的指针,即_node,对于operator*和operator->重载,了解过map/set的都知道,operator*重载对应set的解引用,因为set中只存key,解引用时也只需要"*";operator->重载对应map的解引用,因为map中存key和value,解引用时肯定需要"->"指定对应的值
Ref operator*()//unordered_set的解引用
{
return _node->_data;
}
Ptr operator->()//unordered_map的解引用
{
return &_node->_data//在调用"it->first"时,其实是调用了"it->->first",这里编译器省略了第一次"->",而这里返回的就是第一次调用的结果,即pair类型的地址
}
顺便将等于和不等于重载实现一下
bool operator!=(Self it)
{
return _node != it._node;
}
bool operator==(Self it)
{
return _node == it._node;
}
迭代器的++
unordered_map/unordered_set的迭代器是单向迭代器,因此只有++,没有--
假设现在的迭代器在第一个桶的第一个节点,当它遍历完该桶后,要怎么跳转到下一个有节点的桶?
此时传该哈希表的指针(即_pht)的意义就来了,有了该哈希表的指针,就可以遍历整个哈希表中所有的哈希桶,但要从哪开始遍历?程序怎么知道当前迭代器在哪个桶?
此时传KOfT和Hash的作用就来了,通过对当前迭代器的值除留余数法,就能算出当前迭代器在哪个桶,再去从该桶后面开始遍历即可
Self operator++()//前置++
{
assert(_node);//防止节点本来就为空
if (_node->_next)//如果该桶还有节点
{
_node = _node->_next;
}
else//该桶已被遍历完,需要找下一个桶
{
KOfT koft;//用于从T类型取出key
size_t index = _pht->HashFunc(koft(_node->_data)) % _pht->_table.size();//除留余数法取出映射
index++;
for (; index < _pht->_table.size())//从下一个桶开始找非空桶
{
Node* cur = _pht->_table[index];
if (cur)
{
_node = cur;
return *this;//若找到,就直接返回本身迭代器
}
}
_node = nullptr;//找不到就将迭代器置end()
}
return *this;
}
Self operator++(int)//后置++
{
Self tmp = *this;
++*this;
return tmp;
}
改造哈希表
首先哈希表内部与迭代器支持一下
template<class K,class T,class KOfT,class Hash>//T可能是K或pair,KOfT是为了从T中取出key的仿函数,Hash是将不同类型的K转换为整型的仿函数
class HashTable
{
typedef HashNode<T> Node;
typedef typename __HashIterator<K, T, KOfT, Hash, T&, T*> iterator;
typedef typename __HashIterator<K, T, KOfT, Hash, const T&, const T*> const_iterator;//只读迭代器
friend iterator;//将只读和读写迭代器都变为友元,迭代器不管什么类型就都可以访问HashTable的_table
friend const_iterator;
public:
iterator begin()
{
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i])
return iterator(_table[i], this);//将第一个节点转换为迭代器
}
return end();
}
iterator end()
{
return nullptr;//这里就实现的简单点吧
}
const_iterator begin() const
{
for (size_t i = 0; i < _table.size(); i++)
{
if (_table[i])
return const_iterator(_table[i], this);//将第一个节点转换为迭代器
}
return end();
}
const_iterator end() const
{
return nullptr;
}
//.................
}
由于需要unordered_map需要支持operator[],因此需要先将原来的哈希表的Insert改造一下,返回值改为pair<iterator,bool>
pair<iterator,bool> Insert(const T& data)
{
KOfT koft;//用于取出T类型的Key
if (_table.size() == _num)//负载因子到1时扩容
{
//1.开2倍大小空间(不一定是2倍)
//2.遍历旧表数据,重新计算在新表中位置
//3.释放旧表
vector<Node*> newtable;
size_t newsize = _table.size() ? _table.size() * 2 : 10;
newtable.resize(newsize);
for (auto& cur : _table)
{
//将旧表中的节点取下来重新计算在新表中的位置,并插入进去
Node* head = cur;
while (head)
{
Node* next = head->_next;
size_t index = HashFunc(koft(head->_data)) % newtable.size();
//头插进桶中
head->_next = newtable[index];
newtable[index] = head;
head = next;
}
cur = nullptr;//将旧表该位置置空,否则最后析构旧表时就将节点都析构了
}
_table.swap(newtable);//旧表在出作用域后自动销毁
}
size_t index = HashFunc(koft(data)) % _table.size();//除留余数法取出映射
//先查找值是否重复
Node* cur = _table[index];
while (cur)
{
if (koft(cur->_data) == koft(data))//如果是重复值,插入失败
return make_pair(iterator(cur,this),false);
else
cur = cur->_next;
}
Node* newnode = new Node(data);//开辟新空间
//头插到对应桶中(尾插也可以)
newnode->_next = _table[index];
_table[index] = newnode;
_num++;
return make_pair(iterator(newnode,this),true);
}
Find的返回值也改成迭代器
iterator Find(const K& key)
{
KOfT koft;//用于取出T类型的key
size_t index = HashFunc(key) % _table.size();//算出映射
//去该映射桶中找
Node* cur = _table[index];
while (cur)
{
if (koft(cur->_data) == key)
return iterator(cur,this);
else
cur = cur->_next;
}
return iterator(nullptr,this);//找不到返回空
}
模拟实现unordered_map/unordered_set
和map/set时一样,只需要将哈希表套进去就可以
unordered_map
#pragma once
#include "HashTable.h"
namespace valkyrie
{
using namespace Open_Hash;
template<class K,class V,class Hash = _Hash<K>>
class unordered_map
{
struct KeyOfMap//用于从pair类型中取出K
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename HashTable<K, pair<K, V>, KeyOfMap, Hash>::iterator iterator;
typedef typename HashTable<K, pair<K, V>, KeyOfMap, Hash>::const_iterator const_iterator;
pair<iterator, bool> Insert(pair<K, V> kv)
{
return _ht.Insert(kv);
}
iterator Find(const K& key)
{
return _ht.Find(key);
}
bool Erase(const K& key)
{
return _ht.Erase(key);
}
V& operator[](const K& key)
{
pair<iterator, bool> pr = _ht.Insert(make_pair(key, V()));//不管插入失败还是成功,first中存的都是该Key的迭代器
return pr.first->second;
}
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
const_iterator begin() const
{
return _ht.begin();
}
const_iterator end() const
{
return _ht.end();
}
void clear()
{
_ht.clear();
}
private:
HashTable<K, pair<K, V>, KeyOfMap, Hash> _ht;//哈希表作底层
};
}
unordered_set
#pragma once
#include "HashTable.h"
namespace valkyrie
{
using namespace Open_Hash;
template<class K, class Hash = _Hash<K>>
class unordered_set
{
struct KeyOfSet//为了适配KeyOfMap
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename HashTable<K, K, KeyOfSet, Hash>::iterator iterator;
typedef typename HashTable<K, K, KeyOfSet, Hash>::const_iterator const_iterator;
pair<iterator, bool> Insert(const K& key)
{
return _ht.Insert(key);
}
iterator Find(const K& key)
{
return _ht.Find(key);
}
bool Erase(const K& key)
{
return _ht.Erase(key);
}
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
const_iterator begin() const
{
return _ht.begin();
}
const_iterator end() const
{
return _ht.end();
}
void clear()
{
_ht.clear();
}
private:
HashTable<K, K, KeyOfSet, Hash> _ht;//内部结构是哈希表
};
}
针对迭代器遍历的优化(思路)
在STL实现的unordered_map/unordered_set中,迭代器遍历的顺序就是插入数据的顺序,而目前我们自己实现的迭代器遍历是遍历桶来完成的,有没有什么办法可以完成像STL里的那种效果呢?
我们可以在哈希表的节点结构中再加两个成员:_linknext和_linkprev
template<class T>
struct HashNode
{
T _data;//K或KV
HashNode* _next;//单链表
HashNode* _linknext;//下一个插入的元素
HashNode* _linkprev;//上一个插入的元素
HashNode(const T& data)//默认构造,为下面的new HashNode而准备
:_data(data)
, _next(nullptr)
{ }
};
_linknext是为了遍历,_linkprev是为了在删除时可以找到上一个元素
虽然这样可以在遍历时按照插入的顺序,但这也会让哈希表的结构更加复杂.....这里就不作实现了