由于自己最近在看STL中的hash_table,被它精巧的设计所折服。无论是对桶子个数的确定,对链表的维护方式,以及判断元素在哪个桶子里等等方法都考虑到了方方面面。所以自己写了篇总结。
hash_table存储数据的特性
二叉树,AVL树,RB_tree等数据结构各有各的用途,并且具有对数平均时间,但之所以有这样高的效率取决于输入的数据有足够的随机性,那么hash_table这种数据结构在插入,删除,搜寻等操作上也具有常数平均时间,并且不需要依赖于数据的随机性。
hash冲突
hash_table的中心思想是利用某种映射函数,将大数映射为小数,这种映射函数称为散列函数,通常我们用的散列函数是保留余数法。在想想看,这种将大数映射为小数可能导致不同的数被映射到了相同的位置,这种情况无法避免,因为元素大于桶子的个数。这就是所谓ie的hash冲突。
解决hash冲突的方法
当我们决定用哪种方法时,必须要明白我们存储对象的数据量,数据的特性(例如数据是重复的多还是杂乱无章)
首先对数据进行分析,然后在决定。
(1)线性探测法
如果我们需要存储的数据相比较桶子的个数来说不是很多的情况(也就是所谓的负载系数不是很大)适合用线性探测和二次探测,如果负载系数比较大,那么用开链法效率比较高。
线性探测法找数据位置的方法是:首先利用hash函数计算出数据存放的位置下标,如果该位置已经由数据存入,那么就依次的向后找直到找到没有存放数据的下标,然后将数据放入此下标位置。这样看来如果数据重复性比较高那么这样的方法效率不是很高,因为要不断的解决冲突问题。
(2)二次探测法
二次探测的这种方法解决了线性探测容易导致的问题(就是数据重复的多,那么为了找到合适的桶子需要不断的去解决hash冲突问题)
二次探测找数据位置的方法:如果计算出元素的位置为H,如果该位置已经被使用,那么就依次的向后H+1^2, H+2^2, H+3^2, H+4^2这样的方法去找到合适的位置。(这里注意利用二次探测法找合适的位置其算法过于复杂,要进行加法,乘法比较耗时,如果将
Hi = H0 + i^2
Hi - 1 = H0 + (i-1)^2
改为
Hi - Hi-1 = i^2 - (i-1)^2
Hi = Hi-1 + 2i-1(这里的2i乘法换为左移即可)
这样能提高一点效率
(1)(2)的方法通常情况下比较慢,但是如果我们存入的元素的负载系数小于0.5,并且设置的桶子的个数是质数的情况下,采用以上两种方法可以达到每插入一个新元素所需要的探测次数不大于2。
(3)开链法
开链法是比较常用的方法,通常情况小,我们存储的元素是远远超过桶子的个数的,所以对于这种负载系数大于1的情况通常采用开链法。源码会详细介绍这种方法。
以下是自己将STL的hash_table源码的一部分整理,不完整,主要提取出了自己认为其设计的精妙之处
精妙之处包括:hash_table桶子节点个数的确定,利用vector对桶子进行维护以及扩充,专门有针对字符字符串的哈希函数的处理。
#include <stl_algobase.h>
#include <stl_alloc.h>
#include <stl_construct.h>
#include <stl_tempbuf.h>
#include <stl_algo.h>
#include <stl_function.h>
#include <stl_vector.h>
#include <stl_hash_fun.h>
template<class Value>
struct __hashtable_node{
__hashtable_node *next;
Value val;
};
template<class Value, class Key, class HashFcn,
class ExtractKey, class EqualKey, class Alloc=alloc>
class hashtable{
public:
typedef Key key_type;
typedef Value value_type;
typedef HashFcn hasher;
typedef EqualKey key_equal;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef value_type *pointer;
typedef value_type &reference;
private:
hasher hash;
key_equal equals;
ExtractKey get_key;
//自定义hash_table节点大小的空间配置器
typedef __hashtable_node<Value> node;
typedef simple_alloc<node, Alloc> node_alocator;
//STL中hash_table桶节点是用vector来维护的,这样方便动态扩充
vector<node*, Alloc> buckets;
size_type num_elements;
public:
//hash_table源码中有重载的构造函数,这里我选了其中一种。
hashtable(size_type n, const HashFcn& hf, const EqualKey& eql,
const ExtractKey& ext)
:hash(hf),equals(eql),get_key(ext),num_elements(0)
{
initialize_buckets(n);
}
~hashtable(){clear();}
private:
<strong>//STL中如何确定hash_table的桶子的个数</strong>
/
<span style="color:#006600;">//STL中的hash_table是将桶子的个数设置为质数的,同时源码中先将28各质数计算出来
//以备随时访问。STL用这种方法的好处是将桶子的个数取为质数这样采用保留取余法来
//分配元素的时候比较均匀,这样不容易导致有的桶子后面的链表很长,有的却没有链表
//影响效率。</span>
int __stl_num_prime_last = 28;
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,
1610612741, 3221225473ul, 4294967291ul
};
void initialize_buckets(size_type n)
{
//这里next_size(n)是根据用户申请的桶子的个数n来找到最接近n的质数。
const size_type n_buckets = next_size(n);
buckets.reserve(n_buckets);
//将找到的最接近n的质数的空间全部填充为0
buckets.insert(buckets.end(), n_buckets, (node *)0);
num_elements = 0;
}
size_type next_size(size_type n)const
{
return __stl_next_prime(n);
}
//__stl_next_prime()函数找到桶子的个数n的最近的质数利用的是
<span style="color:#006600;">//算法lower_bound(),这个算法作用是试图在已经排序的[first,last)
//范围内找到n,如果有n则返回指向n的迭代器,如果范围内没有n则返回
//指向第一个不小于n的值的位置的迭代器</span>
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 ;
}
<strong>//STL中如何判断元素的落脚处</strong>
//
//自己写的判断hash_table元素的落脚处总是让元素%桶子的个数,但是STL考虑了
//有些元素类型不能直接拿来对桶子个数进行模运算,例如字符字符串。因此将
//hash()函数进行了封装bkt_num_key(),如果传入的元素的类型为整数型别则
//hash()函数仅仅返回元素值,如果元素型别是字符字符串则要调用
//__stl_hash_string()函数。
size_type bkt_num(const value_type& obj, size_t n)const
{
return bkt_num_key(get_key(obj), n);
}
size_type bkt_num_key(const value_type& obj, size_t n)
{
return hash(key) % n;
}
//这里的hash函数在STL的<stl_hash_fun.h>自己把这个函数写在这里是为了更好
//的分析。
<span style="color:#006600;">//针对char int short long等整数型别,这里的hash函数什么也没有做,只是返
//回原值,但是对于字符字符串(const char *)设计了一个转换函数。
//对这里的字符串哈希函数__stl_hash_string()的介绍
//字符串哈希函数的处理有多种不同的版本。例如SDBM,RS,PJW,ELF,BKDR等版本</span>
template<class Key> struct hash{};
inline size_t __stl_hash_string(const char *)
{
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<int>
{
size_t operator()(int x)const{return x;}
};
template<>
struct hash<char>
{
size_t operator()(char x)const{return x};
};
<strong>//由于插入的元素太多,需要重建vector</strong>
/
<span style="color:#006600;">//pair对组,通常一个函数只能返回一个值,但是要返回多个值就用对组,这里
//pair<iterator, bool>意思是返回两个值一个是指向插入元素的位置的迭代器</span>
//一个是扩充vector是否成功。
pair<iterator, bool>insert_unique(const value_type& obj)
{
//插入元素前先判断需不需要重建vector
resize(num_element + 1);
return insert_unique_noresize(obj);
}
//这里的扩充函数先找到需要扩充的桶子的个数,然后将就旧vector所维护的
//桶节点和桶节点所管理的链表转移到新的vector上,这里转移过程比较复杂
//要仔细理解
template<class V, class K, clss HF, class Ex, class Eq, class A>
void hashtable<V, K, HF, Ex, Eq, A>resize(size_type num_element_hint)
{
const size_type old_n = buckets.size();
if(num_element_hint > old_n){
const size_type n = next_size(num_element_hint);
if(n > old_n){
vector<node *, A> tmp(n,(node *) 0);
for(size_type bucket = 0; bucket < n; ++bucket){
node *first = buckets[bucket];
while(first){
size_type new_bucket = bkt_num(first->val, n);
//转移的过程
buckets[bucket] = first->next;
first->next = tmp[new_bucket];
tem[new_bucket] = first;
first = buckets[bucket];
}
}
<span style="color:#006600;">//这里调用的swap()函数将新的vector的空间大小换成旧的vector
//的空间大小,将旧的vector空间大小换成新的vector的空间的大小
//这样是的旧的buckets[]有了新的风貌,而生成的新的tmp会被系统
//进行资源的回收。</span>
buckets.swap(tmp);
}
}
}
template <class V, class K, class HF, class Ex, class Eq, class A>
pair<typename hashtable<V, K, HF, Ex, Eq, A>::iterator, bool>
hashtable<V, K, HF, Ex, Eq, A>::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;
return pair<iterator, bool>(iterator(tmp, this), true);
}
};