上一篇:《STL源码剖析》笔记-multiset/multimap
前面介绍了rb-tree是一种平衡二叉搜索树,它的操作具有"对数平均时间"的表现(O(log n)),并且需要在元素随机的前提下。而hashtable(散列表)结构,在插入、删除、搜索等操作具有”常数平均时间”的表现(O(1)),但是需要以空间消耗为代价,相当于以空间换时间。
hashtable概述
hashtable支持对任何有名项的存取和删除操作,所以也被视为字典结构。并且他的相关操作具有O(1)的时间复杂度,就像queue一样。一般来说字典结构元素越多,查找必定更加耗时,例如map。那么,hashtable是怎么实现常数时间的操作?下面是一个解释的例子:
假设所有元素都是16bits且不带正负号的整数,范围0~65535,那么简单的使用一个array即可以满足。
首先,配置一个array A,拥有65536个元素,索引号码0~65535,初始值全部为0,每一个元素的值代表相应元素出现的次数。当插入元素i就执行A[i]++,删除元素就执行A[i]–,如果搜索元素i,就检查A[i]是否为0。可以看到,以上每一个操作都是常数时间,不过需要负担array的空间和初始化。
上述方法的确实现了O(1)的时间复杂度,但是存在两个问题。一是元素大小如果是32bits或者更大,那么需要的空间将非常非常大;二是只对整数元素有效,无法满足字符串形式元素的需求。对于第二个问题,可以将字符编码,转换成数值例如ASCII码,但是这样产生的索引值同样会非常大。这样,最终的问题都归结到了需要巨大的空间。
避免使用空间过大的解决方法是通过一个映射函数将大的数字映射为小的数值,这种函数被称为hash function(散列函数)。不过,散列函数会有一个无法避免的问题,可能会有不同的元素被映射到了相同的位置,这就是碰撞问题。下面将介绍碰撞问题的几种解决方法:
一、线性探测
线性探测方法,就是插入时用hash function计算出位置,如果位置上已经有元素,那么就循序向后寻找空的位置,遇到结尾就跳到头部开始寻找;查找也是一样,用hash function计算,如果目标不符就向后查找;删除需要使用软删除,就是只删除记号,因为hash table中的所有元素都关系到其他元素的排列,在hash table进行重新整理的时候再真正删除。
如上图的例子,hash function是对10取余,可以看到这种方法存在一些问题。在插入一系列元素后,散列表中的元素都挤在了一起,这种现象叫做主集团,如果后续插入的元素落在主集团中,插入效率就会越来越低,引起恶性循环。
二、二次探测
二次探测和线性探测的区别是,线性探测在发现计算位置被使用时是按照X+1、X+2…的方式进行尝试,而二次探测则是X+12、X+22…。二次探测能解决线性探测的主集团的问题,但是也有一定可能会导致次集团,次集团的问题可以用double hashing(双重散列)来避免,详细内容可以参考https://www.cnblogs.com/wt869054461/p/5731577.html。
三、开链
前面的两种方法都属于开放寻址法,因为都是在一个数组中存放数据,能够直接寻址。开链法的思路是将同一个hash值的元素都存放到一个list中,hash table中存放的是list的开始地址,每当元素计算的hash值重复时,就在list中加入一个元素。SGI STL采用的就是这种方法,此时SGI STL称hash table中的元素为bucket,意思是它们不是单纯的元素,而是存储了一堆的元素。
hashtable的节点定义
bucket中存放的节点定义如下:
template <class Value>
struct __hashtable_node
{
__hashtable_node* next;
Value val;
};
而bucket存放在hashtable定义的vector中,以便于扩充hashtable的容量。
hashtable的迭代器
template <class Value, class Key, class HashFcn,
class ExtractKey, class EqualKey, class Alloc>
struct __hashtable_iterator {
typedef hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>
hashtable;
typedef __hashtable_iterator<Value, Key, HashFcn,
ExtractKey, EqualKey, Alloc>
iterator;
typedef __hashtable_const_iterator<Value, Key, HashFcn,
ExtractKey, EqualKey, Alloc>
const_iterator;
typedef __hashtable_node<Value> node;
typedef forward_iterator_tag iterator_category;
typedef Value value_type;
typedef ptrdiff_t difference_type;
typedef size_t size_type;
typedef Value& reference;
typedef Value* pointer;
node* cur; // 当前节点
hashtable* ht; // 指向真个hashtable,用于在bucket之间跳转
__hashtable_iterator(node* n, hashtable* tab) : cur(n), ht(tab) {}
__hashtable_iterator() {}
reference operator*() const { return cur->val; }
pointer operator->() const { return &(operator*()); }
iterator& operator++();
iterator operator++(int);
bool operator==(const iterator& it) const { return cur == it.cur; }
bool operator!=(const iterator& it) const { return cur != it.cur; }
};
// 前缀自增
template <class V, class K, class HF, class ExK, class EqK, class A>
__hashtable_iterator<V, K, HF, ExK, EqK, A>&
__hashtable_iterator<V, K, HF, ExK, EqK, A>::operator++()
{
const node* old = cur;
cur = cur->next;
// bucket中的下一个节点为空,也就是说已经是尾部,需要跳转到下一个有节点的bucket,如果接下来的bucket都为空,那么就放回空
if (!cur) {
size_type bucket = ht->bkt_num(old->val);
while (!cur && ++bucket < ht->buckets.size())
cur = ht->buckets[bucket];
}
return *this;
}
// 后缀自增
template <class V, class K, class HF, class ExK, class EqK, class A>
inline __hashtable_iterator<V, K, HF, ExK, EqK, A>
__hashtable_iterator<V, K, HF, ExK, EqK, A>::operator++(int)
{
iterator tmp = *this;
++*this;
return tmp;
}
hashtable的数据结构
以下例举了hashtable一部分定义。
template <class Value, class Key, class HashFcn,
class ExtractKey, class EqualKey, class Alloc>
class hashtable {
public:
// 为模板参数定义别名
typedef Key key_type;
typedef Value value_type;
typedef HashFcn hasher;
typedef EqualKey key_equal;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef value_type* pointer;
typedef const value_type* const_pointer;
typedef value_type& reference;
typedef const value_type& const_reference;
hasher hash_funct() const { return hash; }
key_equal key_eq() const { return equals; }
private:
hasher hash;
key_equal equals;
ExtractKey get_key;
typedef __hashtable_node<Value> node;
typedef simple_alloc<node, Alloc> node_allocator;
vector<node*,Alloc> buckets; // 存放bucket的vector
size_type num_elements; // 元素的总数
...
};
可以看到hashtable中以vector作为bucket的容器,另外hashtable需要很多的模板参数:
- Value,节点实值的型别。
- Key,节点键值的型别。
- HashFcn,散列函数的型别。
- ExtractKey,从节点中取出键值的方法。
- EqualKey,判断键值是否相等的方法。
- Alloc,空间配置器。
开链法并不需要hashtable的大小为质数,不过SGI STL中还是使用质数来作为hashtable的大小。并且,提供了一个函数用来计算最接近并大于某数的质数。
// 预定义了28个质数,并大致为两倍关系
static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
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, 3221225473ul, 4294967291ul
};
inline unsigned long __stl_next_prime(unsigned long n)
{
const unsigned long* first = __stl_prime_list;
const unsigned long* last = __stl_prime_list + __stl_num_primes;
const unsigned long* pos = lower_bound(first, last, n);
return pos == last ? *(last - 1) : *pos;
}
// 最大bucket数量
size_type max_bucket_count() const
{ return __stl_prime_list[__stl_num_primes - 1]; }
hashtable的构造与内存管理
hashtable的内存管理默认使用std::alloc空间管理器,每次申请一个node大小的空间。
typedef simple_alloc<node, Alloc> node_allocator;
node* new_node(const value_type& obj)
{
node* n = node_allocator::allocate();
n->next = 0;
__STL_TRY {
construct(&n->val, obj);
return n;
}
__STL_UNWIND(node_allocator::deallocate(n));
}
void delete_node(node* n)
{
destroy(&n->val);
node_allocator::deallocate(n);
}
hashtable的不提供默认构造函数,
hashtable(size_type n,
const HashFcn& hf,
const EqualKey& eql,
const ExtractKey& ext)
: hash(hf), equals(eql), get_key(ext), num_elements(0)
{
initialize_buckets(n);
}
hashtable(size_type n,
const HashFcn& hf,
const EqualKey& eql)
: hash(hf), equals(eql), get_key(ExtractKey()), num_elements(0)
{
initialize_buckets(n);
}
size_type next_size(size_type n) const { return __stl_next_prime(n); }
// 如果构造时传入50,那么就会从预定义的质数列表中找到大于50的最小元素,也就是53.
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); // vector中所有bucket都是null
num_elements = 0;
}
插入操作和表格重整
插入操作首先需要判断是否需要重整(resize),之后再进行插入:
pair<iterator, bool> insert_unique(const value_type& obj)
{
resize(num_elements + 1);
return insert_unique_noresize(obj);
}
template <class V, class K, class HF, class Ex, class Eq, class A>
void hashtable<V, K, HF, Ex, Eq, A>::resize(size_type num_elements_hint)
{
const size_type old_n = buckets.size();
// 用插入后元素的总数和buckets的大小进行比较,大于时需要resize
// 可以推论:buckets大小应该和hashtable中存放元素的最大数量一致
if (num_elements_hint > old_n) {
// 获得下一个预定义的质数,如果已经最大就不进行resize
const size_type n = next_size(num_elements_hint);
if (n > old_n) {
vector<node*, A> tmp(n, (node*) 0); // 重新分配一个vector
__STL_TRY {
for (size_type bucket = 0; bucket < old_n; ++bucket) {
node* first = buckets[bucket]; // 指向bucket的第一个节点
while (first) {
size_type new_bucket = bkt_num(first->val, n); // 计算当前bucket中的节点在新的vector中的位置
buckets[bucket] = first->next; // 记录下一节点
first->next = tmp[new_bucket]; // 将first的下一节点指向新bucket的第一个节点
tmp[new_bucket] = first; // 将旧bucket的节点放到了新bucket的头部
first = buckets[bucket]; // first指向了下一节点,循环将旧bucket中的节点移到新的bucket
}
}
// 最后将旧vector用新的vector替换掉
buckets.swap(tmp);
}
}
}
}
// 插入不重复的值
template <class V, class K, class HF, class Ex, class Eq, class A>
pair<typename hashtable<V, K, HF, Ex, Eq, A>::iterator, bool>
hashtable<V, K, HF, Ex, Eq, A>::insert_unique_noresize(const value_type& obj)
{
const size_type n = bkt_num(obj); // 计算元素所属bucket的位置
node* first = buckets[n];
// bucket中有相等键值的元素,返回失败
// 此处发现一个问题:先resize可能导致无效的扩容,因为有可能插入键值重复,实际上是不需要扩容的
for (node* cur = first; cur; cur = cur->next)
if (equals(get_key(cur->val), get_key(obj)))
return pair<iterator, bool>(iterator(cur, this), false);
// 分配新节点,并插入到bucket头部
node* tmp = new_node(obj);
tmp->next = first;
buckets[n] = tmp;
++num_elements;
return pair<iterator, bool>(iterator(tmp, this), true);
}
计算元素所属位置(bkt_num)
bkt_num使用SGI STL统一的hash算法(后续会进行介绍)计算出hash值再进行取余,在计算hash值之前,需要先获取元素的key值,这是因为存放的元素可能无法直接计算hash值,比如存放字符串类型,此时就需要进行转换。
size_type bkt_num_key(const key_type& key) const
{
return bkt_num_key(key, buckets.size());
}
size_type bkt_num(const value_type& obj) const
{
return bkt_num_key(get_key(obj));
}
size_type bkt_num_key(const key_type& key, size_t n) const
{
return hash(key) % n;
}
size_type bkt_num(const value_type& obj, size_t n) const
{
return bkt_num_key(get_key(obj), n);
}
复制和清除
template <class V, class K, class HF, class Ex, class Eq, class A>
void hashtable<V, K, HF, Ex, Eq, A>::clear()
{
// 先删除所有bucket中的所有节点
for (size_type i = 0; i < buckets.size(); ++i) {
node* cur = buckets[i];
while (cur != 0) {
node* next = cur->next;
delete_node(cur);
cur = next;
}
// 将bucket指向null
buckets[i] = 0;
}
// 元素数量置为0
num_elements = 0;
// 此时hashtable的vector还是原来的大小,没有进行清除
}
template <class V, class K, class HF, class Ex, class Eq, class A>
void hashtable<V, K, HF, Ex, Eq, A>::copy_from(const hashtable& ht)
{
buckets.clear(); // 先清空原有的vector
buckets.reserve(ht.buckets.size()); // 重新配置vector的最大大小
buckets.insert(buckets.end(), ht.buckets.size(), (node*) 0); // 所有bucket指向null
__STL_TRY {
for (size_type i = 0; i < ht.buckets.size(); ++i) {
if (const node* cur = ht.buckets[i]) {
// 配置节点空间并赋值
node* copy = new_node(cur->val);
buckets[i] = copy;
// 对bucket中的元素一一赋值,并建立指向关系
for (node* next = cur->next; next; cur = next, next = cur->next) {
copy->next = new_node(next->val);
copy = copy->next;
}
}
}
// 修改元素数量
num_elements = ht.num_elements;
}
__STL_UNWIND(clear());
}
hashtable实例
再插入48个元素,使元素数量超过质数53,可以看到vector扩容到了下一个预定义的质数96。
hash functions
在介绍计算元素所属位置函数时说到,对于char、int等整形,hash functions是能够直接得出散列值的,但是对于字符串类型需要进行转换。stl_hash_fun.h中提供了一个转换的方法:
// stl_hash_fun.h
template <class Key> struct hash { };
// 字符串转换成size_t
inline size_t __stl_hash_string(const char* s)
{
unsigned long h = 0;
for ( ; *s; ++s)
h = 5*h + *s;
return size_t(h);
}
__STL_TEMPLATE_NULL struct hash<char*>
{
size_t operator()(const char* s) const { return __stl_hash_string(s); }
};
__STL_TEMPLATE_NULL struct hash<const char*>
{
size_t operator()(const char* s) const { return __stl_hash_string(s); }
};
__STL_TEMPLATE_NULL struct hash<char> {
size_t operator()(char x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned char> {
size_t operator()(unsigned char x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<signed char> {
size_t operator()(unsigned char x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<short> {
size_t operator()(short x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned short> {
size_t operator()(unsigned short x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<int> {
size_t operator()(int x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned int> {
size_t operator()(unsigned int x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<long> {
size_t operator()(long x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned long> {
size_t operator()(unsigned long x) const { return x; }
};
而对于其他类型,比如string、double等,需要自定义对应的hash function来支持hash值的计算。