SGISTL源码阅读二十二 hashtable上
前言
hashtable的部分基本知识
hashtable
,哈希表,也可称为散列表。
哈希表用于以常数平均时间执行插入、删除和查找操作,每个关键字按照某种规定被映射到从0-TableSize-1这个范围中的某个数,并且被放到适当的单元中。
时间复杂度可以达到常数级别听起来让人觉得很兴奋,但是它也是需要付出代价的,哈希表的一个最重要的问题就是解决冲突,如果一个关键字刚好能被映射到一个空位置上,那当然是极好的了,但是如果这个位置上已经存入了其他的关键字呢?
所以实现hashtable
最重要的两个东西就是哈希函数(指定某种创建hashtable的规则)和解决冲突。
常用的hash函数
- 直接定址法:取关键字或者关键字的某个线性函数为散列地址,比如hash(k) = k或hash(k) = a * k + b,a、b为常数。
- 数字分析法:假设关键字是以r为基的数,并且哈希表中可能出现的关键字都是事先知道的,则可取关键字的若干数位组成哈希地址。
- 平方取中法:取关键字平方后的中间几位为哈希地址。通常在选定哈希函数时不一定能知道关键字的全部情况,取其中的哪几位也不一定合适,而一个数平方后的中间几位数和数的每一位都相关,由此使随机分布的关键字得到的哈希地址也是随机的。取的位数由表长决定。
- 折叠法:将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。
…
解决冲突
-
分离连接法
分离链接的做法是将映射到同一个位置的关键字放在一个链表当中。
-
开放定址法
开放地址散列法的基本思想是,遇到了冲突,我们就用另一套法则,将此关键字放在其他的空缺位置上。显而易见,他的缺点是散列表的创建必须足够大,才能够容许我们进行相关的操作,比起分离链接法来讲,虽然算法速度上有所提升,但是__内存浪费__比较大。因为他的装填因子λ应该低于0.5。
装填因子λ:散列表中的元素个数与散列表大小的比值。
开放定址法的实现也有多种
1.线性探测法
在原来的散列表基础之上添加增量序列,(1,2……,TableSize-1)循环试探下一个存储地址。
2.平方探测 – 二次探测
平方探测法:以增量序列1²,-1²,2²,-2²,……,q²,-q² 且 q≤|TableSize/2|循环试探下一个存储地址
以上仅为部分hashtable
的知识,这篇文章以分析STL中实现的hashtable
为主
在SGISTL中,hashtbale
解决充吐的方法是分离链接法(叫法可能不同),称hash table
表格内的元素为桶子(bucket),因为表格内的单元不仅仅是一个节点,可能有多个节点存在同一个bucket里面。
下面我们通过源码阅读来学习它。
深入源码STL中的hashtable
哈希函数
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; }
我们可以看到对于普通的数字或者char
类型,SGISTL使用的哈希函数是直接定址法,并且根据不同的关键字有着不同的重载版本。
对string
的操作是比较特殊的。
hashtable
的数据结构
//hashtable的一个结点
template <class Value>
struct __hashtable_node
{
__hashtable_node* next;
Value val;
};
//...
//缺省使用了SGISTL的空间配置器
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; //hash function的函数型别
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;
//buckets,之前提到过的桶子,这里是一堆桶子,是用vector存储的
//桶子里面放的是节点
vector<node*,Alloc> buckets;
桶子的数量
SGISTL以28个质数来设计桶的数量。(从53开始,左键呈现大约两倍的关系)。
//把这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
};
//用来返回这28个质数中最接近且大于n的一个质数
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;
}
hashtable
的构造函数
public:
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);
}
//拷贝构造函数
hashtable(const hashtable& ht)
: hash(ht.hash), equals(ht.equals), get_key(ht.get_key), num_elements(0)
{
copy_from(ht);
}
//...
//以桶数量n来初始化桶
void initialize_buckets(size_type n)
{
//从28个质数中找到一个合适的值作为桶的数量
const size_type n_buckets = next_size(n);
//调用vector的函数
//reserve是容器预留空间,但在空间内不真正创建元素对象,所以在没有添加新的对象之前,不能引用容器内的元素。
buckets.reserve(n_buckets);
//将所有值置0(类型为node*)
buckets.insert(buckets.end(), n_buckets, (node*) 0);
num_elements = 0;
}
//...
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();
//和initialize_buckets的操作相同
buckets.reserve(ht.buckets.size());
buckets.insert(buckets.end(), ht.buckets.size(), (node*) 0);
__STL_TRY {
//依次复制hashtable ht中桶的节点到当前hashtable
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;
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
已经有了一个比较清除的认识,其实hashtable
也可以认为是一种字典结构,一个关键字对应着一个映射。
接下来我们将继续学习hashtable
的迭代器以及它的相关操作。