hashtable(散列表)是一种数据结构,在元素的插入,删除,搜索操作上具有常数平均时间复杂度O(1);
hashtable名词
散列函数:负责将某一元素映射为索引。
碰撞(collision):不同的元素被映射到相同的位置。
解决碰撞的方法:线性试探法,二次试探法,开链等。
负载系数:元素个数除以表格大小。
主集团:平均插入成本的增长幅度,远高于负载系数的成长幅度。
次集团:hashtable若采用二次探测,则若两个元素经hash function计算出来的位置相同,则插入时所试探的位置也相同,造成某种浪费。
开链:在每个表格元素中位置一个list。
桶(bucket):hashtable表格内的每个元素。
hashtable组织方式
节点类
template <class _Val>
struct _Hashtable_node
{
_Hashtable_node* _M_next;
_Val _M_val;
};
hashtable的节点和list节点相类似。
一个Node的指针指向每个桶上列表的首个元素,而桶上的链表首地址存放在vector向量中。
typedef _Hashtable_node<_Val> _Node;
vector<_Node*,_Alloc> _M_buckets;
hashtable迭代器的自增操作符定义如下:
template <class _Val, class _Key, class _HF, class _ExK, class _EqK,
class _All>
_Hashtable_iterator<_Val,_Key,_HF,_ExK,_EqK,_All>&
_Hashtable_iterator<_Val,_Key,_HF,_ExK,_EqK,_All>::operator++()
{
const _Node* __old = _M_cur; //当前节点的副本
_M_cur = _M_cur->_M_next; //前进一个节点
if (!_M_cur) { //到达当前链表的末端,则转向下一个桶的链表开头
//获得节点副本所在的桶编号
size_type __bucket = _M_ht->_M_bkt_num(__old->_M_val);
//当桶为空,且未到达桶向量的末尾,则继续下一个桶
while (!_M_cur && ++__bucket < _M_ht->_M_buckets.size())
//当前节点指针指向下一个桶的节点链表开头
_M_cur = _M_ht->_M_buckets[__bucket];
}
return *this;
}
hashtable类定义有五个模板参数
template <class _Val, class _Key, class _HashFcn,
class _ExtractKey, class _EqualKey, class _Alloc>
class hashtable;
_Val 值类型
_Key 键类型
_HashFcn 哈希函数
_ExtractKey 提取键的方法
_EqualKey 判断键相等的方法
_Alloc 分配器类型
hashtable插入操作
insert_unique不允许值重复的插入:
pair<iterator, bool> insert_unique(const value_type& __obj)
{
resize(_M_num_elements + 1);
return insert_unique_noresize(__obj);
}
该函数首先调用resize函数,传入当前元素加一的值,看是否需要进行扩容。处理完成后,再将值__obj插入到hashtable中。
resize函数的定义如下:
template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All>
void hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>
::resize(size_type __num_elements_hint)
{
const size_type __old_n = _M_buckets.size(); //桶数量的旧值
if (__num_elements_hint > __old_n) { //传入的新值大于旧值
//从素数表中查找最接近新值的桶数量__n
const size_type __n = _M_next_size(__num_elements_hint);
if (__n > __old_n) { //得到的桶数量大于旧值:需要扩容
//分配容量为__n(新桶数量)的桶向量
vector<_Node*, _All> __tmp(__n, (_Node*)(0),
_M_buckets.get_allocator());
__STL_TRY {
//对旧hashtable中的每个桶
for (size_type __bucket = 0; __bucket < __old_n; ++__bucket) {
_Node* __first = _M_buckets[__bucket]; //取得桶中链表首地址
while (__first) { //未到达旧桶中链表的末尾
//根据链表节点中存放的元素数值_M_val,以新的桶数量__n,
//计算其在新的hashtable中的桶编号(rehash)
size_type __new_bucket = _M_bkt_num(__first->_M_val, __n);
//更新当前桶中节点链表头的指向,使其指向链表中的下一个元素
_M_buckets[__bucket] = __first->_M_next;
//以下两句是增长新桶中的链表,把新节点加进去。
//链表首地址的后继指向新hashtable中计算出来的新桶中的链表开头
__first->_M_next = __tmp[__new_bucket];
//新hashtable,新桶的链表开头指向__first
__tmp[__new_bucket] = __first;
//更新__first至原有的__first->_M_next,即在原hashtable原桶中的链表中前进一个元素。
__first = _M_buckets[__bucket];
}
}
//当旧hashtable中的元素都重新hash到新桶向量后
_M_buckets.swap(__tmp); //将新桶向量与旧桶向量相互交换。
//旧桶数据存放在__tmp向量中,当离开此范围时,__tmp作为一个局部变量,其空间会被自动释放。
}
//发生异常后进行的回滚操作
# ifdef __STL_USE_EXCEPTIONS
catch(...) {
//将新的桶向量中的每个桶
for (size_type __bucket = 0; __bucket < __tmp.size(); ++__bucket) {
//对桶中链表的每个节点
while (__tmp[__bucket]) {
_Node* __next = __tmp[__bucket]->_M_next;
_M_delete_node(__tmp[__bucket]); //删除当前节点
__tmp[__bucket] = __next; //前进一个节点
}
}
throw;
}
# endif /* __STL_USE_EXCEPTIONS */
}
}
}
insert_unique_noresize的函数定义如下:
template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All>
pair<typename hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>::iterator, bool>
hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>
::insert_unique_noresize(const value_type& __obj)
{
const size_type __n = _M_bkt_num(__obj); //将值映射到对应桶中,并获得桶编号
_Node* __first = _M_buckets[__n]; //桶中链表的开头
//桶中链表非空,则遍历链表
for (_Node* __cur = __first; __cur; __cur = __cur->_M_next)
//链表中存在与__obj值相同的节点
if (_M_equals(_M_get_key(__cur->_M_val), _M_get_key(__obj)))
//返回相等位置处的迭代器,并附加插入失败标识。
return pair<iterator, bool>(iterator(__cur, this), false);
//链表中不存在与待插入值相等的元素,
_Node* __tmp = _M_new_node(__obj); //创建新链表节点
__tmp->_M_next = __first; //将新节点的后继设为链表开头
_M_buckets[__n] = __tmp; //将桶的链表开头设为新插入的节点
++_M_num_elements; //增加hashtable元素数量
//返回插入位置的迭代器,并附加插入成功标识。
return pair<iterator, bool>(iterator(__tmp, this), true);
}
结合insert_unique函数中调用的两个函数接口的分析,可知:
- 在进行insert_unique函数执行时,首先尝试对桶向量进行扩容,然后再向hashtable中插入元素。
- hashtable的扩容操作类似于vector的自增长过程,都经过1.申请更大空间;2.将原容器中的数据转移到新的容器中;3.清理原容器的空间;三部曲。
- 与vector容器的自增长所不同的是,在数据转移的过程中,hashtable需要根据新的桶数量,对数据进行重新映射。
insert_equal允许重复的插入:
iterator insert_equal(const value_type& __obj)
{
resize(_M_num_elements + 1);
return insert_equal_noresize(__obj);
}
与insert_unique相比较,函数在扩容后调用允许重复值的非扩充插入函数insert_equal_noresize:
template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All>
typename hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>::iterator
hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>
::insert_equal_noresize(const value_type& __obj)
{
const size_type __n = _M_bkt_num(__obj); //将值映射到对应桶中,并获得桶编号
_Node* __first = _M_buckets[__n]; //桶中链表的开头
///桶中链表非空,则遍历链表
for (_Node* __cur = __first; __cur; __cur = __cur->_M_next)
//链表中存在与__obj值相同的节点,则执行插入操作并返回
if (_M_equals(_M_get_key(__cur->_M_val), _M_get_key(__obj))) {
_Node* __tmp = _M_new_node(__obj); //创建新链表节点
__tmp->_M_next = __cur->_M_next; //将插入节点的后继设为当前节点的后继
__cur->_M_next = __tmp; //当前节点的后继设为插入节点
++_M_num_elements; //更新元素个数
return iterator(__tmp, this); //完成插入操作,返回
}
//链表中没有与插入元素值相同的节点
_Node* __tmp = _M_new_node(__obj); //创建新链表节点
__tmp->_M_next = __first; //将新节点的后继指向链表开头
_M_buckets[__n] = __tmp; //将桶的链表开头指向新插入的节点
++_M_num_elements; //更新元素个数
return iterator(__tmp, this); //完成插入操作,返回
}
insert_unique_noresize与insert_equal_noresize的比较:
- insert_unique_noresize先在hashtable的某个桶中查找与插入值相同的节点,若找到,则直接返回,插入失败。否则,在桶的链表的头部插入新节点。
- insert_equal_noresize先在hashtable的某个桶中查找与插入值相同的节点,若找到,则在其后插入新节点。否则,在桶的链表的头部插入新节点。
hashtable的删除和复制
hashtable删除
template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All>
void hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>::clear()
{
//遍历桶向量
for (size_type __i = 0; __i < _M_buckets.size(); ++__i) {
_Node* __cur = _M_buckets[__i]; //当前桶
while (__cur != 0) { //遍历桶中链表
_Node* __next = __cur->_M_next; //保存后继节点地址
_M_delete_node(__cur); //析构指针所指向的对象,并释放节点
__cur = __next; //指针前进
}
_M_buckets[__i] = 0; //链表开头指向空
}
_M_num_elements = 0; //更新元素数量为0
}
hashtable复制
template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All>
void hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>
::_M_copy_from(const hashtable& __ht)
{
_M_buckets.clear(); //清空原桶向量
_M_buckets.reserve(__ht._M_buckets.size()); //为桶向量重新分配内存,是其能够容纳被复制对象
//初始化桶向量中的列表开头为空
_M_buckets.insert(_M_buckets.end(), __ht._M_buckets.size(), (_Node*) 0);
__STL_TRY {
//遍历桶
for (size_type __i = 0; __i < __ht._M_buckets.size(); ++__i) {
const _Node* __cur = __ht._M_buckets[__i]; //被复制散列(源散列)的某个桶中链表开头
if (__cur) { //链表非空
_Node* __copy = _M_new_node(__cur->_M_val); //创建链表开头节点副本
_M_buckets[__i] = __copy; //更新目标散列对应桶的链表开头
for (_Node* __next = __cur->_M_next; //对链表中剩下的元素,依次
__next;
__cur = __next, __next = __cur->_M_next) {
__copy->_M_next = _M_new_node(__next->_M_val); //创建节点副本
__copy = __copy->_M_next; //在列表中前进
}
}
}
_M_num_elements = __ht._M_num_elements; //更新元素数量
}
__STL_UNWIND(clear()); //复制发生异常时回滚操作
}
小结
- hashtable不依赖元素的随机性,假设元素分布在一个相对固定的范围内,类似于一种字典结构。
- hashtable可提供常数复杂度的插入,删除,搜索操作,但是需要创建哈希表,是以空间的代价换取时间的高效。
- 树结构提供对数时间复杂度的元素搜索操作,依赖于元素的随机性。