hashtable
前面介绍了二叉搜索树、平衡二叉搜索树、以及广泛使用的平衡二叉搜索树:红黑树。
RB-tree红黑树不仅在树形的平衡在表现不错,在效率表现和实现复杂度上也保持相当的平衡,所以运用甚广。
二叉搜索树具有对数平均时间的表现,但输入数据需具有足够的随机性;hashtable散列表在插入、删除、搜寻等操作上也具有常数平均时间表现,且这种表现是以统计为基础,不需要仰赖输入元素的随机性。
hashtable散列表概述
hash table可提供对任何有名项的存取操作和删除操作,因操作对象是有名项,所以hashtable也可视为一种字典结构。这种结构的用意在于提供常数时间之基本操作,类似stack和queue。
举例,如果所有的元素都是16-bits且不带正负号的整数,范围0~65535,简单运用一个array可以满足常数时间的基本操作。配置一个array A,有65535个元素,索引号码0~65535,初值为0,每个元素代表相应元素的出现次数,插入元素i,执行A[i]++,删除元素i执行A[i]- -,搜索元素i,就检查A[i]是否为0,以上每个操作都是常数时间。额外的负担是array的空间和初始化时间。
但是这种解法存在现实问题,如果元素格外多,则array的大小就是个问题。
如何避免使用大的荒谬的array,使用映射函数,将大数映射为小数,将某一元素映射到一个大小可接受的索引,这样的函数称为hash function(散列函数),使用hash function会带来一个问题:可能有不同的元素被映射到相同的位置,这便是所谓的碰撞问题,解决碰撞问题的方法有:线性探测(linear probing)、二次探测(quadratic probing)、开链(separate chaining)等。
线性探测(linear probing)
负载系数(loading factor),意指元素个数除以表格大小,负载系数永远在0~1之间,除非采用开链策略。
当hash function计算出某个元素的插入位置,而该位置上的空间已不再可用时,循序往下寻找(如果到达尾端就绕到头部继续寻找),直到找到一个可用空间为止。进行元素搜寻操作时也一样,如果hash function计算出来的位置上元素值与我们搜寻的目标不符,就循序往下一一寻找,直到找到吻合者,或者遇上空格元素。而元素的删除必须采用惰性删除,只标记删除记号,实际删除操作则待表格重新整理时再进行,这是因为hash table中的每个元素不仅表述它自己,也关系到其他元素的排列。
欲分析线性探测的表现,需要两个假设,1表格足够大,2每个元素都够独立。在此假设下,最坏情况是线性巡访整个表格,平均情况是巡访一半表格。但实际情况更糟糕,举例说明:
新元素第一次插入位置#9,第二次插入位置#8,如果第三个元素插入位置为#9则需要绕到头部插入位置#0,第四个元素如果插入位置为#0、8、9则需要插入位置#1,之后的新元素经过hash function计算后的位置若为#3~#7可直接插入,否则只能插入位置#2。这样的现象在hashing过程中称为主集团primary clustering,之后的插入操作可能会不断解决碰撞问题,主集团也越来越大。
二次探测
二次探测主要解决主集团问题,如果hash function计算出新元素的位置为H,而该位置已被使用,则依序尝试H+1²,H+2²,H+3²,H+4²,…,H+i²,而不是像线性探测依序尝试H+1,H+2,H+3,H+4,…,H+i。
二次探测也会带来一些问题:
线性探测法每次探测必须是不同的位置,二次探测法能否保持如此,能否保证没有位置时插入X一定成功。
二次探测法的运算过程较线性探测法复杂,是否带来执行效率问题。
不论线性探测或二次探测,负载系数过高时,表格能否动态增长。
如果将表格大小设置为质数,并且保持负载系数小于0.5,超过0.5就重新配置整理表格,可保证每插入一个新元素需要的探测次数不超过2。
复杂度的问题,线性探测需要一个加法(加1),一个测试(是否需要回头),以及一个可能的减法(绕转回头)。二次探测需要一个加法(从i-1到i),一个乘法(i²)另一个加法,一个mod运算。
Hi = H0 + i² (mod M)
Hi-1 = H0 + (i-1)² (mod M)
整理
Hi - Hi-1 = i² - (i-1)² (mod M)
Hi = Hi-1 + 2i-1 (mod M)
以前一个H值计算下一个H值,不需要执行二次方的乘法,乘以2可以位移位快速完成,mod运算可证明为非需要。
最后一个array动态增长的问题,扩充表格,需找到一个新的大约两倍质数的表格,表格重建不可能原封不动地拷贝,必须检验旧表格中的所有元素,计算其在新表格中的位置,再插入到新表格中。
二次探测可以消除主集团,却可能造成次集团:两个元素经hash function计算出来的位置若相同,则插入时所探测的位置也相同,形成某种浪费。消除次集团如复式散列。
开链
开链法是在每一个表格元素中维护一个list,hash function分配一个list,在list中执行元素的插入、搜索、删除等操作。虽然针对list的搜索是线性操作,但list够短可保证速度。
使用开链手法,表格的负载系数将大于1。
STL的hashtable便是采用这种做法。
hash_set
STL set以RB-tree为底层机制,hash_set是以hashtable为底层机制,封装hashtable的接口。
RB-tree有自动排序功能而hashtable没有,即set可自动排序hash_set不可。set和hash_set都可完成快速搜索元素。
hash_map
hash_map以hashtable为底层机制。map和hash_map都可完成根据键值快速搜索元素功能,hash_map使用方式与map完全相同,但不可自动排序。
hash_multiset
hash_multiset特性与multiset完全相同,区别为hash_multiset使用hashtable底层机制,不可自动排序。
hash_multiset与hash_set唯一区别是元素插入操作,hash_multiset使用hashtable的insert_equal(),hash_set使用insert_unique()。
hash_multimap
hash_multimap与multimap特性完全相同,区别为hash_multimap使用hashtable底层机制,不可自动排序。
hash_multimap与hash_map实现上的唯一区别为插入操作,hash_multimap使用hashtable的insert_equal(),hash_map使用insert_unique()。
hash_functions
<hash_fun.h>定义中有现成的hash_functions,都是仿函数,在下一篇hashtable设计中计算元素位置_M_bkt_num,调用仿函数,取得可以对hashtable模运算的值。
针对char, int, long等整型,仿函数返回原值,对于字符串设计了一个转换函数。
hashtable无法处理以下类型以外的元素,如string, double, float,需要自定义hash_function。
namespace __gnu_cxx _GLIBCXX_VISIBILITY(default)
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION
using std::size_t;
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<>
struct hash<char*>
{
size_t
operator()(const char* __s) const
{ return __stl_hash_string(__s); }
};
template<>
struct hash<const char*>
{
size_t
operator()(const char* __s) const
{ return __stl_hash_string(__s); }
};
template<>
struct hash<char>
{
size_t
operator()(char __x) const
{ return __x; }
};
template<>
struct hash<unsigned char>
{
size_t
operator()(unsigned char __x) const
{ return __x; }
};
template<>
struct hash<signed char>
{
size_t
operator()(unsigned char __x) const
{ return __x; }
};
template<>
struct hash<short>
{
size_t
operator()(short __x) const
{ return __x; }
};
template<>
struct hash<unsigned short>
{
size_t
operator()(unsigned short __x) const
{ return __x; }
};
template<>
struct hash<int>
{
size_t
operator()(int __x) const
{ return __x; }
};
template<>
struct hash<unsigned int>
{
size_t
operator()(unsigned int __x) const
{ return __x; }
};
template<>
struct hash<long>
{
size_t
operator()(long __x) const
{ return __x; }
};
template<>
struct hash<unsigned long>
{
size_t
operator()(unsigned long __x) const
{ return __x; }
};
_GLIBCXX_END_NAMESPACE_VERSION
} // namespace