基于拉链法的散列表

请先参考深入了解散列表

什么是基于拉链法的散列表?

对于散列算法的碰撞处理,一种直接的办法就是将大小为M的数组中的每个元素指向一条链表,链表中的每个结点都存储了散列值为该元素的索引的键值对。

这种方法称为拉链法,因为发生冲的元素都被存储在链表中。

这个方法的基本思想就是选择足够大的M,使得所有的链表都尽可能的短以保证高效地查找

基于拉链法的散列表的查找方法:

  1. 首先根据散列值找到对应的链表,
  2. 然后沿着链表顺序查找相应的键。

拉链法的一种实现方法

  1. 使用原始的链表数据类型。
  2. 采用一般性的策略(但效率稍低),为M个元素分别构建符号表来保存散列到这里的键。

拉链法中链的平均长度

因为我们要用M条链表保存N个键,无论键在各个链表中的分布如何,链表的平均长度肯定为

N/M

例如,假设所有的键都落在了第一条链表上,所有链表的平均长度仍然

(N+0+0+0+…+0)/M  = N/M。

拉链法在实际情况中很有用,因为每条链表确实都大约有N/M个键值对。在一般情况中,我们能够依赖这种高效的查找和插入操作。

《基于拉链法的散列表》

代码实现

public class SeparateChainingHashST<Key, Value> {
    private static final int INIT_CAPACITY = 4;

    private int n;                                // 键值对的总数
    private int m;                                // 散列表的大小
    private SequentialSearchST<Key, Value>[] st;  // 存放链表对象的数组

    public SeparateChainingHashST() {             //无参构造
        this(997);
    } 
    public SeparateChainingHashST(int m) {       //有参构造
        this.m = m;                              //创建M条链表
        st = (SequentialSearchST<Key, Value>[]) new SequentialSearchST[m];
        for (int i = 0; i < m; i++)
            st[i] = new SequentialSearchST<Key, Value>();
    } 

 private int hash(Key key) {                       //获得键的hash值
        return (key.hashCode() & 0x7fffffff) % m;
    } 
 public Value get(Key key) {                       //获取符号表中的指定键
        int i = hash(key);
        return st[i].get(key);
    } 
 public void put(Key key, Value val) {             //存放一个键值对到符号表
        if (val == null) {
            delete(key);
            return;
        }
        //如果列表的平均长度>= 2,则将表大小增加一倍
        if (n >= 10*m) resize(2*m);
        int i = hash(key);
        if (!st[i].contains(key)) n++;
        st[i].put(key, val);
    } 
 public Iterable<Key> keys() {                      //返回符号表中键的迭代器
        Queue<Key> queue = new Queue<Key>();
        for (int i = 0; i < m; i++) {
            for (Key key : st[i].keys())
                queue.enqueue(key);
        }
        return queue;
    } 

这段简单的符号表实现维护着一条链表的数组,用散列表来为每个键选择一条链表。简单起见,我们使用了SequentialSearchST。在创建st[]时需要进行类型转换,因为Java不允许泛型的数组。默认的构造函数会使用997条链表,因为对于较大的符号表,这种实现比SequentialSearchST大约快1000倍。当你能够预知需要的符号表大小时,这种短小精悍的方案能够得到不错的性能。

一种更可靠的方案是动态调整链表数组的大小,这样无论在符号表中有多少键值对都能保证链表较短。

散列表的大小

在实现基于拉链法的散列表时,我们的目标是选择适当的数组大小M,既不会因为空链表而浪费大量内存,也不会因为链表太长而在查找上浪费大量时间。对于拉链法来说这并不是关键性的选择。

如果存入的键多余预期,查找所需的时间只会比选择更大的数组稍长;如果少于预期,虽然有些空间浪费但查找会非常快。

当内存不是很紧张时,可以选择一个足够大的M,使得查找所选要的时间变为常数;当内存紧张时,选择尽量大的M仍然能够将性能提高M倍。

删除操作

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

  public void delete(Key key) {
        if (key == null) throw new IllegalArgumentException("argument to delete() is null");

        int i = hash(key);
        if (st[i].contains(key)) n--;
        st[i].delete(key);

        // 如果列表的平均长度<= 2,则将表大小减半         if (m > INIT_CAPACITY && n <= 2*m) resize(m/2);
    } 

有序性相关操作

散列最主要的目的在于均匀地将键散布开来,因此在计算散列后键的顺序信息就丢失了。如果你需要快速找到最大键或者最小键,或是查找某个范围内的键,散列表都不是合适的选择,因为这些操作的时间都是线性的。

小小总结:

基于拉链法的散列表的实现简单。在键的顺序并不重要的应用中。它可能是最快的(也是使用最广泛的)符号表实现。

    原文作者:SeYuFac
    原文地址: https://zhuanlan.zhihu.com/p/62137071
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