1. 简介
Hash_table可提供对任何有名项(named item)的存取、删除和查询操作。由于操作对象是有名项,所以hash_table可被视为是一种字典结构(dictionary)。
Hash_table使用名为hash faction的散列函数来定义有名项与存储地址之间的映射关系。使用hash faction会带来一个问题:不同的有名项可能被映射到相同的地址,这便是所谓的碰撞(collision)问题,解决碰撞问题的方法主要有三种:线性探测(linear probing)、二次探测(quadratic probing)、开链(separate chaining)。
1.1 线性探测
先介绍一个名词:负载系数(loading factor),意为元素个数除以表格大小。负载系数永远在0 ~ 1之间(除非使用开链策略)。
当hash faction计算出某个元素的插入位置,而该位置已经被占用时,我们循序往下查找,直到找到一个可用空间为止。然而这凸显了一个问题:平均插入成本的涨幅,远高于负载系数的涨幅。这样的现象在hashing过程中被称为主集团(primary clustering)。此时我们手上有一大团已经被占用的方格,插入操作极有可能在主集团所形成的泥泞中艰难爬行,不断碰撞,最后好不容易才找到一个落脚处,但这又助长了主集团的泥泞面积。
1.2 二次探测
二次探测主要用来解决主集团的问题。如果hash faction计算出新元素的位置为H,但该位置被占用,那我们尝试H + 1^2、H + 2^2、H +3^2…..,而不是H + 1、H + 2、H + 3…….。二次探测可以消除主集团(primary clustering),但是可能造成次集团(secondary clustering)。
1.3 开链
这是我们采用的方法。
在每个表格元素中维护一个 list(链表),list 由 hash faction 为我们,我们在list上执行元素的插入、查找、删除等操作,虽然对list进行的查询是线性操作,但是 list 足够短的话,我们也能获得较好的效率。注意,使用开链法,表格的负载系数可能大于1。
1.4 开链法的名称约定
下面介绍开链法中的基本术语,这些术语将贯穿博客和代码注释。
我们用vector作为hash_table的底层实现,并称hash_table内的每个元素为桶子bucket,每个桶子里装着一个list头指针,通过list头指针,可以串联出一串节点(node)。这就是开链法的精髓:hash_table(底层是vector)的每个元素装着list头指针,每个list头指针可以开出一条链表。
Hash_table的实现方式有点像deque,关于deque的实现,请看另一篇博客:STL简单deque的实现
2.设计与实现
我用VS2013写的程序(github),set和multiset版本的代码位于cghSTL/version/cghSTL-0.4.3.rar
在STL中,set和multiset的底层都需要以下几个文件:
1. globalConstruct.h,构造和析构函数文件,位于cghSTL/allocator/cghAllocator/
2. cghAlloc.h,空间配置器文件,位于cghSTL/allocator/cghAllocator/
3. hash_table_node.h,hash_table节点的实现,位于cghSTL/associative containers/RB-tree/
4. hash_table_iterator.h,hash_table迭代器的实现,位于cghSTL/associativecontainers/cgh_hash_table/
5. cgh_hash_table.h,hash_table的实现,位于cghSTL/associativecontainers/cgh_hash_table/
6. test_cgh_hash_table,测试文件,位于cghSTL