hashtable的节点和桶子
1.hashtable使用分离链接法来解决散列冲突。表的结构是vector,每个元素时一个桶,桶里放的是一个链表。
2.bucket所维护的链表,并不使用标准库的list和slist结构,而是新定义了一个数据结构:
template <class Value>
struct __hashtable_node
{
__hashtable_node* next;
Value val;
}
hashtable的迭代器
1.迭代器类型是forward_iterator(和forward_list类型一样)。它没有后退操作。 也没有逆向迭代器。
2.迭代器的模板参数是:
template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
- Value:节点的实值型别。和红黑树一样,它其实也是键值对。
- Key:节点的键值型别
- HashFcn:hash function的函数型别
- ExtractKey:从节点中取出键值的方式(仿函数)
- EqualKey:判断键值相同与否的方法
- Alloc:空间配置器,缺省使用std::alloc
3.有两个指针成员。cur指针指向当前节点。ht指针指向hashtable容器,作为迭代器和容器的连接。因为迭代器递增的时候,可能需要从一个桶跳到另一个桶。
在operator++()
函数中,能看到迭代器走到一个桶的尾巴的情况:
if (!cur) {
size_type bucket = ht->bkt_num(old->val);
while (!cur && ++bucket < ht->buckets.size())
cur = ht->buckets[bucket];
}
先用bkt_num
函数得到当前迭代器所在的bucket号,然后递增号码,取出下一个桶的链表头节点。如果这个头节点是空的,说明这个桶里面没有元素,接着查看下一个桶。直到头节点不为空,返回curr。
4.如上所示,迭代器的递增行为就是移动到链表的下一个节点;如果已经走到了链表的尾巴,就需要走到下一个有元素的桶,接着沿链表往下走。
hashtable的数据结构
1.模板参数和迭代器相同。
template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
2.数据成员有5个:HashFcn、ExtractKey、EqualKey默认构造的对象,和buckets数组,以及这个数组的大小。
3.标准库的表格大小为质数。标准库已经预先储存了28个质数(逐渐呈现大约2倍的关系),以备随时访问。储存的最大的质数是4294967291,所以桶数量最多也就是这么多。
标准库的__stl_next_prime
函数用来计算,不小于n并且最接近n的那个质数。
hashtable的构造与内存管理
1.不提供默认构造函数。
2.其中的一个构造函数是:
hashtable(size_type n, const HashFcn& hf, const EqualKey& eql)
: hash(hf), equals(eql), get_key(ExtractKey()), num_elements(0)
{
initialize_buckets(n);
}
void initialize_buckets(size_type n)
{
const size_type n_buckets = next_size(n);
buckets.reserve(n_buckets);
buckets.insert(buckets.end(), n_buckets, (node*_ 0);
num_elements = 0;
}
它接受三个参数,节点数量n,HashFcn类实例化的一个对象,EqualKey类实例化的一个对象。注意初始化get_key的是ExtractKey的默认构造函数构造的对象。而表中元素的数量初始化为0。
在initialize_buckets
函数中,next_size函数获得的就是储存的质数中大于等于n的最小的那个,把它作为桶的数量n_buckets。然后把buckets数组的capacity调整为n_buckets。并将vector的每个元素都设置为空指针。
3.插入操作。和红黑树类似,插入操作也有insert_equal
和insert_unique
之分。
4.resize()函数。
上面两种插入操作,在真正插入前都要先判断是不是需要重新调整哈希表的大小。判断标准似乎有些奇特:如果元素个数(把新增元素计入)超过了buckets数组的大小,就重新计算表格的大小。如果计算出来新的表格大小比原来的大(一般情况下当然会增大,除了一种特殊情况,就是旧的数组大小是最大的4294967291,它没法继续增大了),就需要重新建立一个新的buckets数组,把旧数组中的元素放进新的数组。
注意上面所说的“放进”,不需要在新的数组中重新构造元素,而只需要修改原来的链表的指针指向。于是也只需要将旧的数组释放掉,旧的链表节点都还在,只不过连接到了新的地方。
释放旧的数组也很特别,新构造的数组是temp,将它和旧的数组进行swap,于是数据结构中的buckets就成了新的数组,而temp会随着函数调用结束自动释放空间,相当于将旧的数组释放了。
5.insert_unique_noresize函数。
在resize后,insert_unique会调用insert_unique_noresize来进行真正的插入操作。bkt_num
函数用来计算应该放进哪个桶中。然后要判断这个桶里面是不是已经放了这个元素,如果是,就直接返回;否则,构造新的链表节点,把节点插入这个桶的链表头(链表插入都是这样的,往头上插,不会忘尾巴上插)。
和红黑树的插入类似,返回值也是一个pair类型,第一个成员是插入后指向这个元素的迭代器,第二个成员是bool类型,用来反映插入是否成功。如果原来这个元素就已经放在桶里面了,就是失败,否则就是成功。
6.insert_equal_noresize函数
在resize后,insert_equal会调用insert_equal_noresize来进行真正的插入操作。它和insert_unique_noresize的区别在于,它如果发现桶里面已经有要插入的元素了,仍然会构造一个节点,插入到这个重复存在的节点后面。
7.判断元素应该放在哪一个桶里,是用bkt_num
函数实现的。这个函数先调用hash function处理元素,然后再模n,这个n可以作为参数传入,否则n就默认是buckets数组的大小。
SGI标准库内建了一些hash functions,它们都是仿函数。在p268能看到它们的定义。能看到,对于像int、long、char等整数型别,hash functions什么也不做,就是直接返回其自身;但是对于字符字符串(const char*),就设计了一个转换函数:
inline size_t __stl_hash_string(const char* s)
{
unsigned long h = 0;
for ( ; *s; ++s)
h = 5 * h + *s;
return size_t(h);
}
标准库在这里使用了泛化和偏特化的方法,首先定义一个泛化版本的hash函数,然后定义多个偏特化的版本:
/* 泛化版本 */
template <class Key> struct hash {};
/* 一个偏特化版本 */
template<> hash<char*>
{
size_t operator() (const char* s) const {return __stl_hash_string(s);}
}
/* 另一个偏特化版本 */
template<> hash<int>
{
size_t operator() (int x) const {return x;}
};
可以看出,标准库默认支持的键的类型包括整数类型(int、long、char)等,以及字符串类型。标准库没有提供其他类型的hash特化版本,要想使用其他类型的键,用户必须提供自定义的仿函数。比如对于pair类型,网上能找到的最常见的一个仿函数设计:
struct pair_hash
{
template<class T1, class T2>
std::size_t operator() (const std::pair<T1, T2>& p) const
{
auto h1 = std::hash<T1>{}(p.first);
auto h2 = std::hash<T2>{}(p.second);
return h1 ^ h2;
}
};
7.hashtable的复制和整体删除
由于使用链表结构,所以在复制和删除的时候都需要注意内存空间的释放。