STL学习——Hashtable篇
概述
hash table(散列表)是一种在插入,删除,搜索等操作“常数平均时间”完成的数据结构。它是一种字典结构。哈希函数是一种映射函数,它能够将大数映射为小数。负责将某一元素映射为一个“大小可接受之索引”。哈希函数使用过程中可能会出现不同的元素被映射到相同的位置(相同的索引),称为哈希碰撞。解决哈希碰撞的方法:线性探测,二次探测,开链等等。
负载系统:指元素个数除以表格大小。负载系数永远在0-1之间——除非采用开链的策略。哈希过程中,如果平均插入成本的成长幅度,远高于负载系数的成长长度则为主集团现象。线性探测解决哈希冲突过程中可能会出现主集团现象。
二叉探测主要用来解决主集团问题。如果表格大小为质数,且永远保持负载系数在0.5以下,就可以确定每插入一个新元素所需要的探测次数不多于2。线性探测的做法是一个加法(加1)、一个测试(看是否需要绕转回头),以及一个偶需为之的减法(用以绕转回头)。二次探测需要的是一个加法(从i-1到i),一个乘法(i^2),另一个加法,以及一个mod运算。计算复杂度大,可以利用前一个的H值减后一个H值,然后做移位即乘2和减法操作即可。
二次探测可以消除主集团,却可能造成次集团:两个元素经hash function计算出来的位置若相同,则插入时所需探测的位置也相同,形成某种浪费。消除次集团方法可以用复式散列。
开链法消除哈希碰撞,原理是在每一个表格元素中维护一个list:哈希函数为某一个list,然后在list身上执行插入,搜寻,删除等操作。使用开开链法,表格的负载系数将大于1。
hashtable的桶子与节点
开链法实现的哈希表,其表格内的每个单元,涵盖的不只是节点(元素),甚至是一个桶节点。下面是hashtable节点的定义。
template <class _Val> struct _Hashtable_node { _Hashtable_node* _M_next; _Val _M_val; };
hashtable迭代器
template <class _Val, class _Key, class _HashFcn, class _ExtractKey, class _EqualKey, class _Alloc> struct _Hashtable_iterator; template <class _Val, class _Key, class _HashFcn, class _ExtractKey, class _EqualKey, class _Alloc> struct _Hashtable_const_iterator; template <class _Val, class _Key, class _HashFcn, class _ExtractKey, class _EqualKey, class _Alloc> struct _Hashtable_iterator { typedef hashtable<_Val,_Key,_HashFcn,_ExtractKey,_EqualKey,_Alloc> _Hashtable; typedef _Hashtable_iterator<_Val, _Key, _HashFcn, _ExtractKey, _EqualKey, _Alloc> iterator; typedef _Hashtable_const_iterator<_Val, _Key, _HashFcn, _ExtractKey, _EqualKey, _Alloc> const_iterator; typedef _Hashtable_node<_Val> _Node; typedef forward_iterator_tag iterator_category; typedef _Val value_type; typedef ptrdiff_t difference_type; typedef size_t size_type; typedef _Val& reference; typedef _Val* pointer; _Node* _M_cur; // 迭代器目前所指之节点 _Hashtable* _M_ht; // 保持对容器的连结关系(因为可能需要从bucket跳到bucket) _Hashtable_iterator(_Node* __n, _Hashtable* __tab) : _M_cur(__n), _M_ht(__tab) {} _Hashtable_iterator() {} reference operator*() const { return _M_cur->_M_val; } #ifndef __SGI_STL_NO_ARROW_OPERATOR pointer operator->() const { return &(operator*()); } #endif /* __SGI_STL_NO_ARROW_OPERATOR */ iterator& operator++(); iterator operator++(int); bool operator==(const iterator& __it) const { return _M_cur == __it._M_cur; } bool operator!=(const iterator& __it) const { return _M_cur != __it._M_cur; } }; // 注意,hashtable迭代器必须永远维系着与整个“buckets vector”的关系,并记录目前所指的节点。其前进操作是首先 // 尝试从目前所指的节点出发,前进一个位置(节点),由于节点被安置于list内,所以利用节点的next指针即可轻易达 // 成前进操作。如果目前节点正巧是list的尾端,就跳至下一个bucket身上,那正是指向下一个list的头部节点。 template <class _Val, class _Key, class _HF, class _ExK, class _EqK, class _All> _Hashtable_iterator<_Val,_Key,_HF,_ExK,_EqK,_All>& _Hashtable_iterator<_Val,_Key,_HF,_ExK,_EqK,_All>::operator++() { const _Node* __old = _M_cur; _M_cur = _M_cur->_M_next; // 如果存在,就是他。否则进入以下if流程 if (!_M_cur) { // 根据元素值,定位出下一个bucket。其起头出就是我们的目的地 size_type __bucket = _M_ht->_M_bkt_num(__old->_M_val); while (!_M_cur && ++__bucket < _M_ht->_M_buckets.size()) // 注意,operator++ _M_cur = _M_ht->_M_buckets[__bucket]; } return *this; } template <class _Val, class _Key, class _HF, class _ExK, class _EqK, class _All> inline _Hashtable_iterator<_Val,_Key,_HF,_ExK,_EqK,_All> _Hashtable_iterator<_Val,_Key,_HF,_ExK,_EqK,_All>::operator++(int) { iterator __tmp = *this; ++*this; // 调用operator++() return __tmp; } // hashtable的迭代器没有后退操作(operator--()),hashtable也没有定义所谓的逆向迭代器(reverse iterator)。
hashtable数据结构
// hashtable的模板参数相当多,包括: // 1)Value:节点的实值型别; // 2)Key:节点的键值型别; // 3)HashFcn:hash function的函数型别; // 4)ExtractKey:从节点中取出键值的方法(函数或仿函数); // 5)EqualKey:判断键值相同与否的方法(函数或仿函数); // 6)Alloc:空间配置器,缺省使用std::alloc。 template <class _Val, class _Key, class _HashFcn, class _ExtractKey, class _EqualKey, class _Alloc> // 先前声明时,已给予Alloc默认值alloc class hashtable { public: typedef _Key key_type; typedef _Val value_type; typedef _HashFcn hasher; // 为template型别参数重定义一个名称 typedef _EqualKey key_equal; // 为template型别参数重定义一个名称 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 _M_hash; } key_equal key_eq() const { return _M_equals; } private: typedef _Hashtable_node<_Val> _Node; ... private: // 以下三者都是function objects。<stl_hash_fun.h>中定义有数个标准型别(如int,c-style,string等)的hasher hasher _M_hash; key_equal _M_equals; _ExtractKey _M_get_key; vector<_Node*,_Alloc> _M_buckets; // 以vector完成 size_type _M_num_elements; ... // 注意:假设long之后又32位。 // 虽然开链法并不要求表格大小必须为指数,但SGI STL仍然以质数来设计表格大小,并且先将28个质数(逐渐呈现大约两倍 // 的关系)计算好,以备随时访问,同时提供一个函数,用来查询在这28个质数之中,“最接近某个数并大于某个数”的质数。 enum { __stl_num_primes = 28 }; static const unsigned long __stl_prime_list[__stl_num_primes] = { 53ul, 97ul, 193ul, 389ul, 769ul, 1543ul, 3079ul, 6151ul, 12289ul, 24593ul, 49157ul, 98317ul, 196613ul, 393241ul, 786433ul, 1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul, 50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul, 1610612741ul, 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 + (int)__stl_num_primes; const unsigned long* pos = lower_bound(__first, __last, __n); // 以上,lower_bound()是泛型算法,使用lower_bound(),序列需先排序。没问题,上述数组已排序。 return pos == __last ? *(__last - 1) : *pos; } size_type bucket_count() const { return _M_buckets.size(); } // bucket个数即buckets vector的大小 size_type max_bucket_count() const { return __stl_prime_list[(int)__stl_num_primes - 1]; } // 总共可以有多少个buckets,其值为4294967291-1
hashtable的构造与内存管理
// 内存构造 private: typedef _Hashtable_node<_Val> _Node; #ifdef __STL_USE_STD_ALLOCATORS public: typedef typename _Alloc_traits<_Val,_Alloc>::allocator_type allocator_type; allocator_type get_allocator() const { return _M_node_allocator; } private: typename _Alloc_traits<_Node, _Alloc>::allocator_type _M_node_allocator; _Node* _M_get_node() { return _M_node_allocator.allocate(1); } void _M_put_node(_Node* __p) { _M_node_allocator.deallocate(__p, 1); } # define __HASH_ALLOC_INIT(__a) _M_node_allocator(__a), #else /* __STL_USE_STD_ALLOCATORS */ public: typedef _Alloc allocator_type; allocator_type get_allocator() const { return allocator_type(); } private: typedef simple_alloc<_Node, _Alloc> _M_node_allocator_type; _Node* _M_get_node() { return _M_node_allocator_type::allocate(1); } void _M_put_node(_Node* __p) { _M_node_allocator_type::deallocate(__p, 1); } # define __HASH_ALLOC_INIT(__a) #endif /* __STL_USE_STD_ALLOCATORS */ // 构造函数 hashtable(size_type __n, const _HashFcn& __hf, const _EqualKey& __eql, const _ExtractKey& __ext, const allocator_type& __a = allocator_type()) : __HASH_ALLOC_INIT(__a) _M_hash(__hf), _M_equals(__eql), _M_get_key(__ext), _M_buckets(__a), _M_num_elements(0) { _M_initialize_buckets(__n); } hashtable(size_type __n, const _HashFcn& __hf, const _EqualKey& __eql, const allocator_type& __a = allocator_type()) : __HASH_ALLOC_INIT(__a) _M_hash(__hf), _M_equals(__eql), _M_get_key(_ExtractKey()), _M_buckets(__a), _M_num_elements(0) { _M_initialize_buckets(__n); } hashtable(const hashtable& __ht) : __HASH_ALLOC_INIT(__ht.get_allocator()) _M_hash(__ht._M_hash), _M_equals(__ht._M_equals), _M_get_key(__ht._M_get_key), _M_buckets(__ht.get_allocator()), _M_num_elements(0) { _M_copy_from(__ht); } size_type _M_next_size(size_type __n) const // 返回最接近n并大于或等于n的质数 { return __stl_next_prime(__n); } void _M_initialize_buckets(size_type __n) { const size_type __n_buckets = _M_next_size(__n); // 举例:传入50,返回53。以下首先保留53个元素空间,然后将其全部填0 _M_buckets.reserve(__n_buckets); _M_buckets.insert(_M_buckets.end(), __n_buckets, (_Node*) 0); _M_buckets.insert(_M_buckets.end(), __n_buckets, (_Node*) 0); _M_num_elements = 0; } // 插入元素,不允许重复 pair<iterator, bool> insert_unique(const value_type& __obj) { resize(_M_num_elements + 1); // 判断是否需要重建表格,如需要就扩充 return insert_unique_noresize(__obj); } // 插入元素,允许重复 iterator insert_equal(const value_type& __obj) { resize(_M_num_elements + 1); // 判断是否需要重建表格,如需要就扩充 return insert_equal_noresize(__obj); } // 以下函数判断是否需要重建表格。如果不需要,立刻回返。如果需要,就动手... template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All> void hashtable<_Val,_Key,_HF,_Ex,_Eq,_All> ::resize(size_type __num_elements_hint) { // 表格重建与否原则:是拿元素个数(把新增元素计入后)和bucket vector的大小来比。如果前者大于后者,就重建表格 // 由此可判知,每个bucket(list)的最大容量和buckets vector的大小相同 const size_type __old_n = _M_buckets.size(); if (__num_elements_hint > __old_n) { // 确定真的需要重新配置 const size_type __n = _M_next_size(__num_elements_hint); // 找出下一个质数 if (__n > __old_n) { vector<_Node*, _All> __tmp(__n, (_Node*)(0), // 设立新的buckets _M_buckets.get_allocator()); __STL_TRY { // 以下处理每一个旧的bucket for (size_type __bucket = 0; __bucket < __old_n; ++__bucket) { _Node* __first = _M_buckets[__bucket]; // 指向节点所对应串行的起始节点 // 以下处理每一个旧bucket所含(串行)的每一个节点 while (__first) { // 串行还没结束时 size_type __new_bucket = _M_bkt_num(__first->_M_val, __n); // 找出节点落在哪一个新bucket内 _M_buckets[__bucket] = __first->_M_next; // 令旧的bucket指向其所对应之串行的下一个节点(以便迭代处理) __first->_M_next = __tmp[__new_bucket]; // 将当前节点插入到新bucket内,成为其对应串行的第一个节点 __tmp[__new_bucket] = __first; __first = _M_buckets[__bucket]; // 回到旧bucket所指的待处理串行,准备处理下一个节点 } } _M_buckets.swap(__tmp); // vector::swap。新旧两个buckets对调,注意,对调两方如果大小相同,大的会变小,小的会变大,离开时释放local tmp的内存 } # ifdef __STL_USE_EXCEPTIONS catch(...) { for (size_type __bucket = 0; __bucket < __tmp.size(); ++__bucket) { while (__tmp[__bucket]) { _Node* __next = __tmp[__bucket]->_M_next; _M_delete_node(__tmp[__bucket]); __tmp[__bucket] = __next; } } throw; } # endif /* __STL_USE_EXCEPTIONS */ } } } // 在不需要重建表格的情况下插入新节点。键值不允许重复 template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All> pair<typename hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>::iterator, bool> hashtable<_Val,_Key,_HF,_Ex,_Eq,_All> ::insert_unique_noresize(const value_type& __obj) { const size_type __n = _M_bkt_num(__obj); // 决定obj应位于#n bucket _Node* __first = _M_buckets[__n]; // 令first指向bucket对应之串行头部 // 如果bucket[n]已被占用,此时first将不为0,于是进入以下循环,走过bucket所对应的整个链表 for (_Node* __cur = __first; __cur; __cur = __cur->_M_next) if (_M_equals(_M_get_key(__cur->_M_val), _M_get_key(__obj))) // 如果发现与链表中的某键值相同,就不插入,立刻返回 return pair<iterator, bool>(iterator(__cur, this), false); // 离开以上循环(或根本未进入循环)时,first指向bucket所指链表的头部节点 _Node* __tmp = _M_new_node(__obj); // 产生新节点 __tmp->_M_next = __first; _M_buckets[__n] = __tmp; // 令新节点成为链表的第一个节点 ++_M_num_elements; // 节点个数累加1 return pair<iterator, bool>(iterator(__tmp, this), true); } // 在不需要重建表格的情况下插入新节点。键值允许重复 template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All> typename hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>::iterator hashtable<_Val,_Key,_HF,_Ex,_Eq,_All> ::insert_equal_noresize(const value_type& __obj) { const size_type __n = _M_bkt_num(__obj); // 决定obj应位于#n bucket _Node* __first = _M_buckets[__n]; // 令first指向bucket对应链表头部 // 如果bucket[n]已被占用,此时first将不为0,于是进入以下循环,走过bucket所对应的整个链表 for (_Node* __cur = __first; __cur; __cur = __cur->_M_next) if (_M_equals(_M_get_key(__cur->_M_val), _M_get_key(__obj))) { // 如果发现与链表中的某键值相同,就马上插入,然后返回 _Node* __tmp = _M_new_node(__obj); // 产生新节点 __tmp->_M_next = __cur->_M_next; // 将新节点插入于目前为止之后 __cur->_M_next = __tmp; ++_M_num_elements; // 节点个数累加1 return iterator(__tmp, this); // 返回一个迭代器,指向新增节点 } // 进行至此,表示没有发现重复的键值 _Node* __tmp = _M_new_node(__obj); // 产生新节点 __tmp->_M_next = __first; // 将新节点插入于链表头部 _M_buckets[__n] = __tmp; ++_M_num_elements; // 节点个数累加1 return iterator(__tmp, this); // 返回一个迭代器,指向新增节点 } // 判知元素的落脚处(bkt_num) // 程序代码中许多地方都需要知道某个元素值落脚于哪一个bucket之内。此为hash函数的责任,SGI把这个任务包装了一层,先交给bkt_num() // 再由此函数调用hash function,取得一个可执行modulus(取模)运算的数值。这么做原因:有些元素类型无法直接拿来对hashtable的大 // 小进行模运算,如字符串const char *,此时需要做一些转换。 // 只接受键值 size_type _M_bkt_num_key(const key_type& __key) const { return _M_bkt_num_key(__key, _M_buckets.size()); } // 只接受实值(value) size_type _M_bkt_num(const value_type& __obj) const { return _M_bkt_num_key(_M_get_key(__obj)); } // 接受键值和buckets个数 size_type _M_bkt_num_key(const key_type& __key, size_t __n) const { return _M_hash(__key) % __n; } // 接受实值(value)和buckets个数 size_type _M_bkt_num(const value_type& __obj, size_t __n) const { return _M_bkt_num_key(_M_get_key(__obj), __n); } // 整体删除(clear)由于整个hash table由vector和link-list组合而成,故复制和整体删除,都需特征注意内存的释放问题 template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All> void hashtable<_Val,_Key,_HF,_Ex,_Eq,_All>::clear() { for (size_type __i = 0; __i < _M_buckets.size(); ++__i) { // 针对每一个bucket _Node* __cur = _M_buckets[__i]; while (__cur != 0) { // 将bucket list中的每一个节点删除掉 _Node* __next = __cur->_M_next; _M_delete_node(__cur); // buckets vector并未释放掉空间,仍保有原来大小 __cur = __next; } _M_buckets[__i] = 0; // 令bucket内容为null指针 } _M_num_elements = 0; // 令总节点个数为0 } // 复制操作 template <class _Val, class _Key, class _HF, class _Ex, class _Eq, class _All> void hashtable<_Val,_Key,_HF,_Ex,_Eq,_All> ::_M_copy_from(const hashtable& __ht) { // 先清除己方的buckets vector。这操作是调用vector::clear,将整个容器清空buckets.clear() _M_buckets.clear(); // 为己方的buckets vector保留空间,使与对方相同,如果己方空间大于对方,就不动,如果己方空间小于对方,就会增大。 _M_buckets.reserve(__ht._M_buckets.size()); // 从己方的buckets vector尾端开始,插入n个元素,其值为null指针 // 注意,此时buckets vector为空,所以所谓尾端,就是起头处 _M_buckets.insert(_M_buckets.end(), __ht._M_buckets.size(), (_Node*) 0); __STL_TRY { for (size_type __i = 0; __i < __ht._M_buckets.size(); ++__i) { // 针对buckets vector const _Node* __cur = __ht._M_buckets[__i]; // 复制vector的每一个元素(是个指针,指向hashtable节点) if (__cur) { _Node* __copy = _M_new_node(__cur->_M_val); _M_buckets[__i] = __copy; for (_Node* __next = __cur->_M_next; // 针对同一个buckets list,复制每一个节点 __next; __cur = __next, __next = __cur->_M_next) { __copy->_M_next = _M_new_node(__next->_M_val); __copy = __copy->_M_next; } } } _M_num_elements = __ht._M_num_elements; // 重新登录节点个数(hashtable的大小) } __STL_UNWIND(clear()); } // hashtable中的find函数 iterator find(const key_type& __key) { size_type __n = _M_bkt_num_key(__key); // 首先寻找落在哪一个bucket内 _Node* __first; for ( __first = _M_buckets[__n]; // 以下,从bucket list的头开始,一一比对每个元素的键值。对比成功就跳出 __first && !_M_equals(_M_get_key(__first->_M_val), __key); __first = __first->_M_next) {} return iterator(__first, this); } // hashtable中的count函数 size_type count(const key_type& __key) const { const size_type __n = _M_bkt_num_key(__key); // 首先寻找在哪一个bucket内 size_type __result = 0; // 以下,从bucket list的头开始,一一比对每个元素的键值。比对成功就累加1。 for (const _Node* __cur = _M_buckets[__n]; __cur; __cur = __cur->_M_next) if (_M_equals(_M_get_key(__cur->_M_val), __key)) ++__result; return __result; } // 哈希函数,下面定义为仿函数。大部分哈希函数中什么也没做,只是忠实返回原值。 template <class _Key> struct hash { }; 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; } };
参考文献
STL源码剖析——侯捷
STL源码