1、基本概念
hashtable是一种存储成对的键值和实值元素的字典结构。不同于红黑树的对数级的平均时间消耗,hashtable提供常数级的时间消耗。
2、散列函数
为了达到常数级的时间消耗,底层必须使用array这种连续空间的内存结构。想要存储元素,就需要将元素的键值映射成对应的索引值,实现这种功能的映射函数就称为散列函数(hash function)。散列函数的实现是很重要的,不仅影响着存储array的大小,还决定着存储array利用率等很多东西。
3、元素碰撞
使用散列函数将元素的键值映射到一定区域内的索引位置,不可避免会出现不同的的键值得到相同的索引位置,这种情况就称为元素碰撞。当发生碰撞时,就需要采取一定的策略解决碰撞问题。
3.1线性探测。
当发生元素碰撞时,循环向下一个位置进行查找,到达尾端就跳转到头部继续查找,直到找到一个可以插入的空余位置。只要array够大,必然可以找到一个插入位置,但花费的时间就很难计算了。同理,在查找元素时也可能会花费大量的时间。同时,在碰撞发生后占用了碰撞位置附件其他元素的位置,导致这一区域的碰撞概率大大增加,从而形成恶性循环,我们称这种情况为主集团(primary cloustering)。
3.2平方探测。
为了解决主集团问题,我们在元素碰撞发生后,第1次移动一个位置,第2次移动4个位置,以此类推。由于是平方探测,为了保证必然可以找到一个插入位置,我们必须设定array的大小为质数。当保证负载系数在0.5以下时,没插入一个新元素的探测次数不超过两次。
相对于线性探测,平方探测的计算消耗更加大,但我们可以对这一消耗进行优化。已知H(i)=H(0)+i*i(mod M),H(i-1)=H(0)+(i-1)*(i-1)(mod M),可得H(i)=H(i-1) + (2i -1)(mod M),通过上一个位置可快速计算出下一个查找位置。
当负载系数超过0.5时,我们需要找出下一个新的且足够大的质数,对array进行扩充。此时并不是直接拷贝,而是扫描原有array内的元素,将其重新映射到新的array内。
平方探测解决了主集团问题,但一样会造成某些区域的碰撞概率较高,从而形成次集团(secondary cloustering)。
3.3双散列
为了解决次集团,我们在元素碰撞发生后,使用另一个散列函数查找插入位置。第二个散列函数的选择至关重要,最好能保证所有的位置都可被探测到。当相对于平方探测,散列函数的计算是更加耗时,因而需要根据不同的情况进行具体的选择。
3.4开链
不同于前面的散列方法,开链的做法是将相同散列值的元素放到同一个list里,这样完全避免了碰撞的问题,但由于使用了链表进行元素的存储,需要花费一些额外的空间。使用开链,array的负载系数将大于1,对于空间的利用率更高。标准STL就是使用这一方法实现的hashtable。
4、实际应用
hash_set和hash_map不属于标准库,但很多版本的STL都实现散列的版本,以下是测试用例:
hash_set<int> set;
set.insert(3);
set.insert(196);
set.insert(1);
set.insert(389);
set.insert(194);
set.insert(387);
hash_set<int>::iterator iter1 = set.begin();
hash_set<int>::iterator iter2 = set.end();
for (;iter1 != iter2;++iter1)
{
cout<<*iter1<<" "; //3 387 196 1 389 194
}
cout<<endl;
这里需要注意的,由于hashtable的限制,有些数据类型是无法处理的,对应的hash_map也是无法处理的。例如设置类型为const char*时,不能使用缺省的equal_to(T),需要自己定义比较函数和哈希函数,以下为测试代码:
struct eqstr
{
enum
{ // parameters for hash table
bucket_size = 4, // 0 < bucket_size
min_buckets = 8 // min_buckets = 2 ^^ N, 0 < N
};
size_t operator()(const char* str) const
{
unsigned long h = 0;
for (;*str;++str)
{
h = 5*h + *str;
}
return size_t(h);
}
bool operator()(const char* str1,const char* str2) const
{
return strcmp(str1,str2) == 0;
}
};
typedef hash_map<const char*,int,eqstr> StrHashMap;
StrHashMap map2;
map2["a"] = 1;
map2["b"] = 3;
map2["c"] = 7;
map2["d"] = 4;
StrHashMap::iterator iter5 = map2.begin();
StrHashMap::iterator iter6 = map2.end();
for (;iter5 != iter6;++iter5)
{
cout<<(*iter5).first<<" "<<(*iter5).second<<endl;
/*a 1
b 3
c 7
d 4*/
}
cout<<endl;