用同一个哈希表实现unordered_map和unordered_set(C++实现)【STL】
1. 模板参数控制
我们知道,unordered_set和unordered_map与set和map是一样的,前者不是真正的键值对,它的value值和key值相同;后者是真正的键值对。STL非常注重代码的复用,它们在底层使用了同一棵红黑树模板实现,这也是此文要用同一个哈希表实现unordered_set和unordered_map的原因。如果各自拥有一个哈希表,set和unordered_set只要一个key值即可。
1.1 容器模板参数
unordered_set
template<class K>
class unordered_set
{
public:
// ..
private:
HashTable<K, K> _ht;
};
unordered_map
template<class K, class V>
class unordered_map
{
public:
//...
private:
HashTable<K, pair<K, V>> _ht;
};
1.2 结点类的定义
同一个哈希表实现unordered_set和unordered_map,就需要控制底层的哈希表的模板参数。由于原先实现哈希表时默认是以<key,value>作为键值对的,而哈希表在底层是不知道上层是unordered_set还是unordered_map,所以为了区分两者键值对的第二个模板参数,将哈希表中的第二个模板参数从V
改成T
:
- 当上层是unordered_set时,T和K相同;
- 当上层是unordered_map时,T就是value。
同时,原先的键值对也要改成模板参数T,由于这个T可能是key,也有可能是<key,value>键值对,所以将原来结点类中的_kv
(键值对)改成_data
,以保存数据。
template<class T>
struct HashNode
{
T _data; // 保存数据
HashNode<T>* _next; // 后继指针
HashNode(const T& data) // 结点构造函数
:_data(data)
, _next(nullptr)
{}
};
1.3 仿函数获取键值
上面的操作将unordered_set和unordered_map的键值对是用一个T类型的_data
保存的,由于哈希函数要根据键值key计算,所以要取出_data
中的key值。在这里可以使用一个仿函数获取,仿函数其实就是一个结构体中重载的operator()
。
template<class K, class V>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
// ...
private:
HashTable<K, pair<K, V>, MapKeyOfT> _ht;
};
template<class K>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
// ...
private:
HashTable<K, K, SetKeyOfT> _ht;
};
因此,在哈希表中的参数列表要新增一个仿函数:
template<class K, class T, class KeyOfT>
class HashTable
{
// ...
};
2. 字符串哈希函数
在使用map和set时,大多数情况key值都是字符串类型,unordered_set和unordered_map也是一样的,但是string虽然是常用的类型,但是它无法直接取模,这也是哈希常见的问题。为此,有许多大佬研究了这个问题。
Kernighan和Dennis在《The C programming language》中提出的BKDRHash 算法,是一个高效且实用的哈希函数,它对每次累加后的结果再乘以素数131。
至于为什么是131,这是一个数学问题,但实际上也有其他类似的哈希算法,只是将131改成了其他数字,大多数是素数。
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
// 模板的特化
template<>
struct Hash<string>
{
size_t operator()(const string& s)
{
size_t value = 0;
for (auto ch : s)
{
value = value * 131 + ch;
}
return value;
}
};
这种专门针对某种特定类型而写的模板,是模板的特化。
注意:
模板的特化必须在原有模板的基础上特化的。
所以哈希表的模板参数还要增加一个:
template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
class HashTable
{
// ...
};
3. 哈希表默认成员函数
3.1 默认构造函数
当实例化一个哈希表对象时,哈希表的两个成员会被这样初始化:
- _table调用vector的默认构造函数初始化;
- _size被默认值0初始化。
vector<Node*> _table; // 哈希表
size_t _size = 0; // 有效元素个数
但是拷贝构造函数和默认构造函数是二选一的,所以需要使用 default 关键字显示指定生成默认构造函数。
//构造函数
HashTable() = default; //显示指定生成默认构造函数
3.2 拷贝构造函数
深拷贝实现拷贝构造函数,新哈希表和旧哈希表中的结点的地址都是相同的。
步骤:
- 将新哈希表的大小risize到旧哈希表大小;
- 将旧哈希表的每个桶的结点全部拷贝到新的哈希表中;
- 更新新哈希表的
_size
。
//拷贝构造函数
HashTable(const HashTable& ht)
{
_table.resize(ht._table.size());
for (size_t i = 0; i < ht._table.size(); i++)
{
if (ht._table[i])
{
Node* cur = ht._table[i];
while (cur)
{
Node* copy = new Node(cur->_data);
copy->_next = _table[i];
_table[i] = copy;
cur = cur->_next;
}
}
}
_size = ht._n;
}
3.3 赋值运算符重载函数
赋值运算符重载函数的本质就是拷贝构造函数:
在参数部分构造一个哈希表,然后将新旧哈希表的地址和有效数据_size交换,为了=
能连续赋值,最后返回当前对象的this指针。这里巧妙使用了参数部分构造新对象和函数结束时调用对应的析构函数。
//赋值运算符重载函数
HashTable& operator=(HashTable ht) // 这里调用了构造函数
{
_table.swap(ht._table);
swap(_size, ht._size); // 交换数据
return *this;
} // 调用析构函数
3.4 析构函数
由于每个哈希桶中的结点都是new出来的,所以要遍历哈希表,将每个哈希桶中的所有结点delete。
// 析构函数
~HashTable()
{
for (size_t i = 0; i < _table.size(); i++)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
free(cur);
cur = next;
}
_table[i] = nullptr;
}
}
4. 正向迭代器
由于上层容器有++和–等操作,所以要先在哈希表中增加迭代器,以供上层封装。STL中实现了双向迭代器,但这里只实现了正向迭代器。
4.1 定义迭代器结构体
迭代器实际上就是对结点的封装,它本质上是结点的地址,是指针。由于不同的运算符重载函数的返回值可能不同,类型有引用、指针和迭代器本身,所以在迭代器内部typedef一下。
// 前置声明
template<class K, class T, class KeyOfT, class Hash>
class HashTable;
//正向迭代器
template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
struct __HTIterator
{
typedef HashNode<T> Node; // 哈希结点类型
typedef HashTable<K, T, KeyOfT, Hash> HT; // 哈希表类型
typedef __HTIterator<K, T, KeyOfT, Hash> Self; // 正向迭代器类型
Node* _node; // 结点指针
HT* _pht; // 哈希表的地址
// ...
};
注意
这里将哈希表的结构体和迭代器的结构体放在了同一个头文件中,其原因是模板不支持分离编译,为了结构清晰,将迭代器放在哈希表之前,而迭代器也需要使用到哈希表的定义,所以需要前置声明,就像声明函数一样。
4.2 构造函数
由于需要++
操作,所以必须要保存哈希表的地址(稍后就会知道)。
// 构造函数
__HTIterator(Node* node, HT*, pht)
:_node(node)
, _pht(pht)
{}
4.3 operator*()和operator->()
对结点进行*
解引用操作,就是取出结点中保存的value值。
->
成员访问操作符就是取出结点中数据的地址。
因此需要区分它们的返回值。
T& operator*()
{
return _node->_data;
}
T* operator->()
{
return &_node_ -> _data;
}
4.4 operator==()和operator!=()
判断结点是否相等,只需要判断结点的地址是否相同即可。
bool operator!=(const Self& s) const
{
return _node != s._node;
}
bool operator==(const Self& s) const
{
return _node == s._node;
}
4.5 operator++()
运算符++
的重载,可以分为两种情况:
- 如果当前在桶中:找链表的下一个结点;
- 如果当前在桶的最后一个元素:找下一个非空的桶:
- 通过哈希函数计算当前桶的下标值;
- 然后对算出来的下标值++,得到下一个桶的位置;
- 如果下一个桶为空,则重复上述操作;
- 找到非空桶,更新当前结点的值;
- 走到最后一个位置也没找到非空桶,返回nullptr。
// 前置++
Self& operator++()
{
if (_node->_next) // 当前结点不是哈希桶的最后一个结点
{
_node = _node->_next; // 迭代
}
else // 当前结点是哈希桶的最后一个结点
{
KeyOfT kot;
Hash hash;
size_t index = hash(kot(_node->_data)) % _pht->_table.size(); // 得到当前哈希桶的下标
index++; // 下标+1
while (index < _pht->_table.size()) // 遍历哈希表中所有哈希桶
{
if (_pht->_table[index]) // 当前哈希桶不为空
{
_node = _pht->_table[index];
return *this; // 更新结点指针并返回
}
index++; // 当前哈希表为空,往后找
}
_node = nullptr; // 上一个是空桶
}
return *this;
}
5. 哈希表正向迭代器
5.1 定义迭代器
写好正向迭代器以后,可以在哈希表的内部封装迭代器了。在此之前,需要在哈希表的内部进行以下操作:
-
为了能方便地像使用STL中的iterator类型一样,将带有模板参数的迭代器typedef为iterator
注意:为了方便哈希表外部使用iterator类型,要在哈希表的public区域定义。
-
除此之外,由于正向迭代器中
++
操作符重载函数使用了哈希表中的_table
,而它是哈希表类的私有成员,所以要将正向迭代器结构体作为哈希表类的友元。 -
将哈希表类中的Find函数的返回值类型改为由结点指针和哈希表地址构造的正向迭代器。将Insert函数的返回值类型改成由正向迭代器和布尔类型构成的键值对。
template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
class HashTable
{
typedef HashNode<T> Node;
// 将正向迭代器类声明为哈希表类的友元
template<class K, class T, class KeyOfT, class Hash>
friend struct __HTIterator;
public:
typedef __HTIterator<K, T, KeyOfT, Hash> iterator; // 定义迭代器
iterator begin()
{
size_t i = 0;
while (i < _table.size()) // 遍历哈希表中所有哈希桶
{
if (_table[i]) // 当前哈希桶不为空
{
return iterator(_table[i], this); // 返回桶地址和表地址构造的迭代器
}
i++; // 下一个桶
}
return end();
}
iterator end()
{
return iterator(nullptr, this); // 返回nullptr和表地址构造的迭代器
}
// ...
private:
vector<Node*> _table; // 哈希表
size_t _size = 0; // 有效元素个数
};
6. 实现unordered_set和unordered_map
实现unordered_set和unordered_map,只要调用哈希表开放的接口即可。
6.1 unordered_set
template<class K, class Hash = HashFunc<K>>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename HashTable<K, K, SetKeyOfT>::iterator iterator; // 定义迭代器
iterator begin()
{
return _ht.begin();
}
iterator end()
{
return _ht.end();
}
// 插入函数
pair<iterator, bool> insert(const K& key)
{
return _ht.Insert(key);
}
// 删除函数
void erase(const K& key)
{
_ht.Erase(key);
}
// 查找函数
iterator find(const K& key)
{
return _ht.Find(key);
}
private:
HashTable<K, K, SetKeyOfT> _ht;
};
6.2 unordered_map
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename HashTable<K, pair<K, V>, MapKeyOfT>::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);
}
//删除函数
void erase(const K& key)
{
_ht.Erase(key);
}
//查找函数
iterator find(const K& key)
{
return _ht.Find(key);
}
//赋值运算符重载
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
iterator it = ret.first;
return it->second;
}
private:
HashTable<K, pair<K, V>, MapKeyOfT> _ht;
};