STL hashtable

SGI版本的STL中的哈希表以及hash_map和hash_set都沒有被算入C++標準中的,所以在C++標準中的unorder_map和unorder_set纔是最標準化的,其中底層使用的hashtable和我這篇文章中講的hashtable區別不大,所以在文章的最後,我會對比一下SGI版本的哈希表和當下C++標準中的哈希表的區別

一、哈希表概述

哈希表同樣是一個鍵值對應一個實值,但是哈希表找到表中想要的實值並不需要像紅黑樹一般使用對數時間搜索,因爲哈希表中實值和鍵值是有一個對應函數的,這種函數被稱爲散列函數,通過實值可以求得鍵值。如果知道了鍵值,也不需要想紅黑樹一樣從根節點開始往下查找,而是可以像數組一樣直接找到鍵值對應的內存。其實也就是使用了連續內存的功勞。個人理解哈希表相比於紅黑樹,是用空間來換取時間。
在哈希表中有兩個問題比較核心,一個問題是鍵值和實值如何對應,也就是散列函數是何種形式,能夠讓散列表中的空間利用率更高,或者讓哈希表佔用空間更小。
第二個問題就是如何解決碰撞問題。如果不同的實值被散列函數映射到同一個鍵值,如何解決?
①散列函數
SGI的散列函數很簡單,就是實值%TableSize。
而其他常用的散列函數可以去網上搜一搜

②碰撞問題解決
在書中主要提到了三種解決方式,線性探測,二次探測和開鏈。

  • 線性探測

就是當發生碰撞時,就自動往後移一格,如果還碰撞,繼續往後移,直到沒有碰撞爲止。
以一個例子來看,注意這裏使用的散列函數就是實值%TableSize。
在這裏插入圖片描述

  • 二次探測

在這裏插入圖片描述

  • 開鏈

這是最常用的,也是SGI使用的方法,就是把相同鍵值的元素放到一起,放在一張鏈表上,形式如下:
其中把hashtable內的元素稱爲桶子。

在這裏插入圖片描述
注意以下所說的實值和鍵值,分別指用戶層的鍵值和和哈希表桶數組的索引值。如果指其他,會有特殊說明

二、SGI中哈希表源碼分析

①哈希表中每一個存放數據的節點的數據結構

template <class _Val>
struct _Hashtable_node
{
  _Hashtable_node* _M_next;
  _Val _M_val;
};

並沒有用使用list或者slist,而是單獨又創建了一個鏈表節點

②哈希表的迭代器
哈希表的迭代器時forward_iterator單向迭代器,只有++運算符,沒有–運算符。
迭代器中存儲了兩個成員變量

class _Hashtable_iterator
{
...
  typedef _Hashtable_node<_Val> _Node;
  _Node* _M_cur;//哈希表中一個元素的指針
  _Hashtable* _M_ht;//指向哈希表的桶數組的指針
...
}

哈希表迭代器中的++重載函數:

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 (!_M_cur) {
    size_type __bucket = _M_ht->_M_bkt_num(__old->_M_val);//找到節點元素對應的桶索引,沿桶數組找到第一個非零桶元素,該桶元素中指向的節點就是++的結果
    while (!_M_cur && ++__bucket < _M_ht->_M_buckets.size())
      _M_cur = _M_ht->_M_buckets[__bucket];
  }
  return *this;//可能會返回NULL,如果返回NULL,就是後面再也沒找到下一個值
}

③哈希表的數據結構

hasher                _M_hash;//散列函數(哈希映射函數)
key_equal             _M_equals;//判斷鍵值相同與否的函數
_ExtractKey           _M_get_key;//從節點中取出鍵值的函數,類似於紅黑樹的KeyofValue
vector<_Node*,_Alloc> _M_buckets;//桶數組,注意是用的vector實現的
size_type             _M_num_elements;//哈希表中元素個數

④桶數組的大小
在SGI版本中,桶數組的大小隻從28個質數當中去挑選,即便你選擇了桶數組的數目,仍然會改變爲最靠近的那28個質數,其實沒什麼用處。在C++標準中,就沒有這個限制了。

⑤桶數組的插入操作

