第三章 3.4 散列表

来源:互联网 发布:macbook装windows教程 编辑:程序博客网 时间:2024/05/18 01:38
 如果所有的键都是小整数,我们可以用一个数组来实现无序的符号表,将键作为数组的索引而数组中键i处储存的就是它对应的值,这样我们就可以快速访问任意键的值。对于此节中学习散列表,它这种简易方法的扩展并能够处理更加复杂的类型的键。我们用算法操作将键转化为数组的索引来访问数组中的键值对。
       使用散列的查找算法分为两步:
  • 用散列函数将被查找的键转化为数组的索引,理想状态下,不同的键都能转化为不同的索引值。有可能我们需要面对两个或者多个键都会散列相同的索引值的情况。
  • 第二步就是一个处理碰撞冲突的过程,对于处理冲突的方法:拉链法和线性探测法;
散列表是算法在时间和空间上作出权衡的经典例子。如果没有内存限制,我们可以直接将键作为数组的索引,那么所有查找操作只需要访问内存一次即可完成。但这种理想情况不会经常出现,因为当键很多时,需要的内存太大。另一方面,如果没有时间限制,我们可以使用无序数组并进行顺序查找,这样就只需要很少的内存。而散列表则使用了适度的空间和时间并在这两个极端之间找到了一种平衡。对此,我们不许重写代码,只需要调整散列算法的参数就可以在空间和时间之间作出取舍。

3.4.1 散列函数
   
    第一个问题就是散列函数的计算,这个过程会将键转化为数组的索引。如果有一个能够保存M个键值对的数组,我们需要一个能够将任意键转化为该数组范围内的索引的散列函数。我们要找的散列函数应该易于计算并且能够均匀分布所有的键。即对于任意键,0到M-1之间的每个整数都有相等的可能性与之对应(与键无关)。

    散列函数和键的类型有关。严格来说,对于每种类型的键都我们都需要一个与之对应的散列函数。



3.4.1.6 java的约定

       每种数据类型都需要相应的散列函数,于是java令所有数据类型都继承了一个能够返回一个32位整数的hashCode()方法。每一种数据类型的hashCode()方法都必须和equals()方法一致,也就是说,如果a.equals(b)返回true,那么a.hashCode()的返回值必然和b.hashCode()返回值相同。相反,如果两个对象的hashCode()方法的返回值不同,那么我们就知道这两个对象不同。但如果两个对象的hashCode()方法的返回值相同,这两个对象也有可能不同,我们还需要用equals()方法进行判断。
      如果要为自定义的数据类型定义散列函数,就需要同时重写hashCode()和equals()两个方法。默认散列函数会返回对象的内存地址,

3.4.1.7 将hashCode()的返回值转化为一个数组索引
      
3.4.1.8 自定义的hashCode() 方法
散列表的用例希望hashCode()方法能够将键平均地散布为所有可能的32位整数。也就是说,对于任意对象x,你可以调用x.hashCode()并认为有均等的机会得到2^32中的任意一个32位整数值。Java中的String, Integer, Double, File 和URL对象的hashCode()方法都能实现这一点。而对于自定义的数据类型,需要自己实现这一点。

3.4.2 基于拉链法的散列表
       一个散列函数能够将键转化为数组索引。散列算法的第二步是碰撞处理,也就是处理两个或多个键的散列值相同的情况。一个直接的办法是将大小为M的数组中的每个元素指向一个链表,链表中的每个结点都存储了散列值为该元素的索引的键值对,这种方法称为拉链法。因为发生冲突的元素都被存储在链表中,这个方法的基本思想就是选择足够大的M,使得所有链表都尽可能短以保证高效的查找。查找分为两步:首先根据散列值找到对应的列表,然后沿着链表查找相应的键。此处的实现方法是使用原始的链表数据类型。

      
3.4.2.1 散列表的大小
在实现基于拉链法的散列表时,我们的目标是选择适当的数组大小M, 既不会因为空链表浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。而拉链法的一个好处就是这并不是关键性选择。如果存入的键多于预期,查找所需的时间只会比选择更长的数组稍长;乳沟少于预期,虽然有些空间浪费但查找会更快。另一种方法是动态调整数组的大小以保持短小的链表。

3.4.2.2 删除操作

   要删除一个键值对,先用散列值找到含有该键的SequentialSearchST对象,然后调用该对象的delete()方法,

3.4.2.3 有序性相关的操作
散列最主要目的在于均匀得将键散布开来,因此在计算散列后键的顺序信息就丢失了,如果需要快速找到找到最大或最小的键,或是查找某个范围内的键,或是实现表有序符号表API中的其他任何方法,散列表都不是合适的选择。因为这些操作的运行时间都将是线性的。
基于拉链法的散列表的鄂实现简单,在键的顺序并不重要的应用中,它可能是最快的符号表实现。



3.4.3 基于线性探测法的散列表

实现散列表的另一种方法就是用大小为M 的数组保存N个键值对,其中M>N。我们需要依靠数组中的空位解决碰撞冲突。基于这种策略的所有方法被统称为开放地址散列表。
开放地址散列表只能怪最简单的方法叫做线性探测法:当碰撞发生时(当一个键的散列值已经被另一个不同的键占用),我们直接检查散列表的下一个位置(将索引值加1)。这样的线性探测可能会产生三种结果:
  • 命中,该位置的键和被查找的键相同;
  • 未命中,键为空(该位置没有键);
  • 继续查找,该位置的键和被查找的键不同。
开放地址类的散列表的核心思想是与其将内存用作链表,不如将它们作为在散列表的空元素。这些空元素可以作为查找结束的标志。我们在实现中使用了并行数组,一条保存键,一条保存值,同时向前面那样使用散列函数产生访问数据所需的数组索引。
原创粉丝点击