哈希(1) - 介绍

1.引言

如果设计一个员工信息存储系统,用他们的电话号码做为key,而且要让以下的这些查询操作尽可能的高效:
  1. 插入一个电话号码以及相关的信息.
  2. 搜索一个电话号码以及相关的信息.
  3. 删除一个电话号码以及相关的信息.

一般都能够考虑使用以下的数据结构来存储不同电话号码的信息。

  1. 电话号码和记录的数组。
  2. 电话号码和记录的链表。
  3. 电话号码做为key的平衡二叉树。
  4. 直接訪问数据表。

对于数组和链表,我们须要花费线性时间来搜索,在实际应用中比較耗时。我们使用数组并将数据排好序。利用二分查找搜索电话号码,须要O(Logn)的时间,可是插入和删除会变得耗时。由于要保持排序。

使用平衡二叉树,能够得到比較稳定的搜索,插入以及删除时间。

全部的这些操作能够确保在O(Logn) 时间之内。

还有一种方法是是使用直接訪问数据表,建立一个大的数组且使用电话号码做为索引。

假设没有提供号码,则这个数组元素为NIL,否则数组元素会保存一个指向电话记录的指针。

这个方法的时间复杂度是前面全部方案中最小的。能够在O(1)的时间内完毕全部操作。比如,假设要插入一个电话号码,首先使用给定的号码创建一条记录,然后使用号码做为索引。将指向这个记录的指针保存下来。



可是。这个方法有非常多的实际操作限制。

首先的问题是须要大量的内存空间。比如。假设号码由n个数字组成,则须要O(m * 10n)的空间。当中m是记录指针的大小。

还有一个问题是编程语言中的整形,可能没有n位数这么大。

基于上述限制。直接訪问表并不常使用。

在全部的这些数据结构中。哈希是最佳方案。使用哈希,能够得到O(1)的平均查找时间以及O(n)的最坏查找时间。

2.哈希介绍

哈希是基于直接訪问表的一个改进。

方案是使用哈希函数,将给定的电话号码或者其他key,转换为一个小的数值,并将此小数值做为称为哈希表中的索引。

哈希函数: 将一个非常大的电话号码数值转换为小数值的函数。映射得到的小数值做为哈希表的索引。


一个好的哈希函数应该具备以下特征:
1) 有效可计算性(Efficiently computable)。
2) 必须均匀地映射key(即每一个key相应每一个表位置)。

比如,对于电话号码。一个坏的哈希函数可能使用前3位数字,而可能更好的函数应该是考虑后3位数字。

注意。这并不一定是最佳哈希函数,可能存在更好的方法。

哈希表: 即一个数组,元素存储的是指向相关电话号码记录的指针。假设没有号码能映射到某个索引,则这个索引代表的元素为NIL。

3.哈希冲突处理方法

冲突处理: 由于哈希函数处理的是一个非常大的整数或者字符串。而映射到的范围是一个比較小的数值。则非常有可能两个key拥有同样的映射值。对于新插入的key。映射到了一个已存在的位置上时,这样的情况称之为冲突,而且必须使用一些冲突检測技术来进行处理。以下是两种处理冲突的方法:

  • 链接法(Chaining):主要思想是将哈希表中的每一项指向一个记录链表。这个链表拥有同样的哈希值。链表法相对照较简单,可是须要额外的内存。
  • 开放定址法(Open Addressing): 这种方法的基本思想是:当发生地址冲突时,依照某种方法继续探測哈希表中的其它存储单元。直到找到空位置为止。

以下是对这两种方法的详细介绍:

3.1. 链接法

a. 链接法解决冲突的方法
将全部keyword为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。

凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。

在链接法中,装填因子α能够大于1。但一般均取α≤1。

b.链接法的长处
(1)链接法处理冲突简单。且无堆积现象。即非同义词决不会发生冲突。因此平均查找长度较短;
(2)因为链接法中各链表上的结点空间是动态申请的。故它更适合于造表前无法确定表长的情况。
(3)开放定址法为降低冲突,要求装填因子α较小,故当结点规模较大时会浪费非常多空间。

而链接法中可取α≥1,且结点较大时,链接法中添加的指针域可忽略不计。因此节省空间;
(4)在用链接法构造的散列表中,删除结点的操作易于实现。仅仅要简单地删去链表上对应的结点就可以。

