散列表的学习和探讨(算法导论第11章)

抱怨没有用,只能靠自己。 ——崔万志

  许多的应用需要一个动态的集合结构,它至少支持INSERT、SEARCH和DELETE的字典操作。
散列表是实现了字典操作的一种有效数据结构。尽管最坏情况下散列表查找一个元素的时间与链表的查找时间相同,达到了O(n)。散列表是普通数据概念的推广。由于对于普通数组可以直接寻址,数组能够提供对元素的快速访问但难于扩展;链表易于扩展但不能对其元素进行快速访问。对于大量元素的数据来说,我们当然希望两全其美。散列表提供了达到此目标的一种方法。散列表又称哈希表,它有以下几个术语
1. 关键字:元素的存储部分,数据库的元素通过它进行存储,查找等操作(也称作散列关键字)
2. 散列表元:散列数组的某个位置,其后跟着另外一个包含其元素的结构
3. 散列函数:对关键字和散列表元提供映射的函数
4. 完全散列函数:对关键字和整数提供一一映射的函数
在散列表中,不是直接把关键字用作数组下标,而是根据关键字通过散列函数计算出来的。


1. 直接寻址表

  当关键字的的全域(范围)U比较小的时,直接寻址是简单有效的技术,一般可以采用数组实现直接寻址表,数组下标对应的就是关键字的值,即具有关键字k的元素被放在直接寻址表的槽k中。直接寻址表的字典操作实现比较简单,直接操作数组即可以,只需O(1)的时间。

2. 散列表

  直接寻址表的不足之处在于当关键字的范围U很大时,在计算机内存容量的限制下,构造一个存储|U|大小的表不太实际。当存储在字典中的关键字集合K比所有可能的关键字域U要小的多时,散列表需要的存储空间要比直接寻址表少的很多,从而使用直接寻址会造成大量空间的浪费。散列表通过散列函数h计算出关键字k在槽的位置。散列函数h将关键字域U映射到散列表T[0….m-1]的槽位上。即h:U->{0,1…,m-1}。采用散列函数的目的在于缩小需要处理的小标范围,从而降低了空间的开销。
  散列表存在的问题:两个关键字可能映射到同一个槽上,即碰撞(collision)。需要找到有效的办法来解决碰撞。下边介绍两种冲突解决方法:
  链接法:在链接法中把散列到同一个槽中的所有元素放到一个链表中。
  开放地址法:所有的元素都在散列表中,每一个表项或包含动态集合的一个元素,或包含NIL。这种方法中散列表可能被填满,以致于不能插入任何新的元素。在开放寻址法中,当要插入一个元素时,可以连续地检查或探测散列表的各项,直到有一个空槽来放置待插入的关键字为止。有三种技术用于开放寻址法:线性探测、二次探测以及双重探测。

3. 散列函数

  好的散列函数的特点是每个关键字都等可能的散列到m个槽位上的任何一个中去,并与其他的关键字已被散列到哪一个槽位无关。多数散列函数都是假定关键字域为自然数N={0,1,2,….},如果给的关键字不是自然数,则必须有一种方法将它们解释为自然数。例如对关键字为字符串时,可以通过将字符串中每个字符的ASCII码相加,转换为自然数。算法导论中介绍了三种设计方案:除法散列法、乘法散法和全域散列法。
  除法散列法:
  

h(k)=kmodm

  在使用除法散列法的时候应该避免选择m的某些值,比如m不应为2的幂。通常希望 h(k) 的值依赖于 k 的所有位而不是最低 p 位,一个不太接近2的整数幂的素数是m的一个较好的选择。例如我们将n=2000个字符串,其中每个字符有8位,我们不介意一次不成功我们平均检查3个元素,这样我们分配散列表的大小m为701.因为701是一个接近2000/3,但又不接近任何2的任何次幂的素数。
  乘法散列法:
  第一步:用关键字k乘上常数A (0< A < 1 ),并且提取kA的小数部分。第二步:用m乘以这个数值然后在向下取整。
  
h(k)=m(kAmod1)

  乘法散列法的一个优点是对m的选择不是特别的关键,一般选择它为2的某个幂次(m=2pp)。
  全域散列法:
  给定一组散列函数H,每次进行散列时候从H中随机的选择一个散列函数h,使得h独立于要存储的关键字。全域散列函数类的平均性能是比较好的。
  

4. 开放地址法解析

  相对于链接法,开放地址法不使用指针,直接计算出槽序列。不使用存储指针可以节省空间,是的可以使用同样的空间提供更多的槽,潜在的减少了冲突,提高了检索速度。为了使用开放寻址法插入元素需要连续的检查散列表,或者称为探查,直到找到空槽来放置待插入的关键字为止。
  开放地址法散列表的插入、查找和删除。
  参考链接
  1. 解决哈希表的冲突-开放地址法和链地址法
  2. 《算法导论》读书笔记之第11章 散列表

posted @ 2016-02-28 21:58  snowwolf101  阅读(397)  评论(0编辑  收藏  举报