1. 程式人生 > >STL原始碼剖析(五)hashtable

STL原始碼剖析(五)hashtable

文章目錄

1. hashtable概述

  • hashtable即我們在資料結構中所說的散列表,也叫雜湊表,在插入、刪除、搜尋等操作上具有“常數平均時間”的表現,而且這種表現是以統計為基礎,不需仰賴輸入元素的隨機性
  • 使用hash function將某一元素對映為一個“大小可接受的索引”,通過該索引找到在array中的位置,從而構成一個hashtable;使用hash function可能帶來一個問題:即不同的元素經過hash function的作用被對映到相同的位置,這樣就產生了碰撞,我們應該採取什麼方法來解決碰撞呢?
  • 解決碰撞的方法有許多,我們在這主要分析三種: 1.線性探測 2.二次探測3.開鏈法

1.1 線性探測

  • 1.利用hash function計算出某個元素的插入位置
  • 2.若該位置上的空間不可用,則循序往下尋找,直到找到一個可用空間為止

分析線性探測的表現:

  • 首先需做兩個假設:1)表格足夠大 2)每個元素能夠獨立
  • 在此假設情況下,最壞的情況是線性巡訪整個表格,平均情況則是巡訪一半表格,可這也與我們期望的常數時間相差甚遠
  • 線性探測還可能帶來主集團問題,主集團即平均插入成本的成長幅度遠高於負載係數的成長幅度

1.2 二次探測

  • 1.利用hash function計算出某個元素的插入位置H
  • 2.若該位置實際上已被使用,則依序嘗試H + 1 ^2、H + 2 ^2...H + i ^2,直到發現新空間

分析二次探測:

  • 二次探測是為了消除主集團,但卻可能造成次集團:兩個元素經hash function計算出來的位置若相同,則插入時所探測的位置也相同,形成某種浪費

1.3 開鏈法

  • 1.在每一個表格元素種維護一個list
  • 2.hash function為我們分配某一個list,然後在list上執行元素的插入、搜尋、刪除操作

SGI STL的hashtable正是採用這種做法實現的

2. hashtable的桶與節點

  • 稱hashtable表格中的元素為“桶”,是因為表格內的每個單元,涵蓋的不只是個節點,甚至可能是一“桶”節點
    在這裡插入圖片描述

  • hashtable的節點定義:

template <class Value>
struct __hashrable_node {
	__hashtable_node* next;
	Vlalue val;
};

STL並不以list或slist維護hashtable node,而是以vector製造的bucket來維護,至於為什麼用vector,是因為需要動態擴充能力

3. hashtable迭代器

  • hashtable迭代器的定義(僅部分原始碼):
template <class Value, class Key, class HashFcn, 
	class ExtractKey, class EqualKey, class Alloc>
struct __hashtable_iterator {
	//...
	typedef __hashtable_node <Value> node;
	typedef forward_iterator_tag  iterator_category;  //hashtable沒有後退操作
	//...
	node* cur;   //迭代器所指節點
	hashtable* ht;  //連線容器
	
