前言
unordered 系列容器(unordered_map / unordered_set …),其底层封装了一张哈希表(开散列、哈希桶),重点在于哈希表的结构以及相关的接口的实现。
一、模拟实现 unordered 系列容器
哈希表(哈希桶)的结构:
1.1 定义哈希表的节点结构
这里直接拿我们自己写的开散列(原先是KV模型的)改造下,使其既可兼容K模型,也可兼容KV模型:
哈希表里面具体存的是什么类型的元素,是由模板参数 T 来决定:
- 如果是 unordered_set,传给 T 的就是 Key
- 如果是 unordered_map,传给 T 的就是 pair<const Key, V>
哈希表节点结构:
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data)
: _data(data)
, _next(nullptr)
{}
};
1.2 定义哈希表的迭代器
迭代器的结构:
👉注意:哈希表迭代器封装的是「节点指针」和「哈希表指针」,因为要在哈希表中找下一个桶,所以要用到哈希表指针
template<class K, class T, class Hash, class KeyOfT>
class HashTable;
template<class K, class T, class Hash, class KeyOfT>
struct HTIterator
{
typedef HashNode<T> Node;
typedef HashTable<K, T, Hash, KeyOfT> HashTable;
typedef HTIterator<K, T, Hash, KeyOfT> Iter;
Node* _node;
HashTable* _ht;
HTIterator(Node* node, HashTable* ht)
: _node(node)
, _ht(ht)
{}
T& operator*();
T* operator->();
Iter& operator++();
};
迭代器中运算符的重载:
1、让迭代器具有类似指针的行为,* 和 -> 运算符的重载
T& operator*()
{
return _node->_data;
}
T* operator->()
{
return &_node->_data;
}
2、让迭代器可以移动,前置++运算符的重载(单向迭代器,不支持–操作)
- 如果当前桶的节点没有遍历完,则让节点指针 _node 指向下一个节点
- 如果当前桶的节点遍历完了,则让节点指针 _node 指向下一个不为空的桶的第一个节点
Iter& operator++()
{
if (_node->_next)
{
_node = _node->_next;
}
else
{
size_t index = Hash()(KeyOfT()(_node->_data)) % _ht->_tables.size();
index++;
while (index < _ht->_tables.size())
{
if (_ht->_tables[index])
{
_node = _ht->_tables[index];
return *this;
}
index++;
}
_node = nullptr;
}
return *this;
}
3、让迭代器可以比较,== 和 != 运算符的重载
bool operator==(const Iter& it) const
{
return _node == it._node;
}
bool operator!=(const Iter& it) const
{
return _node != it._node;
}
1.3 定义哈希表的结构
一个数组,数组中存储着各个链表头结点的地址。
注意:
这里有一个私有成员访问的问题,我们在哈希表迭代器中封装了一个「哈希表指针」,当一个桶遍历完成时,用来在哈希表中找下一个桶,但哈希表 _tables 是私有成员,无法访问,所以需要将迭代器类模板声明为友元
template<class K, class T, class Hash, class KeyOfT>
class HashTable
{
typedef HashNode<T> Node;
template<class K, class T, class Hash, class KeyOfT>
friend struct HTIterator;
friend struct HTIterator<K, T, Hash, KeyOfT>;
public:
typedef HTIterator<K, T, Hash, KeyOfT> iterator;
iterator begin();
iterator end();
HashTable() = default;
HashTable(const HashTable& ht);
HashTable& operator=(HashTable ht);
~HashTable();
Node* Find(const K& key);
pair<iterator, bool> Insert(const T& data);
bool Erase(const K& key);
private:
vector<Node*> _tables;
size_t _n = 0;
};
① begin() 和 end() 的实现
注意:this 指针指向当前调用 begin() 成员函数的哈希表对象的
iterator begin()
{
for (size_t i = 0; i < _tables.size(); i++)
{
if (_tables[i])
{
return iterator(_tables[i], this);
}
}
return end();
}
iterator end()
{
return iterator(nullptr, this);
}
② 默认成员函数的实现
1)构造函数的实现
HashTable() = default;
2)拷贝构造函数的实现(深拷贝)
必须是深拷贝,浅拷贝出来会导致两个哈希表中存放的是同一批哈希桶的地址
- 将新哈希表大小调整为
ht._tables.size() 。 - 遍历
ht._tables 的所有哈希桶,将桶里的节点一一「 拷贝 」到新哈希表对应位置上。 - 更改新哈希表中的有效节点个数为
ht._n 。
HashTable(const HashTable& ht)
{
_tables.resize(ht._tables.size());
for (size_t i = 0; i < ht._tables.size(); i++)
{
Node* cur = ht._tables[i];
while (cur)
{
Node* copy = new Node(cur->_data);
copy->_next = _tables[i];
_tables[i] = copy;
cur = cur->_next;
}
}
_n = ht._n;
}
3)赋值运算符重载函数的实现(现代写法)
通过参数间接调用拷贝构造函数,然后将拷贝构造出来的哈希表和当前哈希表的两个成员变量分别进行交换即可,这样当前哈希表就拿到了自己想要的内容,当函数调用结束后,拷贝构造出来的哈希表 ht 出了作用域会被自动析构。
HashTable& operator=(HashTable ht)
{
_tables.swap(ht._tables);
_n = ht._n;
return *this;
}
4)析构函数的实现
~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;
}
_n = 0;
}
1.4 实现哈希表的相关接口
① 查找节点
查找节点接口中,需要获取「不同类型元素」的「关键码 key」,因为元素间的比较是通过 key 值来比较的,以及通过元素关键码 key 取模计算出哈希位置。
所以需要借助两个仿函数类:
- 仿函数类 KeyOfT,获取不同类型的 data 中的关键码 key 值(如果 data 是 key 就取 key,如果是 pair 就取 first)
- 仿函数类 Hash,有些元素关键码 key 的类型不能直接取模,需要将其转换成可以取模的 size_t 类型
Node* Find(const K& key)
{
if (_tables.size() == 0)
{
return nullptr;
}
size_t index = Hash()(key) % _tables.size();
Node* cur = _tables[index];
while (cur)
{
if (key == KeyOfT()(cur->_data))
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
② 插入节点
插入节点接口中,需要获取「不同类型元素」的「关键码 key」,因为元素间的比较是通过 key 值来比较的,以及通过元素关键码 key 取模计算出哈希位置。
所以需要借助两个仿函数类:
- 仿函数类 KeyOfT,获取不同类型的 data 中的关键码 key 值(如果 data 是 key 就取 key,如果是 pair 就取 first)
- 仿函数类 Hash,有些元素关键码 key 的类型不能直接取模,需要将其转换成可以取模的 size_t 类型
函数的返回值为 pair<iterator, bool> 类型对象:
- 目的是为了在 unordered_set 和 unordered_map 中模拟实现 operator[] 运算符重载函数。
插入元素接口功能:插入元素前,会通过该元素的关键码 key 检查该元素是否已在哈希表中(不允许冗余):
- 如果在,返回:pair<指向该元素的迭代器, false>
- 如果不在,先插入节点,再返回:pair<指向该元素的迭代器, true>
pair<iterator, bool> Insert(const T& data)
{
if (_n == _tables.size())
{
size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector<Node*> newTables;
newTables.resize(newSize);
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur != nullptr)
{
Node* next = cur->_next;
size_t index = Hash()(KeyOfT()(cur->_data)) % newSize;
cur->_next = newTables[index];
newTables[index] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t index = Hash()(KeyOfT()(data)) % _tables.size();
Node* cur = _tables[index];
while (cur)
{
if (KeyOfT()(data) == KeyOfT()(cur->_data))
{
return make_pair(iterator(cur, this), false);
}
cur = cur->_next;
}
Node* newNode = new Node(data);
newNode->_next = _tables[index];
_tables[index] = newNode;
_n++;
return make_pair(iterator(newNode, this), true);
}
③ 删除节点
删除节点接口中,需要获取「不同类型元素」的「关键码 key」,因为元素间的比较是通过 key 值来比较的,以及通过元素关键码 key 取模计算出哈希位置。
所以需要借助两个仿函数类:
- 仿函数类 KeyOfT,获取不同类型的 data 中的关键码 key 值(如果 data 是 key 就取 key,如果是 pair 就取 first)
- 仿函数类 Hash,有些元素关键码 key 的类型不能直接取模,需要将其转换成可以取模的 size_t 类型
bool Erase(const K& key)
{
if (_tables.size() == 0)
{
return false;
}
size_t index = Hash()(key) % _tables.size();
Node* cur = _tables[index];
Node* prev = nullptr;
while (cur)
{
if (key == KeyOfT()(cur->_data))
{
if (cur == _tables[index])
{
_tables[index] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
cur = nullptr;
_n--;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
1.5 针对除留余数法的取模问题
实现哈希表,计算元素关键码对应哈希位置的哈希函数采用的是「除留余数法」,需要传「仿函数」将不能取模的类型转换成可以取模的 size_t 类型。
这里给出针对普通类型取模和 string 类型取模的仿函数,放到全局域中,方便模拟实现 unordered_set 和 unordered_map 时使用。
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return key;
}
};
template<>
struct HashFunc<string>
{
size_t operator()(const string& key)
{
size_t hash_key = 0;
for (size_t i = 0; i < key.size(); i++)
{
hash_key *= 131;
hash_key += key[i];
}
return hash_key;
}
};
1.6 unordered_set 的模拟实现
在 unordered_set 中直接封装一张哈希表,然后将哈希表的迭代器和相关接口包装下即可。
namespace winter
{
template<class K, class Hash = HashFunc<K>>
class unordered_set
{
struct KeyOfSet
{
const K& operator()(const K& key) const
{
return key;
}
};
public:
typedef typename hash_bucket::HashTable<K, K, Hash, KeyOfSet>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
pair<iterator, bool> insert(const K& key)
{
return _ht.Insert(key);
}
private:
hash_bucket::HashTable<K, K, Hash, KeyOfSet> _ht;
};
}
测试:
void test_unordered_set()
{
unordered_set<int> s;
s.insert(4);
s.insert(3);
s.insert(2);
s.insert(8);
s.insert(9);
s.insert(1);
s.insert(6);
unordered_set<int>::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
1.7 unordered_map 的模拟实现
在 unordered_map 中直接封装一张哈希表,然后将哈希表的迭代器和相关接口包装下即可。
namespace winter
{
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
struct KeyOfMap
{
const K& operator()(const pair<const K, V>& kv) const
{
return kv.first;
}
};
public:
typedef typename hash_bucket::HashTable<K, pair<const K, V>, Hash, KeyOfMap>::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)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return (ret.first)->second;
}
private:
hash_bucket::HashTable<K, pair<const K, V>, Hash, KeyOfMap> _ht;
};
}
测试:
void test_unordered_map()
{
unordered_map<string, string> m;
m.insert(make_pair("tree", "树"));
m.insert(make_pair("sort", "排序"));
m.insert(make_pair("map", "地图"));
m["boy"] = "男孩";
m["map"] += ",映射";
unordered_map<string, string> copy(m);
unordered_map<string, string>::iterator it = m.begin();
while (it != m.end())
{
cout << it->first << ":" << it->second << endl;;
++it;
}
cout << endl;
}
|