而对开放地址法构造的散列表,删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是由于各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放地址法处理冲突的哈希表上运行删除操作。仅仅能在被删结点上做删除标记,而不能真正删除结点。

c. 链接法的缺点
指针须要额外的空间,故当结点规模较小时,开放定址法较为节省空间。而若将节省的指针空间用来扩大哈希表的规模,可使装填因子变小。这又降低了开放定址法中的冲突,从而提高平均查找速度。

3.2. 开放定址法

它的过程可用下式描写叙述: 

H i ( key ) = ( H ( key )+ d i ) mod m ( i = 1,2,…… , k ( k ≤ m – 1)) 
当中: H ( key ) 为keyword key 的直接哈希地址。 m 为哈希表的长度, di 为每次再探測时的地址增量。 
採用这样的方法时,首先计算出元素的直接哈希地址 H ( key ) ,假设该存储单元已被其它元素占用,则继续查看地址为 H ( key ) + d 2 的存储单元,如此反复直至找到某个存储单元为空时,将keyword为 key 的数据元素存放到该单元。 
增量 d 能够有不同的取法,并依据其取法有不同的称呼: 
( 1 ) d i = 1 。 2 。 3 。 …… 线性探測再散列; 
( 2 ) d i = 1^2 。- 1^2 。 2^2 。- 2^2 。 k^2, -k^2…… 二次探測再散列; 
( 3 ) d i = 伪随机序列/伪随机再散列。 

a.线性探查法(Linear Probing)
该方法的基本思想是:
  将散列表T[0..m-1]看成是一个循环向量。若初始探查的地址为d(即h(key)=d),则最长的探查序列为:
d,d+l。d+2,…,m-1,0。1,…,d-1
 即:探查时从地址d開始。首先探查T[d]。然后依次探查T[d+1],…,直到T[m-1],此后又循环到T[0],T[1],…。直到探查到T[d-1]为止。



探查过程终止于三种情况:
 (1)若当前探查的单元为空,则表示查找失败(若是插入则将key写入当中);
  (2)若当前探查的单元中含有key,则查找成功,但对于插入意味着失败;
 (3)若探查到T[d-1]时仍未发现空单元也未找到key,则不管是查找还是插入均意味着失败(此时表满)。



聚集或堆积现象
  用线性探查法解决冲突时,当表中i,i+1,…。i+k的位置上已有结点时。一个散列地址为i,i+1。…。i+k+1的结点都将插入在位置i+k+1上。把这样的散列地址不同的结点争夺同一个后继散列地址的现象称为聚集或堆积(Clustering)。这将造成不是同义词的结点也处在同一个探查序列之中,从而添加了探查序列的长度。即添加了查找时间。

若散列函数不好或装填因子过大,都会使堆积现象加剧。

b.二次探查法(Quadratic Probing)
二次探查法的探查序列是:
hi=(h(key)+i*i)%m 0≤i≤m-1 //即di=i2
   即探查序列为d=h(key)。d+12。d+22。…,等。
 该方法的缺陷是不易探查到整个散列空间。


c.双重散列法(Double Hashing)
该方法是开放定址法中最好的方法之中的一个,它的探查序列是:
       hi=(h(key)+i*h1(key))%m 0≤i≤m-1 //即di=i*h1(key)
     即探查序列为:
       d=h(key),(d+h1(key))%m。(d+2h1(key))%m。…,等。
   该方法使用了两个散列函数h(key)和h1(key),故也称为双散列函数探查法。
注意:定义h1(key)的方法较多。但不管採用什么方法定义,都必须使h1(key)的值和m互素,才干使发生冲突的同义词地址均匀地分布在整个表中,否则可能造成同义词地址的循环计算。
  【例】 若m为素数,则h1(key)取1到m-1之间的不论什么数均与m互素。因此,我们能够简单地将它定义为:
               h1(key)=key%(m-2)+1
  【例】对例9.1。我们可取h(key)=key%13,而h1(key)=key%11+1。


  【例】若m是2的方幂,则h1(key)可取1到m-1之间的不论什么奇数。

posted on 2017-05-17 16:44  slgkaifa  阅读(341)  评论(0)    收藏  举报

导航