	//建構函式
	__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 V, class K, class HF, class ExK, class EqK, class A>
__hashtable_iterator<V, K, HF, ExK, EqK, A>&
__hashtable_iterator<V, K, HF, ExK, EqK, A>::operator++()
{
	const node* old = cur;
	cur = cur->next;  //如果存在節點則就是該cur,不存在節點則進入if流程,找到下一個bucket
	if (!cur) {  
		size_type bucket = ht->bkt_num(old->val);   //找到當前值所對應的桶
		while (!cur && ++ bucket < ht->buckets.size())
			cur = ht->buckets[bucket];  //令cur指向下一個bucket
	}
	return *this;
}
template <class V, class K, class HF, class ExK, class EqK, class A>
__hashtable_iterator<V, K, HF, ExK, EqK, A>&
__hashtable_iterator<V, K, HF, ExK, EqK, A>::operator++(int)
{
	iterator tmp = *this;
	++*this;
	return tmp;
{

4. hashtable資料結構

  • 在下面定義中,可以看到buckets聚合體以vector完成:
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_allloc<node, Alloc> node_allocator;
	vector<node*, Allloc> buckets;    //以buckets維護節點
	size_type num_elements;     //節點個數
public:
	size_type bucket_count()  const  { return buckets.size(); }
	...
};


value:節點的實值型別
Key:節點的鍵值型別
HashFcn:hashfunction的函式型別
ExtractKey:從節點中取出鍵值的方法
E權力Key: 判斷鍵值相同與否的方法
Alloc:空間配置器
  • SGI STL以質數來設計表格大小,並且先將28個質數計算好,已備隨時訪問,同時提供一個函式,用來查詢在這28個函式中“最接近並大於或等於n的那個質數”
static const int __stl_num_primes = 28;
static const unsigned long __stl_prime_list[__stl_num_primes] = 
{
	53,		97,		193,	389,	769,
	1543,	3079,	6151,	12289,	24593,
	49157,	98317,	196613,	393241,	786433,
	1572869,	3145739,	6291469,	12582917,	25165843,
	50331653,	100663319,	201326611,	402653189,	805306457,
	16110612741,	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 + __stl_num_primes;
	const unsigned long* pos = lower_bound(first, last, n);
	return pos == last ? *(last-1): *pos;
}
//最大buckets數
size_type max_bucket_count() const 
{
	return __stl_prime_list[__stl_num_primes - 1];  }

5. hashtable構造與記憶體管理

  • 節點配置與節點釋放函式:
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)
{
	destroy(&n->val);
	node_alllocaor::deallocate(n);
}
  • hashtable構造:
hashtable(size_type n, const HashFcn& hf, const EqualKey& eql)
	: hash(hf), equals(eql), get_key(ExtractKey()), num_elements(0)
{
	initialize_buckets(n);
}
void initialize_buckets(size_type n)
{
	buckets.reserve(n_bckets);
	buckets.insert(buckets.end(), n_buckets, (node*) 0);
	num_elements = 0;
}
  • 插入元素:分為兩種,insert_unique與insert_equal
// 插入操作,不允許重複
  pair<iterator, bool> insert_unique(const value_type& obj)
  {
   	  resize(num_elements + 1);
  	  return insert_unique_noresize(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 = bkt_num(obj);
	  node*  first = buckets[n];
 
  // 如果已經存在則直接返回
  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);
 
  // 插入新節點
	 node* tmp = new_node(obj);  //產生新節點
  	 tmp->next = first;        //將新節點插入連結串列頭部
  	 buckets[n] = tmp;
 	 ++num_elements;      //節點個數累計加1
  	 return pair<iterator, bool>(iterator(tmp, this), true); ///返回一個迭代器,指向新增節點
}


