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.size(); ++i, ++ite)
		cout << *ite << ' ';
	cout << endl;
	//0 1 2 2 3 4 5 6 7 8 9 10 11 108 12 13 14 15 16 17 18 19 20 21 
	//22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
	//43 44 45 46 47 53 55 59 63
	cout << *(ite.find(2)) << endl;  //2
	cout << iht.count(2) << endl;  //1
	return 0;
}

通过上述这个实例,可以对于resize有更深的理解,对于元素操作也能又更好的的理解

7. hash functions

hash function是计算元素位置的函数,SGI将这项任务交给bkt_num函数,然后再调用下列提供的hash function,取得一个可以对hashtable进行模运算的值

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);
}

template <>
__STL_TEMPLATE_NULL struct hash<char*>
{
	size_t operator()(const char *s)  const { return __stl_hash_string(s); }
};
//...

猜你喜欢

转载自blog.csdn.net/qq_38790716/article/details/84680580