【数据结构】之散列链表(Java语言描述)

  散列链表,在JDK中的API实现是 HashMap 类。

  为什么HashMap被称为“散列链表”?这与HashMap的内部存储结构有关。下面将根据源码进行分析。

  首先要说的是,HashMap中维护着的是一个数组: transient Node<K,V>[] table; ,数组中的每个元素都是一个 Node 对象。这里的Node是HashMap的一个内部类,代码如下:

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

  可以看出来,这个Node类明显是一个链表的结构。也就是说,HashMap是一个链表的数组,这样一来,HashMap作为“链表”的部分就清楚了。

  那么HashMap为什么还被定义为“散列”呢?我们来看HashMap中的 put() 方法。put()方法中直接调用了 putVal() 方法,putVal()方法代码如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

  通过上面的代码可以看到,HashMap分析某个节点应该存储到哪条链中的依据是:先通过HashMap中的 hash() 方法获取到当前元素的key的哈希码,然后通过 (n - 1) & hash 运算,确定该节点具体应该位于哪条链上。这样一来,就相当于在HashMap中创建了一个“索引”,如果要取元素,只需要通过key的哈希值,就可以轻松地索引到节点所在的链,从而遍历链中的所有元素,最终得到目标节点,大大提高了查找的效率。通过这种方式,可能会导致数组中某些下标没有存储任何数据,HashMap的“散列”就体现在这里。

  HashMap的这种“散列”的思想,使得HashMap的查询效率比其他的任何数据结构都要高。但是,我们都知道,最好的散列,就是数组中的每个元素都是一个只有一个节点的链,即数组中的每个下标都只对应一个节点,这样,就可以保证HashMap的查询效率达到最高。当然,这种情况是很难实现的,但是HashMap给我们提供了一个能极大限度地达到这种效果的机制,那就是HashMap的扩容机制。

  HashMap的扩容机制体现在putVal()方法中: if (++size > threshold) resize(); 。 resize() 方法就是HashMap的扩容方法,后面详述; threshold 是HashMap的扩容临界值,默认是当前HashMap中数组长度的0.75倍; size 是当前HashMap中存储的节点的数量。也就是说,当HashMap中存储的节点数量超过扩容临界值的时候,就要开始扩容。通过这样方式,使HashMap能够极大地实现最佳散列效果。简单来说,如果不扩容,那么所有元素都会挤在那个数组中,难免会导致数组中某个下标对应的链过长;而通过扩容,增大了 (n - 1) & hash 操作中的基数n,因此各个元素可以重新散列,铺得更散。HashMap就是通过这种方式达到散列的。

 

posted on 2017-08-08 21:39  ITGungnir  阅读(531)  评论(0编辑  收藏  举报

导航