SGI 中的 hashtable 使用的是开链法(separate chaining)。这种做法是在每一个表格元素中维护一个 list;hash function 为我们分配某一个 list,然后我们在那个 list 身上执行元素的插入,搜寻和删除等操作。虽然针对 list 而进行的搜寻只能是一种线性操作,但如果 list 够短,速度还是够快的。
一:hashtable 的桶子(buckets)与节点(nodes)
1:首先我们将 hash table 表格内的元素称为桶子(bucket),此名称的大约意思是:表格内的每个单元,涵盖的不只是个节点(元素),甚至可能是一“桶”节点;
2:hash table 的节点定义代码如下:
template <class Value>
struct __hashtable_node
{
__hashtable_node* next;
Value val;
}
注意,bucket 所维护的 linked list,并不采用 STL 的 list 或 slist,而是自行维护上述的 hash table node。至于 buckets 聚合体,则以 vector 完成,以便有动态扩张能力;
二:hashtable 的迭代器
hashtable 的迭代器为前向迭代器,没有后退操作(operator--()
),其代码如下:
//hashtable 的前向声明
template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc = alloc>
class 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; //保持对容器的连结关系(因为可能需要从 bucket 跳到 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 Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
auto __hashtable_iterator<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>::operator++()
-> __hashtable_iterator&
{
const node* old = cur;
cur = cur->next; //如果存在,就是它,否则进入 if 流程
if (!cur) {
// 根据元素值,定位出下一个 bucket,其起头处就是我们的目的地
size_type bucket = ht->bkt_num(old->val);
while (!cur && ++bucket < ht->buckets.size())
cur = ht->buckets[bucket];
}
return *this;
}
template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
inline auto __hashtable_iterator<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>::operator++(int)
-> __hashtable_iterator
{
iterator tmp = *this;
++*this; //调用 operator++
return tmp;
}
在 hashtable 的前向声明中,有六个模板参数,模板参数 Value 表示的是节点的实值型别,Key 表示的是节点的键值型别,HashFcn 表示的是 hash function 的函数型别,ExtractKey 表示的是从节点中取出键值的方法(函数或仿函数),EqualKey 表示的是判断键值相同与否的方法(函数或仿函数),Alloc 为空间配置器,默认使用 std::alloc。
三:hashtable 的数据结构
1:hashtable 的构造与内存管理
在 hashtable 中,专属的节点配置器被定义如下:
private:
typedef __hashtable_node<Value> node;
typedef simple_alloc<node, Alloc> node_allocator;
节点配置与释放函数代码如下:
private:
node* new_node(const value_type& obj) {
node* n = node_allocator::allocate();
n->next = nullptr;
__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 中的一个构造函数如下:
public:
hashtable(size_type n, const HashFcn& hf, const EqualKey& eql) : hash(hf), equals(eql),
get_key(ExtractKey()), num_elements(0)
{ initialize_buckets(n); }
private:
void initialize_buckets(size_type n) {
const size_type n_buckets = next_size(n); //返回最接近 n 并大于等于 n 的质数
bucket.reserve(n_buckets);
bucket.insert(bucket.end(), n_buckets, static_cast<node*>nullptr)
num_elements = 0;
}
2:hashtable 的插入操作与表格重整
1):insert_unique()
表示不允许插入重复的元素,代码如下:
public:
pair<iterator, bool> insert_unique(const value_type& obj) {
resize(num_elements + 1); //判断是否需要重建表格,如需要就扩充
return insert_unique_noresize(obj);
}
其中resize()
函数用来判断是否需要重建表格,insert_unique_noresize()
表示在不需要重建表格的情况下,插入新节点,键值不能重复,这两个函数的代码如下:
template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
void hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>::resize(size_type num_elements_hint)
{
//如果元素个数大于 bucket vector 大小,则重建表格
const size_type old_n = buckets.size();
if (num_elements_hint > old_n) { //确定真的需要重新配置
const size_type n = next_size(num_elements_hint) //找出下一个质数
if (n > old_n) {
vector<node*, Alloc> tmp(n, static_cast<node*>(nullptr)); //设立新的 buckets
__STL_TRY {
//以下处理每一个旧的 bucket
for (size_type bucket = 0; bucket < old_n; ++bucket) {
node* first = buckets[bucket]; //指向节点所对应之串行的起始节点
//以下处理每一个旧 bucket 所含(串行)的每一个节点
while (first) { //串行还没有结束
//以下找出节点落在哪一个新 bucket 内
size_type new_bucket = bkt_num(first->val, n);
//令旧 bucket 指向其所对应之串行的下一个节点
buckets[bucket] = first->next;
//将当前节点插入到新 bucket 内,成为其对应串行的第一个节点
first->next = tmp[buckets];
tmp[buckets] = first;
//回到旧 bucket 所指的待处理行,准备处理下一个节点
first = buckets[bucket];
}
}
buckets.swap(tmp); // vector::swap. 新旧两个 buckets 对调
//离开时释放 local tmp 的内存
}
}
}
}
template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
auto hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>::insert_unique_noresize(const value_type& obj)
-> pair<iterator, bool>
{
const size_type n = bkt_num(obj); //决定 obj 应位于 #n bucket
node* first = buckets[n]; //令 first 指向 bucket 对应之串行头部
//如果 buckets[n] 已被占用,此时 first 将不为 0,于是进入以下循环,
//走过 bucket 所对应的整个链表
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);
//离开以上循环(或根本未进入循环)时,first 指向 bucket 所指链表的头部结点
node* tmp = new_node(obj); //产生新节点
tmp->next = first;
buckets[n] = tmp; //令新节点成为链表的第一节点
++num_elements; //节点个数累加1
return pair<iterator, bool>(iterator(tmp, this), true);
}
2):insert_equal()
表示可以插入相同的元素,函数代码如下:
public:
pair<iterator, bool> insert_equal(const value_type& obj) {
resize(num_elements + 1); //判断是否需要重建表格,如需要就扩充
return insert_equal_noresize(obj);
}
insert_equal_noresize()
函数代码如下:
template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
auto hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>::insert_equal_noresize(const value_type& obj)
-> pair<iterator, bool>
{
const size_type n = bkt_num(obj); //决定 obj 应位于 #n bucket
node* first = buckets[n]; //令 first 指向 bucket 对应之链表头部
//如果 buckets[n] 已被占用,此时 first 将不为0,于是进入以下循环
//走过 bucket 所对应的整个链表
for(node* cur = first; cur; cur = cur->next) {
if (equals(get_key(cur->val), get_key(obj))) {
//如果发现与链表中的某键值相同,就马上插入,然后返回
node* tmp = new_node(obj); //产生新节点
tmp->next = cur->next; //将新节点插入目前位置之后
cur->next = tmp;
++num_elements; //节点个数累加1
return iterator(tmp, this); //返回一个迭代器,指向新增节点
}
}
//进行至此,没有发现重复的键值
node* tmp = new_node(obj); //产生新节点
tmp->next = first;
buckets[n] = tmp; //令新节点成为链表的第一节点
++num_elements; //节点个数累加1
return pair<iterator, bool>(iterator(tmp, this), true); //返回一个迭代器,指向新增节点
}
3:判知元素的落脚处
当我们插入一个元素时,我们需要判断它应该要被插入哪一个 bucket 之间,这可求助于bkt_num()
函数,代码如下:
public:
//下面四个版本用来判断元素落在哪一个 bucket 之内
//版本1:接受实值 (value) 和 buckets 个数
size_type bkt_num(const value_type& obj, size_t n) const
{ return bkt_num_key(get_key(obj), n); }
//版本2:只接受实值
size_type bkt_num(const value_type& obj) const
{ return bkt_num_key(get_key(obj)); }
//版本3:只接受键值
size_type bkt_num_key(const key_type& key) const
{ return bkt_num_key(key, buckets.size()); }
//版本4:接受键值和 buckets 个数
size_type bkt_num_key(const key_type& key, size_t n) const
{ return hash(key) % n; }
4:复制(copy_from()
)和整体删除(clear()
)
复制和整体删除要特别注意内存的释放问题,代码如下:
template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
void hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>::clear()
{
//针对每一个 bucket
for (size_type i =0; i != buckets.size(); ++i) {
node* cur = buckets[i];
//将 bucket list 中的每个节点删除掉
while (cur) {
node* next = cur->next;
delete_node(cur);
cur = next;
}
buckets[i] = nullptr; //令 bucket 内容为 null 指针
}
num_elements = 0; //令总节点个数为0
//注意,buckets vector 并未释放掉空间,仍保持原来大小
}
template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc>
void hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>::copy_from(const hashtable& ht)
{
//先清除己方的 buckets vector ,这操作是调用 vector::clear。将整个容器清空是buckets.clear()
//为己方的 buckets vector 保留空间,使与对方相同
//如果己方空间大于对方,就不动,如果己方空间小于对方,就增大
buckets.reserve(ht.buckets.size());
//从己方的 buckets vector 尾端开始,插入 n 个元素,其值为 null 指针
//注意,此时的 buckets vector 为空,所以所谓尾端,就是起头处
buckets.insert(buckets.end(), ht.buckets.size(), static_cast<node*>(nullptr));
__STL_TRY {
//针对 buckets vector
for (size_type i = 0; i != ht.buckets.size(); ++i) {
//复制 vector 的每一个元素(是个指针,指向 hastable 节点)
if (const node* cur = ht.buckets[i]) {
node* copy = new_node(cur->val);
buckets[i] = copy;
//针对同一个 bucket list,复制每一个节点
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());
}
5:查找(find()
)和计数(count()
)函数
这两个函数代码如下:
public:
iterator find(const key_type& key) {
size_type n = bkt_num_key(key); //首先寻找落在哪一个 bucket 内
node* first;
//以下,从 bucket list 的头开始,一一比对每个元素的键值,比对成功就跳出
for (first = buckets[n]; first && !equals(get_key(first->val), key); first = first->next)
{}
return iterator(first, this);
}
size_type count(const key_type& key) const {
const size_type n = bkt_num_key(key); //首先寻找落在哪一个 bucket 内
size_type result = 0;
//以下,从 bucket list 的头开始,一一比对每个元素的键值,比对成功就累加1
for (const node* cur = bucket[n]; cur; cur = cur-> next)
if (equals(get_key(cur->val), key))
++result;
return result;
}