【Data Structure & Algorithm】HashSet, HashMap, HashTable原理详解

HashSet, HashMap, HashTable原理详解


关于这几个结构重点需要区分的是:

1. HashMapHashSet允许值为null,而HashTable不行。

2. HashMapHashSet是非同步的,HashTable是同步的。

3. Set, Map底层为RB-Tree,有自动排序功能;HashSet, HashMap底层为HashTable,不有自动排序功能。

4. 关于Set(1) HashSet:速度快,常数时间O(1),不可以自动排序; (2)TreeSet(普通Set):速度慢,对数时间O(lgn),但可以自动排序。

 

系统中各个类关系如下:


1. HashTable概述

HashTable可以被视作为一种字典结构,可以提供常数时间基本操作,正如stack和queue一样,初始化建表时间较多,但是查找起来时间效率会大大提高。

HashTable解决“碰撞”问题的方法有线性探测(Linear probing)、平方探测(quadratic probing)、开链法(separate chaining)。每种方法的效率不同,这与array的装填因子α有关。

(1)线性探测(Linear probing)

装填因子α是只元素个数除以表格大小,范围在0~1之间。当hash function计算出某个元素的插入位置,而该位置上的空间已经不再可用时,便逐一往下寻找,直到找到一个可用空间为止,但是在表格搜索的过程之中所要耗费大量的时间这个问题上,就有文章可做了。故而后面会引入平方探测法(quadratic probing),该方法会解决数据插入过于集中的问题,并且大大缩短搜寻时间,提高时间效率。

至于元素删除,必须采用惰性删除(lazydeletion),就是指标记删除记号,实际删除操作则待表格重新整理时再进行。因为hash table中的每一个元素不仅表述它自己,也关系到其他元素的排列。

假设表格足够大、每个元素都能够独立,则最坏情况是线性寻访整个表格,平均情况则是寻访一半表格,但是这已经和之前所期望的常数时间差很远了,而实际情况更糟糕。原因就在于每个元素不是完全独立的。这很清楚地凸显了一个问题:平均插入成本的成长幅度远高于装填因子的成长幅度,这样的现象在hashing过程中称为主集团(primary clustering)。此时已经有一大团已经被用过的方格,插入操作极有可能在主集团形成区域中进行,不断解决碰撞问题,最后找到空间,但是却有助长了主集团的面积。


(2)平方探测(quadratic probing)

又称为二次探测,主要用来解决主集团的问题。如果hash function计算出新元素的位置为H,而该位置实际上已被使用,那么就依序尝试H+12, H+22, H+32,…H+i2,而非如线性探测那样依序尝试。假设表格大小为质数(prime),而且永远保持装填因子α在0.5以下(超过0.5就重新配置并重新整理表格),那么就可以确定每插入一个新元素所需要的探测次数不多于2

二次探测可以消除主集团(primary clustering),却有可能造成次集团(secondary clustering):两个元素经hashing function计算出来的位置若相同,则插入时所探测的位置也相同,形成某种浪费。消除次集团的办法也有,例如复式散列(double hashing)。


(3)开链(separate chaining)

此方法在每一个表格元素中维护一个list:hashing function分配某个list,然后在那个list是你上执行元素的插入、搜寻、删除等操作。虽然针对list而进行的搜寻只能是一种线性操作,但如果list够短,速度还是够快。对于开链法,表格的装填因子将大于1,SGI STL的hash table便是采用这种做法。

2. HashTable的桶子(buckets)与节点(nodes)

       以开链法(separatechaining)完成hash table,表格内的元素为桶子(buckets),意思是表格内的每个单元涵盖的不只是个单个节点元素,而是多个节点。


在hash table的节点定义如下:

template<class Value>
struct_hashtable_node
{
       _hashtable_node* next;
       value val;
};

这里bucket所维护的linked list并不采用STL的list或slist,而是自行维护上述的hash table node,至于bucket聚合体则以vector完成,以便有动态扩充能力。hashtable的数据结构就是vector,其定义如下:

vector<node*,Alloc> buckets;  //以vector完成
size_typenum_elements;

hashtable的迭代器就是buckets vector,没有后退操作(operator- -()),hashtable也没有定义逆向迭代器(reverse iterator)。

3. HashTable函数操作

(1)插入(insert)与表格重整(resize)

用户调用库函数开始进行插入节点元素操作时,又分为两种情况:1. 函数insert_unique()不允许插入重复;2. 函数insert_equal()允许插入重复;

1.  insert_unique()操作:----不允许插入重复

iht.insert_unique(59);

iht.insert_unique(63);

iht.insert_unique(108);

在hashtable内会进行以下操作:

//插入元素,允许重复
pair<iterator,bool> insert_equal(const value_type& obj)
{
       resize(num_elements + 1);   //库函数,判断是否需要重建表格,需要就扩充
       return insert_unique_noresize(obj);
}

注:至于重建表格的判断,就是拿元素个数(把新增元素计入后)和bucket vector原来的大小比较,如果前者大于后者,就重建表格每个bucket(list)的最大容量和buckets vector的大小相同

2.  insert_equal()操作: ----允许插入重复

iht.insert_equal(59);

iht.insert_equal(63);

在hashtable内会进行以下操作:

//插入元素,允许重复
pair<iterator,bool> insert_equal(const value_type& obj)
{
       resize(num_elements + 1);   //库函数,判断是否需要重建表格,需要就扩充
       return insert_unique_noresize(obj);
}

(2)复制(copy_from)与整体删除(clear)

由于整个hashtable由vector和linked-list组合而成,因此复制和整体删除都要特别注意内存释放问题,hashtable提供的两个相关函数clear()和copy_from()。但注意clear()buckets vector并未释放掉空间,仍保持原来大小

4. HashSet

       STL set以RB-Tree为底层机制,而hashset以hashtable为底层机制。由于hashset所供应的操作接口hashtable都提供了,所有已几乎所有的hashset操作行为,都只是转调用hashtable的操作行为而已。

       RB-Tree有自动排序功能而Hashtable没有,反映出来的结果就是set的元素有自动排序功能而hashset没有;hashtable有一些无法处理的类型,凡是hashtable无法处理者,hashset也无法处理。

以下是hashset的定义:

template<class Value,
class HashFcn = hash<Value>,
class EqualKey = equal_to<Value>,
class Alloc = alloc>
classhash_set
{
 private:
       typedef hashtable<Value, Value, HashFcn, identity<Value>,EqualKey, Alloc> ht;
       ht rep;  //底层机制以hashtable完成
 public:
   ... ...
}

5. HashMap

       hashmap以hashtable为底层机制。由于hashmap所供应的操作接口hashtable都提供了,所有已几乎所有的hashmap操作行为,都只是转调用hashtable的操作行为。

       RB-Tree有自动排序功能而Hashtable没有,反映出来的结果就是map的元素有自动排序功能而hashmap没有;hashtable有一些无法处理的类型,凡是hashtable无法处理者,hashmap也无法处理。

以下是hashmap定义:

template<class Key,
	 class T,
	 class HashFcn = hash<Key>,
	 class EqualKey = equal_to<Key>,
	 class Alloc = alloc>
classhash_map
{
  private:
       typedef hashtable<pair<const Key, T >, Key, HashFcn, selectlst<pair<constKey, T> >, EqualKey, Alloc> ht;
       ht rep;  //底层机制以hashtable完成
  public:
     ...
}

参考文献:《STL源码解析》第五章 关联式容器

你可能感兴趣的:(HashMap,hashset,STL,Hashtable)