##摘要
我们前面介绍了所谓的二叉搜索树和平衡二叉搜索树,还有一种被广泛使用的平衡二叉搜索树——RB-tree(红黑树),红黑树不仅在树形的平衡上表现不错,在效率表现和实现复杂度上也保持相当的“平衡”,所以被广泛使用,也因此成为STL set和map的标准底层机制。
二叉搜索树具有对数平均时间(longarithmic average time)的表现,但这样的变现构造在一个假设上:输入数据有足够的随机性。本节将介绍一种名为hashtable(散列表)的数据结构,这种结构在插入、删除、搜寻等操作上也具有“常数平均时间”的表现,而且这种表现是以统计为基础,不需要依赖输入元素的随机性。
##hashtable概述
hash table可提供对任何有名项(name item)的存取操作和删除操作。由于操作对象是有名项,所以hashtable也可被视为一种字典结构(dictionary)。这种结构的用意在于提供常数时间之基本操作,就像stack或deque那样。咋听之下是不可能完成的任务,因为制约条件如此之少,而元素的增加,搜寻操作必定耗费更多的时间。但也不是绝对,如果所有的元素都是16位且不带正负号的整数,范围0—65535,那么简单地运用一个数组就可以满足上述期望。首先配置一个array A,拥有65535+1个元素,索引号码0~65535,初始值都为0,如下图。每一个元素值代表相应元素的出现次数。如果插入元素i,我们就执行A[i]++,如果删除元素i,我们就执行A[i]–,如果搜寻元素i,我们就检查A[i]是否为0.以上的每一个操作都是常数时间。这种解法的额外负担(overhead)是array的空间和初始化时间。
但这个解法存在两个现实问题。一是如果元素是32位而非16位,我们所准备的array A,初始值大小就必须是2^32=4GB,这就大的不切实际了。第二,如果元素型态是字符串而非整数,将无法被拿来作为array的索引。
第二个问题(关于索引)不难解决,就像数值1234是由阿拉伯数字1,2,3,4构成一样,字符串“array”是由’a’,‘r’,‘r’,‘a’,'y’构成。那么既然数值1234是1101010+21010+310*+410^0,我们也可以把字符编码,每个字符以一个7-bits数值来表示(ASCLL编码),从而将字符串“array”表现为:'a’128128128128+'r’128128128+'r’128128+'a’128+'y’1
于是先前的array实现法就可适用于“元素型别为字符串”的情况了。但这并不实用,因为这会产生出非常巨大的数值。“array”的索引值将是:106128128128128+106128128128+104128128+111128+117*1=28678174709
这就太不实际了。更长的字符串会导致更大的索引值。这就回归到第一个问题:array的大小。
如何避免使用一个大得荒谬的array呢?办法之一就是使用某种映射函数,将大数映射为小数。负责将某一元素映射为一个“大小可接受值索引”,这样的函数称为hash function(散列函数)。假如x是任意整数,TableSize是array大小,则X%TableSize会得到一个整数,范围在0~TableSize之间,恰好可以作为表格的索引。
使用hash function会带来一个问题:可能有不同的元素被映射到相同的位置(亦有相同的索引)。这是无法避免的,因为元素个数大于array容量。这便是所谓的“碰撞(ollision)”问题。解决问题的方法有许多种,包括线性探测(linear probing)、二次探测(quadratic probing)、开链(separate chaining)…等做法。每一种方法都很容易,导出的效率各不相同——与array的填满程度有很大的关联。
##线性探测(linear probing)
负载系数(loading factor):意指元素个数除以表格大小。负载系数永远在0-1之间,除非使用开链策略。
当hashfunction计算出某个元素的插入位置,而该位置上的空间已不再可用时,最简单的办法就是循序往下一一寻找(如果达到尾端,就绕头部继续寻找),直到找到一个可用空间为止。只要表格(array)足够大,操作时,道理也相同,如果hash function计算出来的位置上的元素值与我们的搜寻目标不符,就循序往下一一寻找,直到找到吻合者,或直到遇到空格元素。止于元素的删除,必须采用惰性删除(lazy deletion),也就是只删除标号,实际删除操作侧待表格重新整理时在进行。因为hash table中的每一个元素不仅表述它自己,也关系到其它元素的排列。
如下图:依序插入5个元素,array的变化:
欲分析线性探测的表现,需要两个假设:(1)表格足够大;(2)每个元素都够独立。在此假设之下,最坏的情况是线性巡防整个表格,平均情况则是巡防一半表格,这已经和我们所期望的常数时间天差地远了,而实际情况更为糟糕。明显的问题出在第二个假设:若持续上图的最后状态,除非新元素经过hash function的计算之后直接落在位置#4-#7,否则位置#4-#7永远不可能被运用,因为位置#3永远是第一考虑。换句话说,新元素不论是8,9,0,1,2,3中的哪一个,都会落在#3位置上。新元素如果是4或者5,6,7,才会各自落在位置#4,#5,#6,#7上。这很清楚地突显了一个问题:平均插入成本的成长幅度,远高于负载系数的成长幅度——这样的现象在hashing过程中被称为主集团(primary clustering)。此时的我们手上有的是一大团被用过的方格,插入操作极有可能在主集团所形成的泥泞中奋力爬行,不断解决碰撞的问题,最后才射门得分,但却是又助长了主集团的泥泞面积。
##二次探测(quadratic probing)
二次探测可以消除主集团,却可能造成次集团(secondary clustering):两个元素经过hash function计算出来的位置若相同,则插入式所探测的位置也形同,形成某种浪费。另外,消除次集团可用复式散列(double hashing)。
##开链(separate chaining)
另一种与二次探测法相近的便是开链法。这种做法是在每一个表格中维护一个list:hash function为我们分配某一个list,然后我们在那个list身上执行元素的插入,搜寻、删除等操作。虽然针对list而进行的搜寻只能是一种线性操作,但list够短,速度还是够快。
08-08
05-23
“相关推荐”对你有帮助么?
-
非常没帮助
-
没帮助
-
一般
-
有帮助
-
非常有帮助
提交