本文主要分析g++ stl中哈希表的實現方法。stl中,除了以紅黑樹爲底層存儲結構的map和set,還有用哈希表實現的hash_map和hash_set。map和set的查詢時間是對數級的,而hash_map和hash_set更快,能夠達到常數級,不過哈希表須要更多內存空間,屬於以空間換時間的用法,並且選擇一個好的哈希函數也不那麼容易。node
1、 哈希表基本概念數組
哈希表,又名散列表,是根據關鍵字直接訪問內存的數據結構。經過哈希函數,將鍵值映射轉換成數組中的位置,就能夠在O(1)的時間內訪問到數據。舉個例子,好比有一個存儲家庭信息的哈希表,經過人名查詢他們家的信息,哈希函數爲f(),數組info[N]用於存儲,那麼張三家的信息就在info[f(張三)]上。由此,不需比較即可知道張三家裏有幾口人,人均幾畝地,地裏有幾頭牛。很快對不對,不過有時候會出現f(張三)等於f(李四)的狀況,這就叫哈希碰撞。碰撞是由哈希函數形成的,良好的哈希函數只能減小哈希碰撞的機率,而不能徹底避免。這就須要處理衝突的方法,通常有兩種:緩存
1. 開放定址法數據結構
先存儲了張三的信息,等到存李四的信息時發現,這位置有記錄了,怎麼辦,假如李四這人不愛跟張三一塊湊熱鬧,就從新找了個位置。這個方法就多了,他能夠放在後面一個位置,若是這位置還有的話,就再放後面一個位置,以此類推,這就叫線性探測;他可能嫌一個個位置找太慢了,因而就按照12,22,32的間隔找,這就叫平方探測;或者再調用另一個哈希函數g()獲得新的位置,這就叫再哈希…函數
2. 開鏈法this
若是李四這人嫌從新找個坑太麻煩了,願意和張三放在一塊兒,經過鏈表鏈接,這就是開鏈法。開鏈法中一個位置可能存放了多個紀錄。spa
一個哈希表中元素的個數與數組的長度的比值稱爲該哈希表的負載因子。開放定址法的數組空間是固定的,負載因子不會大於1,當負載因子越大時碰撞的機率越大,當負載因子超過0.8時,查詢時的緩存命中率會按照指數曲線上升,因此負載因子應該嚴格控制在0.7-0.8如下,超過期應該擴展數組長度。 開鏈法的負載因子能夠大於1,插入數據的指望時間O(1),查詢數據的指望時間是O(1+a),a是負載因子,a過大時也須要擴展數組長度。設計
2、 stl哈希表結構指針
stl採用了開鏈法實現哈希表,其中每一個哈希節點有數據和next指針,code
template<class _Val> struct _Hashtable_node { _Hashtable_node* _M_next; _Val _M_val; };
哈希表定義時要指定數組大小n,不過實際分配的數組長度是一個根據n計算而來的質數,
void _M_initialize_buckets(size_type __n) { const size_type __n_buckets = _M_next_size(__n); _M_buckets.reserve(__n_buckets); _M_buckets.insert(_M_buckets.end(), __n_buckets, (_Node*) 0); _M_num_elements = 0; } inline unsigned long __stl_next_prime(unsigned long __n) { const unsigned long* __first = _Hashtable_prime_list<unsigned long>::_S_get_prime_list(); const unsigned long* __last = __first + (int)_S_num_primes; const unsigned long* pos = std::lower_bound(__first, __last, __n); return pos == __last ? *(__last - 1) : *pos; }
從 prime_list中找到第一個大於n的數,list是已經計算好的靜態數組,包含了29個質數.
template<typename _PrimeType> const _PrimeType _Hashtable_prime_list<_PrimeType>::__stl_prime_list[_S_num_primes] = { 5ul, 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 };
好比指定哈系表長度爲50,最後實際分配的是53,指定長度爲100,最後實際分配的長度是193.能夠發現__stl_prime_list數組中,後一個數老是大約等於前一個數的兩倍,這不是巧合。當插入數據時,若是全部元素個數大於哈希表數組長度,爲了使哈希表的負載因子永遠小於1,就必須調用resize從新分配,增加速度跟vector差很少,每次分配數組長度差很少翻倍。
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) { 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_type __tmp(__n, (_Node*)(0), _M_buckets.get_allocator()); __try { for (size_type __bucket = 0; __bucket < __old_n; ++__bucket) { _Node* __first = _M_buckets[__bucket]; while (__first) { size_type __new_bucket = _M_bkt_num(__first->_M_val, __n); _M_buckets[__bucket] = __first->_M_next; __first->_M_next = __tmp[__new_bucket]; __tmp[__new_bucket] = __first; __first = _M_buckets[__bucket]; } } _M_buckets.swap(__tmp); } __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_exception_again; } } } }
每次新插入的元素都放在鏈表的第一個節點前面。
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); _Node* __first = _M_buckets[__n]; 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); _Node* __tmp = _M_new_node(__obj); __tmp->_M_next = __first; _M_buckets[__n] = __tmp; ++_M_num_elements; return pair<iterator, bool>(iterator(__tmp, this), true); }
3、 哈希函數
哈希函數用於計算元素在數組中的位置, M_bkt_num_key簡單封裝了哈希函數,與數組長度取餘獲得元素在數組中的位置。
size_type _M_bkt_num_key(const key_type& __key, size_t __n) const { return _M_hash(__key) % __n; }
_M_hash都定義在<hash_func.h>中,所有是仿函數。除了對字符串設計了一個轉換函數以外,其餘都是返回原值:
inline size_t __stl_hash_string(const char* __s) { unsigned long __h = 0; for ( ; *__s; ++__s) __h = 5 * __h + *__s; return size_t(__h); } template<> struct hash<char*> { size_t operator()(const char* __s) const { return __stl_hash_string(__s); } }; template<> struct hash<const char*> { size_t operator()(const char* __s) const { return __stl_hash_string(__s); } }; template<> struct hash<char> { size_t operator()(char __x) const { return __x; } }; template<> struct hash<int> { size_t operator()(int __x) const { return __x; } }; template<> struct hash<unsigned int> { size_t operator()(unsigned int __x) const { return __x; } }; template<> struct hash<long> { size_t operator()(long __x) const { return __x; } }; ……