HashSet, HashMap, HashTable原理详解
关于这几个结构重点需要区分的是:
1. HashMap与HashSet允许值为null,而HashTable不行。
2. HashMap与HashSet是非同步的,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源码解析》第五章 关联式容器