 //插入操作,允許重複
  iterator insert_equal(const value_type& obj)
  {
   	    resize(num_elements + 1);
    	return insert_equal_noresize(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_equal_noresize(const value_type& obj)
{
 	  const  size_type  n = bkt_num(obj);
	  node*  first = buckets[n];
 
  // 如果已經存在則直接插入並返回
  for (node* cur = first; cur;  cur = cur->_next)
    	if (equals(get_key(cur->val), get_key(obj))) {
    		 node* tmp = new_node(obj);  //產生新節點
  			 tmp->next = first;        //將新節點插入連結串列頭部
  			 buckets[n] = tmp;
 			 ++num_elements;      //節點個數累計加1
  			 return pair<iterator, bool>(iterator(tmp, this), true); ///返回一個迭代器,指向新增節點
 		}
    		  
 
  // 表示沒有發現新節點
	 node* tmp = new_node(obj);  //產生新節點
  	 tmp->next = first;        //將新節點插入連結串列頭部
  	 buckets[n] = tmp;
 	 ++num_elements;      //節點個數累計加1
  	 return pair<iterator, bool>(iterator(tmp, this), true); ///返回一個迭代器,指向新增節點
}


//判斷是否需要重建表格
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 = buckets.size();
 	 // 超過原來表格的大小時才進行調整
  	 if (__num_elements_hint > old_n) {
    		// 新的表格大小
   		 const size_type  n = next_size(num_elements_hint);
 		   // 在邊界情況下可能無法調整(沒有更大的素數了)
   		 if (n > old_n) {
      		vector<node*, All> tmp(n, (node*)(0),
     	    __STL_TRY {
       	 // 填充新的表格
     		   for (size_type bucket = 0;bucket < old_n; ++bucket) {
     	     		  node* first = buckets[bucket];  //指向節點所對應的起始節點
       	       		  while (first) {
       	       		  			//以下找出節點落在哪一個新bucket內
             				   size_type __new_bucket = bkt_num(first->val, n);
             				   	//令舊節點指向對應序列的下一個節點
         					   buckets[bucket] = first->next;
         					   //將當前節點插入到新bucket內
          				       first->next = tmp[new_bucket];
               				   tmp[new_bucket] = first;
               				   //準備處理下一個節點
          	   				   first = buckets[bucket];
       				   }
     		  }
        // 通過swap交換
  	      buckets.swap(tmp); 
     	   }
   	   }
    }
}
  • 判知元素的落腳處:
size_type bkt_num(const value_type& obj, size_t n)  const {
	return bkt_num_key(get_key(obj), n);
}
size_type bkt_num(const value_type& obj)  const 
{
	return bkt_num_key(get_key(obj));
}
size_type bkt_num_key(const value_type& key)  const {
	return bkt_num_key(key, buckets.size());
}
size_type bkt_num_key(const value_type& key,szie_t n)  const {
	return hash(key) % n;
}

6. 元素操作

操作 功能
copy_from 複製整個hashtable
clear hashtable整體刪除
find 搜尋鍵值為key的元素
count 計算鍵值為key的元素個數
  • 一個例項:
#include <iostream>
#include <hash_set>
#include <algorithm>
using namespace std;

int main()
{
	//指定保留50個buckets
	hashtable<int, int, hash<int>, 
	identify<int>, equal_to<int>, alloc>  iht(50, hash<int>(), equal_to<iny>());
	
	cout << iht.size() << endl; //0
	cout << iht.bucket_count() << endl;  //53,滿足要求的最小質數
	cout << iht.max_bucket_count() << endl; //4294967291
	
	iht.insert_unique(59);
	iht.insert_unique(63);
	iht.insert_unique(108);
	iht.insert_unique(2);
	iht.insert_unique(53);
	iht.insert_unique(55);
	cout << iht.size() << endl;  //6
	
	//宣告迭代器
	hashtable<int, int, hash<int>, 
	identify<int>, equal_to<int>, alloc>::iterator  ite = iht.begin();
	for (int  i = 0; i < iht.size(); ++i, ++ite)
		cout << *ite << ' ';  //53 53 2 108 59 63
	cout << endl;
		
	for (int  i = ); i < iht.bucket_count() ; ++i) {
		int n = iht.elems_in_bucket(i);
		if (n != 0)
			cout << "bucket[" << i << "] has " << n << " elems." << endl;
	}
	//bucket[0] has 1 elems
	//bucket[2] has 3 elems
	//bucket[6] has 1 elems
	//bucket[10] has 1 elems
	
	//插入48個元素
	for (int  i = 0; i <= 47; ++i)
	{
		iht.insert_equal(i);   
	}
	cout << iht.size() << endl;   //54
	cout << iht.bucket_count() << endl;  //大於53,更改buckets個數為大於53的質數,97
	for (int  i = 0; i < iht.bucket_count(); ++i){
		int n = iht.elems_in_bucket(i);
		if (n != 0)
			cout << "bucket[" << i << "] has " << n << " elems." << endl;
	}
	//列印結果:bucket[2]和bucket[11]的節點個數為2
	//其餘的bucket[0]~bucket[47]的節點個數為1
	//此外,bucket[53],[55],[59],[63]的節點個數為1
	ite = ite.begin();
	for (int  i = 0; i < iht