Chapter 5: 关联式容器之 hashtable
2017-01-19 20:41
281 查看
SGI 中的 hashtable 使用的是开链法(separate chaining)。这种做法是在每一个表格元素中维护一个 list;hash function 为我们分配某一个 list,然后我们在那个 list 身上执行元素的插入,搜寻和删除等操作。虽然针对 list 而进行的搜寻只能是一种线性操作,但如果 list 够短,速度还是够快的。
2:hash table 的节点定义代码如下:
注意,bucket 所维护的 linked list,并不采用 STL 的 list 或 slist,而是自行维护上述的 hash table node。至于 buckets 聚合体,则以 vector 完成,以便有动态扩张能力;
在 hashtable 的前向声明中,有六个模板参数,模板参数 Value 表示的是节点的实值型别,Key 表示的是节点的键值型别,HashFcn 表示的是 hash function 的函数型别,ExtractKey 表示的是从节点中取出键值的方法(函数或仿函数),EqualKey 表示的是判断键值相同与否的方法(函数或仿函数),Alloc 为空间配置器,默认使用 std::alloc。
节点配置与释放函数代码如下:
hashtable 中的一个构造函数如下:
其中
2):
4:复制(
复制和整体删除要特别注意内存的释放问题,代码如下:
5:查找(
这两个函数代码如下:
一: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 ; //令 first 指向 bucket 对应之串行头部 //如果 buckets 已被占用,此时 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 = 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 ; //令 first 指向 bucket 对应之链表头部 //如果 buckets 已被占用,此时 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 = ne d298 w_node(obj); //产生新节点 tmp->next = first; buckets = 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 ; 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 ; cur; cur = cur-> next) if (equals(get_key(cur->val), key)) ++result; return result; }
相关文章推荐
- STL关联式容器之散列表——hashtable
- 【STL源码剖析读书笔记】【第5章】关联式容器之hashtable
- 【STL源码剖析读书笔记】【第5章】关联式容器之hashtable
- Chapter 5: 关联式容器之 set 和 multiset
- STL学习——STL中的关联式容器总结(RB-tree、set、map、hashtable、hash_set、hash_map)
- Chapter 5: 关联式容器之 RB_tree
- 三 关联式容器(三)hashtable
- Chapter 5:关联式容器之 map 和 multimap
- STL源码剖析-关联式容器之hashtable
- STL源码剖析 - 第5章 关联式容器 - hashtable
- 三 关联式容器(五)hash_map与hash_multimap
- STL源码剖析-----关联式容器
- 统计公司员工喜欢吃的水果,并打印出最喜欢吃的前K中水果【map关联式容器,k/V结构】
- 《STL源码剖析》学习笔记系列之五——关联式容器(1)
- java容器--Map{Hashtable,HashMap,WeakHashMap}
- C++ Primer 随笔 Chapter 9 顺序容器
- STL源码剖析 - 第5章 关联式容器 - set
- STL源码剖析 - 第5章 关联式容器 - hash_set
- linux下练习 c++ 关联式容器共性测试,使用
- 零元学Expression Blend 4 - Chapter 14 用实例了解布局容器系列-「Pathlistbox」II