pair<iterator, bool> insert_unique(const value_type& __obj)
{
  resize(_M_num_elements + 1);//判斷是否需要重建桶數組,需要就擴充
  return insert_unique_noresize(__obj);//插入元素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)
{
  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),
                                 _M_buckets.get_allocator());//新建一個桶數組,並且數組中每個元素都是0
      __STL_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];//這裏具體可過程以查看書260頁,我認爲都是繁瑣的指針操作而已,沒有什麼技術含量          
          }
        }
        _M_buckets.swap(__tmp);
      }
    }
  }
}
//這個插入操作也很簡單,就是先找到對應的鍵值,然後在那個桶下找是否有相同實值的元素,有就立刻返回,
//沒有就將元素obj插在鏈表的首節點
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);
}

而insert_equal只有最後一個插入函數不同,源碼如下:

//就是先找到對應的鍵值,然後在那個桶下找是否有相同實值的元素,有插入到相同節點下方,
//沒有就將元素obj插在鏈表的首節點
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);
  _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))) {
      _Node* __tmp = _M_new_node(__obj);
      __tmp->_M_next = __cur->_M_next;
      __cur->_M_next = __tmp;
      ++_M_num_elements;
      return iterator(__tmp, this);
    }

  _Node* __tmp = _M_new_node(__obj);
  __tmp->_M_next = __first;
  _M_buckets[__n] = __tmp;
  ++_M_num_elements;
  return iterator(__tmp, this);
}

⑥如何從實值找到鍵值

size_type _M_bkt_num(const value_type& __obj) const
{
  return _M_bkt_num_key(_M_get_key(__obj));
}//由實值找到對應的鍵值,這是用戶層的實值和鍵值,比如set中實值和鍵值是一樣的,map中鍵值是實值的first參數

size_type _M_bkt_num_key(const key_type& __key) const
{
  return _M_bkt_num_key(__key, _M_buckets.size());
}
//個人認爲下面這個函數纔是完整的散列函數,將用戶層的鍵值轉化成哈希表中桶數組的索引鍵值
//其中_M_hash就是hash functions,只不過SGI中的hash function很少,並且只給簡單的幾個類型提供函數,很多函數沒有提供函數,如果要使用,需要自定義。
size_type _M_bkt_num_key(const key_type& __key, size_t __n) const
{
  return _M_hash(__key) % __n;
}

三、SGI和C++標準中哈希表有什麼不一樣

①C++標準中桶數組的尺寸不再是質數,而是如下的源碼:

void _Check_size()
{	// grow table as needed
if (max_load_factor() < load_factor())

	{	// rehash to bigger table
	size_type _Newsize = bucket_count();

	if (_Newsize <  512)
		_Newsize *= 8;	// 小於512就是乘以8
	else if (_Newsize < _Vec.max_size() / 2)
		_Newsize *= 2;	// 大於512就是乘以2
	_Init(_Newsize);
	_Reinsert();
	}
}

②散列函數不一樣
源碼如下:


???由於看不到_Mask的值,我也不知道這個原理,有待理解


size_type _Hashval(const key_type& _Keyval) const
{	// return hash value, masked and wrapped to current table size
size_type _Num = ((_Traits&)*this)(_Keyval) & _Mask;
if (_Maxidx <= _Num)
	_Num -= (_Mask >> 1) + 1;
return (_Num);
}

③SGI模板中,只有幾種類型可以當鍵值,但在C++標準中,基本所有的類都可以當鍵值,也就是hash function補充更完整了

四、利用hashtable實現的容器

hash_set:鍵值和實值(這都是應用層的鍵值實值)是一樣的(實現機制和set一樣,都是identity函數)。是排列沒有順序的,鍵值不能重複。
hash_map:鍵值和實值是不一樣的(實現機制和map一樣,都是select1st函數),並且每個元素的數據都是pair。是排列沒有順序的,鍵值不能重複。
hash_multiset:鍵值可以重複。
hash_multimap:鍵值可以重複

在C++標準中對應的容器:
hash_set->unordered_set
hash_map->unordered_map
hash_multiset->unordered_multiset
hash_multimap->unordered_multimap

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章