作者:disappearedgod

3.4 散列表

使用散列的查找算法分为两步。

第一步是用散列函数将被查找的键转化为数组的一个索引。理想情况下,不同的键都能转化为不同的索引值。

第二部是一个处理碰撞冲突的过程。(拉链法和线性探测法)

散列表是算法在时间和空间上做出平衡的经典例子。

散列表的理论:

散列表的理论依据是根据如下理论总结出来的。尽管验证这个假设很困难,假设J仍然是考察散列函数的方式,原因有两点:

首先,设计散列函数时,尽量避免随意指定参数以防止大量的碰撞,这是我们的重要目标;

其次,尽管我们肯能无法验证假设本身,它提示我们使用数学分析来预测散列算法的性能并在试验中进行验证。

假设J(均匀散列假设)。我们使用的散列函数能够均匀并独立地将所有的键散布于0与M-1之间。

3.4.1 散列函数

3.4.1.1 典型的例子

3.4.1.2 正整数

除留余数法:选择大小为素数M的数组,对于任意正整数k,计算k除以M的余数。一般、M取素数,但是由于(美国)邮编的历史原因和IP分配原则并不是随机的,所以这种方法的索引很容易出现碰撞。

3.4.1.3 浮点数

如果键是在0、1间的实数,我们可以将它乘以M并四舍五入得到一个0到M-1的索引值。它是有缺陷的,因为这种情况下键的高位起更大的作用。

3.4.1.4 字符串

除留余数法用来处理字符串的时候,我们只需要将他们当做大整数即可。调用charAt()函数。如果R比任何字符的值都大,这种计算相当于将字符串当作一个N位的R进制值,将它除以M并取余。一种叫Horner方法的经典算法用N词乘法、加法和取余来计算一个字符串的散列值。只要R足够小,不造成溢出,那么结果就能够如我们所愿,落在0到M-1之内。使用一个较小的素数,例如31,可以保证字符串中的所有字符都能发挥作用。(JAVA Stirng CLASS)

int  hash=0;
for(int= 0; i<s.length(); i++)
    hash = (R * hash + s.chatAt(i))%M;

3.4.1.5 组合键

3.4.1.6 JAVA的约定

3.4.1.7 将hasCode()的返回值转化为一个数组索引

因为我们需要的是数组的索引而不是一个32位的证书,我们在实现中会将默认的hashCode()方法和除留余数法结合起来长生一个0到M-1的整数,方法如下

private int has(Key x)
{    return (x.hashcode() & 0x7fffffff)%M; }

3.4.1.8 自定义的hasCode()方法

3.4.1.9 软缓存

3.4.2 基于拉链法的散列表

一个散列函数能够将键转化为数组索引。散列算法的第二步是碰撞处理,也就是处理两个或多个键的散列值相同的情况。一种直接的办法是将大小为M的数组中的每个元素指向一条链表,链表中的每个节点都存储了散列值为该元素的索引的键值对。这种方法称为拉链法。

这个方法的基本思想就是选择足够大的M,使得所有链表都尽可能短以保证高效的查找。查找分为两步:首先根据散列值找到对应的链表,然后沿着链表顺序查找相应的键。

拉链法的一种实现方法是使用原始的链表数据类型来扩展SequentialSearchST。另一种更简单的方法(但效率更低)是采用一般性的策略,为M个元素分别构建符号表来保存散列到这里的键。

下面算法实现的SeparateChainingHashST使用了一个SequentialSearchST对象的数组,在put()和get()方法来完成相应的任务。

public class SeparateChainingHashST<Key, Value>
{
    private int N; // number of key-value pairs
    private int M; // hash table size
    private SequentialSearchST<Key, Value>[] st; // array of ST objects
    public SeparateChainingHashST()
    { this(997); }
    public SeparateChainingHashST(int M)
    { // Create M linked lists.
        this.M = M;
        st = (SequentialSearchST<Key, Value>[]) new SequentialSearchST[M];
        for (int i = 0; i < M; i++)
        st[i] = new SequentialSearchST();
     }  
     private int hash(Key key)
     { return (key.hashCode() & 0x7fffffff) % M; }
     public Value get(Key key)
    { return (Value) st[hash(key)].get(key); }
    public void put(Key key, Value val)
    {   st[hash(key)].put(key, val); }
    public Iterable<Key> keys()
    // See Exercise 3.4.19.
}

命题K

在一张含有M条链表和N个键的散列表中,(在假设J成立的前提下),任意一条链表中的键的数量均在 N/M 的常数因子范围内的概率无限趋向于1.

性质L

在一张含有M条链表和N个键的散列表中,未命中查找和插入操作所需的比较次数为~N/M.

3.4.2.1 散列表的大小

3.4.2.2 删除操作

3.4.2.3 有序性相关操作 散列最主要的目的在于均匀地将键散布开来。

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

3.4.3.1 删除操作

3.4.3.2 键簇

3.4.3.3 线性探测法的性能分析 3.4.4 调整数组大小

3.4.4.1 拉链法

3.4.4.2 均摊分析

3.4.5 内存使用