前言:
前面我们学习了unordered_map和unordered_set容器,比较了他们和map、set的查找效率,我们发现他们的效率比map、set高,进而我们研究他们的底层是由哈希实现。哈希是一种直接映射的方式,所以查找的效率很快。与学习红黑树和map、set的思路一样,我们现在学完了unordered_map和unordered_set,本章将模拟实现底层结构来封装该容器!
作者建议在阅读本章前,可以先去看一下前面的红黑树封装map和set——红黑树封装map和set
这两篇文章都重在强调泛型编程的思想,上一篇由于是初认识,作者讲解的会更详细一点~
目录
2、operator*和operator->及operator!=和operator==的模拟实现
(四)封装unordered_map和unordered_set
(一)如何复用一个哈希桶
我们学习过知道,unordered_map和unordered_set容器存放的结点并不一样,为了让它得到复用,我们就需要对哈希桶进行改造,将哈希桶改造的更加泛型一点,既符合Key模型,也符合Key_Value模型。
1、结点的定义:
所以我们这里还是和封装map和set
时一样,无论是Key
还是Key_Value
,都用一个类型T来接收,这里高维度的泛型哈希表中,实现还是用的是Kye_Value
模型,K是不能省略的,同样的查找和删除
要用,故我们可以引出两个容器各自模板参数类型。
2、两个容器各自的模板参数类型
如何取到想要的数据:
- 我们给每个容器配一个仿函数
- 各传不同的仿函数,拿到想要的不同的数据
同时我们再给每个容器配一个哈希函数。
3、改造哈希桶
通过上面1和2,我们可以把各自存放的数据泛化成data:
这样我们哈希桶的模板参数算是完成了
- 哈希函数我们可以自由选择并传
- 仿函数在各自容器的封装中实现,用于比较时我们可以取出各自容器想要的数据
我们把上一篇文章封装的哈希桶拿来改造:
//K --> 键值Key,T --> 数据
//unordered_map ->HashTable<K, pair<K, V>, MapKeyOfT> _ht;
//unordered_set ->HashTable<K, K, SetKeyOfT> _ht;
template<class K, class T, class KeyOfT, class HashFunc>
class HashTable
{
template<class K, class T, class KeyOfT, class HashFunc>
friend class __HTIterator;
typedef HashNode<T> Node;
public:
typedef __HTIterator<K, T, KeyOfT, HashFunc> iterator;
iterator begin()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
if (cur)
{
return iterator(cur, this);
}
}
return end();
}
iterator end()
{
return iterator(nullptr, this);
}
~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;
}
}
size_t GetNextPrime(size_t prime)
{
const int PRIMECOUNT = 28;
static const size_t primeList[PRIMECOUNT] =
{
53, 97, 193, 389, 769,
1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433,
1572869, 3145739, 6291469, 12582917, 25165843,
50331653, 100663319, 201326611, 402653189, 805306457,
1610612741, 3221225473, 4294967291
};
//获取比prime大那一个素数
size_t i = 0;
for (i = 0; i < PRIMECOUNT; i++)
{
if (primeList[i] > prime)
return primeList[i];
}
return primeList[i];
}
pair<iterator, bool> Insert(const T& data)
{
HashFunc hf;
KeyOfT kot;
iterator pos = Find(kot(data));
if (pos != end())
{
return make_pair(pos, false);
}
//负载因子 == 1 扩容 -- 平均每个桶挂一个结点
if (_tables.size() == _n)
{
//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
size_t newSize = GetNextPrime(_tables.size());
if (newSize != _tables.size())
{
vector<Node*> newTable;
newTable.resize(newSize, nullptr);
//遍历旧表
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
//再对每个桶挨个遍历
while (cur)
{
Node* next = cur->_next;
size_t hashi = hf(kot(cur->_data)) % newSize;
//转移到新的表中
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
//将原表置空
_tables[i] = nullptr;
}
newTable.swap(_tables);
}
}
size_t hashi = hf(kot(data));
hashi %= _tables.size();
//头插到对应的桶即可
Node* newnode = new Node(data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
//有效数据加一
_n++;
return make_pair(iterator(newnode, this), true);
}
iterator Find(const K& key)
{
if (_tables.size() == 0)
{
return iterator(nullptr, this);
}
KeyOfT kot;
HashFunc hf;
size_t hashi = hf(key);
//size_t hashi = HashFunc()(key);
hashi %= _tables.size();
Node* cur = _tables[hashi];
//找到指定的桶之后,顺着单链表挨个找
while (cur)
{
if (kot(cur->_data) == key)
{
return iterator(cur, this);
}
cur = cur->_next;
}
//没找到返回空
return iterator(nullptr, this);
}
bool Erase(const K& key)
{
if (_tables.size() == 0)
{
return false;
}
HashFunc hf;
KeyOfT kot;
size_t hashi = hf(key);
hashi %= _tables.size();
//单链表删除结点
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
//头删
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
private:
//指针数组
vector<Node*> _tables;
size_t _n = 0;
};
主要改造的地方就是上述所注意的地方:
- 比较时需要调用各自的仿函数
- 调用外部传的哈希函数
还有对扩容的二次思考:
研究表明:除留余数法,最好模一个素数
- 通过查STL官方库我们也发现,其提供了一个取素数的函数
- 所以我们也提供了一个,直接拷贝过来
-
- 这样我们在扩容时就可以每次给素数个桶
-
- 在扩容时加了一条判断语句是为了防止素数值太大,过分扩容容易直接把空间(堆)干崩了
(二)哈希桶的迭代器的模拟实现
1、begin()和end()的模拟实现
- 以第一个桶中第一个不为空的结点为整个哈希桶的开始结点
- 以空结点为哈希桶的结束结点
2、operator*和operator->及operator!=和operator==的模拟实现
这两组和之前实现的一模一样,大家自行理解。
3、operator ++的模拟实现
注:
- 这里要在哈希桶的类外面访问其私有成员
- 我们要搞一个友元类
- 迭代器类是哈希桶类的朋友
- 这样就可以访问了
思路:
- 判断一个桶中的数据是否遍历完
- 如果所在的桶没有遍历完,在该桶中返回下一个结点指针
- 如果所在的桶遍历完了,进入下一个桶
- 判断下一个桶是否为空
- 非空返回桶中第一个节点
- 空的话就遍历一个桶
- 后置++和之前一眼老套路,不赘述
注意:
unordered_map和unordered_set是不支持反向迭代器的,从底层结构我们也能很好的理解(单链表找不了前驱)所以不支持实现迭代器的operator- -
最后注意一点,我们需要知道哈希桶大小,所以不仅要传结点地址,还要传一个哈希桶,这样才能知道其大小,除此,由于哈希桶改造在后面,所以我们要在前面声明一下:
(三)迭代器和改造哈希桶的总代码
#include<vector>
#include<string>
#include<iostream>
using namespace std;
template<class K>
struct DefaultHash
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct DefaultHash<string>
{
size_t operator()(const string& key)
{
//BKDR
size_t hash = 0;
for (auto ch : key)
{
hash = hash * 131 + ch;
}
return hash;
}
};
namespace Bucket
{
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 HashFunc>
class HashTable;
//哈希桶的迭代器
template<class K, class T, class KeyOfT, class HashFunc>
class __HTIterator
{
typedef HashNode<T> Node;
typedef __HTIterator<K, T, KeyOfT, HashFunc> Self;
public:
Node* _node;
__HTIterator() {};
//编译器的原则是向上查找(定义必须在前面,否则必须先声明)
HashTable<K, T, KeyOfT, HashFunc>* _pht;
__HTIterator(Node* node, HashTable<K, T, KeyOfT, HashFunc>* pht)
:_node(node)
, _pht(pht)
{}
Self& operator++()
{
if (_node->_next)
{
_node = _node->_next;
}
else//当前桶已经走完了,要走下一个桶
{
KeyOfT kot;
HashFunc hf;
size_t hashi = hf(kot(_node->_data)) % _pht->_tables.size();
hashi++;
//找下一个不为空的桶 -- 访问到了哈希表中私有的成员(友元)
for (; hashi < _pht->_tables.size(); hashi++)
{
if (_pht->_tables[hashi])
{
_node = _pht->_tables[hashi];
break;
}
}
//没有找到不为空的桶,用nullptr去做end标识
if (hashi == _pht->_tables.size())
{
_node = nullptr;
}
}
return *this;
}
T& operator*()
{
return _node->_data;
}
T* operator->()
{
return &_node->_data;
}
bool operator!=(const Self& s) const
{
return _node != s._node;
}
bool operator==(const Self& s) const
{
return _node == s._node;
}
};
//K --> 键值Key,T --> 数据
//unordered_map ->HashTable<K, pair<K, V>, MapKeyOfT> _ht;
//unordered_set ->HashTable<K, K, SetKeyOfT> _ht;
template<class K, class T, class KeyOfT, class HashFunc>
class HashTable
{
template<class K, class T, class KeyOfT, class HashFunc>
friend class __HTIterator;
typedef HashNode<T> Node;
public:
typedef __HTIterator<K, T, KeyOfT, HashFunc> iterator;
iterator begin()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
if (cur)
{
return iterator(cur, this);
}
}
return end();
}
iterator end()
{
return iterator(nullptr, this);
}
~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;
}
}
size_t GetNextPrime(size_t prime)
{
const int PRIMECOUNT = 28;
static const size_t primeList[PRIMECOUNT] =
{
53, 97, 193, 389, 769,
1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433,
1572869, 3145739, 6291469, 12582917, 25165843,
50331653, 100663319, 201326611, 402653189, 805306457,
1610612741, 3221225473, 4294967291
};
//获取比prime大那一个素数
size_t i = 0;
for (i = 0; i < PRIMECOUNT; i++)
{
if (primeList[i] > prime)
return primeList[i];
}
return primeList[i];
}
pair<iterator, bool> Insert(const T& data)
{
HashFunc hf;
KeyOfT kot;
iterator pos = Find(kot(data));
if (pos != end())
{
return make_pair(pos, false);
}
//负载因子 == 1 扩容 -- 平均每个桶挂一个结点
if (_tables.size() == _n)
{
//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
size_t newSize = GetNextPrime(_tables.size());
if (newSize != _tables.size())
{
vector<Node*> newTable;
newTable.resize(newSize, nullptr);
//遍历旧表
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
//再对每个桶挨个遍历
while (cur)
{
Node* next = cur->_next;
size_t hashi = hf(kot(cur->_data)) % newSize;
//转移到新的表中
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
//将原表置空
_tables[i] = nullptr;
}
newTable.swap(_tables);
}
}
size_t hashi = hf(kot(data));
hashi %= _tables.size();
//头插到对应的桶即可
Node* newnode = new Node(data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
//有效数据加一
_n++;
return make_pair(iterator(newnode, this), true);
}
iterator Find(const K& key)
{
if (_tables.size() == 0)
{
return iterator(nullptr, this);
}
KeyOfT kot;
HashFunc hf;
size_t hashi = hf(key);
//size_t hashi = HashFunc()(key);
hashi %= _tables.size();
Node* cur = _tables[hashi];
//找到指定的桶之后,顺着单链表挨个找
while (cur)
{
if (kot(cur->_data) == key)
{
return iterator(cur, this);
}
cur = cur->_next;
}
//没找到返回空
return iterator(nullptr, this);
}
bool Erase(const K& key)
{
if (_tables.size() == 0)
{
return false;
}
HashFunc hf;
KeyOfT kot;
size_t hashi = hf(key);
hashi %= _tables.size();
//单链表删除结点
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
//头删
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
private:
//指针数组
vector<Node*> _tables;
size_t _n = 0;
};
}
(四)封装unordered_map和unordered_set
有了上面的哈希桶的改装,我们这里的对map和set的封装就显得很得心应手了。
unordered_map的封装:
#include "HashTable.h"
namespace zc
{
template<class K, class V, class HashFunc = DefaultHash<K>>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename Bucket::HashTable<K, pair<K, V>, MapKeyOfT, HashFunc>::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);
}
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> ret = insert(make_pair(key, V()));
return ret.first->second;
}
private:
Bucket::HashTable<K, pair<K, V>, MapKeyOfT, HashFunc> _ht;
};
void test_map()
{
unordered_map<string, string> dict;
dict.insert(make_pair("sort", "排序"));
dict.insert(make_pair("left", "左边"));
dict.insert(make_pair("left", "下面"));
dict["string"];
dict["left"] = "上面";
dict["string"] = "字符串";
unordered_map<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
cout << it->first << " " << it->second << endl;
++it;
}
cout << endl;
for (auto e : dict)
{
cout << e.first << " " << e.second << endl;
}
}
}
这里unordered_map中的operator[ ]我们知道其原理之后,模拟实现就非常方便,直接调用插入函数,控制好参数和返回值即可。
对unordered_set的封装:
#include "HashTable.h"
#include "HashTable.h"
namespace zc
{
template<class K, class HashFunc = DefaultHash<K>>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
//2.48
typedef typename Bucket::HashTable<K, K, SetKeyOfT, HashFunc>::iterator iterator;
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
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);
}
private:
Bucket::HashTable<K, K, SetKeyOfT, HashFunc> _ht;
};
struct Date
{
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
bool operator==(const Date& d) const
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
int _year;
int _month;
int _day;
};
struct DateHash
{
size_t operator()(const Date& d)
{
//return d._year + d._month + d._day;
size_t hash = 0;
hash += d._year;
hash *= 131;
hash += d._month;
hash *= 1313;
hash += d._day;
//cout << hash << endl;
return hash;
}
};
void test_set()
{
unordered_set<int> s;
//set<int> s;
s.insert(2);
s.insert(3);
s.insert(1);
s.insert(2);
s.insert(5);
s.insert(12);
unordered_set<int>::iterator it = s.begin();
//auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
unordered_set<Date, DateHash> sd;
sd.insert(Date(2022, 3, 4));
sd.insert(Date(2022, 4, 3));
}
}
最后大家可以利用代码中给的测试函数进行测试!
感谢你的阅读!