hashtable
hash table
可提供对任何有名项(named item)的存取操作和删除操作。由于操作对象是有名项,所以,hash table
也可以被视为一种字典结构。这种结构的用意在于提供常数时间的基本操作。
线性探测
当hash function 计算出某个元素的插入位置,而该位置上的空间已不再可用时,最简单的方法就是循序往下一一寻找(如果到达尾端,就绕道头部继续寻找),直到找到一个可用空间为止。只要表格足够大,总是能够找到一个安身立命的空间。但是要花多少时间就很难说了。进行元素搜寻操作时,如果 hash function 计算出来的位置上的元素值与我们的搜寻目标不符,就循序往下一一寻找,直到找到吻合者,或直到遇上空格元素。至于元素的删除,必须采用惰性删除,也就是只标记删除记号,实际删除操作待表格重新整理时再进行——这是因为hash table中的每一个元素不仅表述它自己,也关系到其他元素的排列。
二次探测
开链
开链法就是在每一个表格元素中维护一个list
;hash function
为我们分配某一个list
,然后我们在那个list
身上执行元素的插入,搜寻,删除等操作。虽然针对list
而进行的搜寻只能是一种线性操作,但如果list
够短,速度还是够快。
template<class Value>
struct __hashtable_node
{
__hashtable_node* next;
Value val;
};
注意,bucket所维护的linked list,并不采用STL的list或slist,而是自行维护上述的hash table node。至于buckets聚合体,则是以vector完成,以便有动态扩充能力。
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>;
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){}
__hastable_iterator(){}
reference operator*() const { return cur->val; }
pointer operator->() const { return &(operator*()); }
iterator& operator++(){
const node* old = cur;
cur = cur->next;
if(!cur){
//根据元素值,定位出下一个bucket。其起头处就是我们的目的地
size_type bucket = ht->bkt_num(old->val);
while(!cur && ++bucket < ht->buckets.size())
cur = ht->buckets[bucket];
}
return *this;
}
iterator operator++(int){
iterator tmp = *this;
++*this;
return tmp;
}
bool operator==(const iterator& it) const { return cur == it.cur; }
bool operator!=(const iterator& it) const { return cur != it.cur; }
};
请注意,hashtable的迭代器没有后退操作(operator--()
),hashtable也没有定义所谓的逆向迭代器。
hashtable的数据结构
template <class Value, class Key, class HashFcn, class ExtractKey,
class EqualKey, class Alloc>
class hashtable{
public:
typedef HashFcn hasher;
typedef EqualKey key_equal;
typedef size_t size_type;
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;
size_type num_elements;
public:
size_type bucket_count() const { return buckets.size(); }
}
hashtable的模板参数相当多,包括:
- Value:节点的实值类型
- Key:节点的键值类型
- HashFcn:hash function的函数类型
- ExtractKey:从节点中取出键值的方法
- EqualKey:判断键值相同与否的方法
- Alloc:空间配置器
注意,先前谈及概念时,指出hash functions
是计算元素位置的函数,SGI
将这项任务赋予了bkt_num(),由它调用hash function
取得一个可以执行取模运算的值。
虽然开链法并不要求表格大小必须为质数,但是SGI STL仍然以质数来设计表格大小,并且将28个质数计算好,以便随时访问。
hashtable的构造与内存管理
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)
{
destory(&n->val);
node_allocator::deallcate(n);
}
hashtable(size_type n,
const HashFcn& hf,
const EqualKey& eql)
: hash(hf), equals(eql),get_key(ExtractKey()), num_elements(0)
{
initialze_buckets(n);
}
void initialize_buckets(size_type n)
{
const size_type n_buckets = nuext_size(n);
//举例,传入50,返回53,以下首先保留53个元素空间,然后将其全部填0
buckets.reserve(n_buckets);
buckets.insert(buckets.end(), n_buckets, (node*)0);
num_elements = 0;
}
//next_size()返回最接近n并大于n的质数
size_type next_size(size_type n) const { return __stl_next_prime(n); }
插入操作与表格重整
当客户端开始插入元素(节点)时
iht.insert_unique(59);
iht.insert_unique(63);
iht.insert_unique(108);
//插入元素,不允许重复
pair<iterator, bool> insert_unique(const value_type& obj)
{
resize(num_elements + 1); //判断是否需要重建表格,如需要就扩充
return insert_unique_noresize(obj);
}
//以下判断是否需要重建表格。如果不需要,立刻返回。如果需要,就动手
tempate<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)
{
//以下“表格重建与否”的判断原则颇为奇特,是拿元素个数(把新增元素计入后)和
//bucket vector的大小来比,如果前者大于后者,就重建表格
//由此判断,链表的节点数和buckets vector的大小相同
const size_type old_n = buckets.size();
if(num_elemets_hint > old_n){ //确定是否真的需要重新配置
const size_type n = next_size(num_element_hint); //找出下一个质数
if(n > old_n){
vector<node*, A> tmp(n, (node*)0);
__STL_TRY{
//以下处理每一个旧的bucket
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内
buckets[bucket] = first->next; //令旧bucket指向其所对应串行的下一个节点
//将当前节点插入到新bucket内,称为其对应串行的第一个节点
first->next = tmp[new_bucket];
tmp[new_bucket] = first;
//回到旧bucket所指的待处理串行,准备处理下一个节点
first = buckets[bucket];
}
}
buckets.swap(tmp); //vector::swap。新旧两个buckets对调
//离开时释放local tmp的内存
}
}
}
}
//在不需要重建表格的情况下插入新节点。键值不允许重复
tempate<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); //决定obj应位于#n bucket
node* first = buckets[n];
for(node* cur = first; cur; cur = cur->next)
if(equals(get_key(cur->val), gey_key(obj)))
//如果发现与链表中的某个键值相同,就不插入,立刻返回
return pair<iterator, bool>(iterator(cur, this), false);
node* tmp = new_node(obj); //产生新节点
tmp->next = first;
buckets[n] = tmp; //令新节点称为链表的第一个节点
++num_elements; //节点个数加1
return pair<iterator, bool>(iterator(tmp, this), true);
}
如果客户端执行的是另一种节点插入行为(不再是insert_unique,而是insert_equal):
iterator insert_equal(const value_type& obj)
{
resize(num_elements + 1); //判断是否需要重建表格,如需要就扩充
return insert_equal_noresize(obj);
}
//在不需要重建表格的时候插入新节点,键值允许重复
tempate<class V, class K, class HF, class Ex, class Eq, class A>
typename hashtable<V,K,HF,Ex,Eq,A>::iterator
hashtable<V,K,HF,Ex,Eq,A>::insert_unique_noresize(const value_type& obj)
{
const size_type n = bkt_num(obj);
node* first = buckets[n]; //令first指向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;
return iterator(tmp, this); //返回迭代器,指向新节点
}
//没有发现重复的键值
node* tmp = new_node(obj); //产生新节点
tmp->next = first; //将新节点插入链表头部
buckets[n] = tmp;
++num_elements; //节点个数累加1
return iterator(tmp, this); //返回一个迭代器,指向新增节点
}
判断元素的落脚处
hash function
需要计算出某个元素落脚于哪一个bucket
之内,SGI
把这个任务包装了一层,先交给bkt_num()
函数,然后再由此函数调用hash function
,取得一个可以执行modulus(取模)运算的数值。为什么这么做?因为有些元素的类型无法直接拿来对hashtable大小进行模运算。
//版本1:接受实值value和buckets个数
size_type bkt_num(const value_type& obj, size_t n) const {
return bkt_num_key(get_key(obj), n);
}
//版本2:只接受实值value
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;
}
复制和整体删除
tempate<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];
//将bucket list中的每一个节点删除掉
while(cur != 0){
node* next = cur->next;
delete_node(cur);
cur = next;
}
buckets[i] = 0;
}
num_elements = 0;
//注意,buckets vector并未释放掉空间,仍保有原来大小
}
tempate<class V, class K, class HF, class Ex, class Eq, class A>
void hashtable<V,K,HF,Ex,Eq,A>::copy_fron(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(), (node*)0);
__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;
for(node* next = cur->next;next;cur=next,next=cur->next){
copy->next = new_node(next->val);
copy = copy->next;
}
}
}
num_elements = jt.num_elements;
}
__STL_UNWIND(clear());
}
hash function
SGI hashtable
无法处理string,double,float
。欲处理这些类型,用户必须自行定义hash function
。
可以处理char,unsigned char,signed char, short,int,unsigned int, long, unsigned long, char*, const char*
。