hashMap

来源:互联网 发布:淘宝可以免费开店吗 编辑:程序博客网 时间:2024/06/03 20:00

数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端。

数组

数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(log2n);数组的特点是:寻址容易,插入和删除困难;

链表

链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。

哈希表

哈希表((Hash table)既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。

哈希表有多种不同的实现方法,其中最常用的一种方法—— 拉链法,我们可以理解为”链表的数组”,如图:

JDK中HashMap实现模型

从数据结构来看可以知晓哈希表是由数组+链表组成的,每个元素存储的是一个链表的头结点。
HashMap数组初始化大小为16:

static final int DEFAULT_INITIAL_CAPACITY = 16;

那么这些元素是按照什么样的规则存储到数组中呢?
一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。

static int hash(int h) {                h ^= (h >>> 20) ^ (h >>> 12);        return h ^ (h >>> 7) ^ (h >>> 4);    }
确定数组index:hashcode % table.length取模

HashMap存取时,都需要计算当前key应该对应Entry[]数组哪个元素,即计算数组下标;算法如下:

    static int indexFor(int h, int length) {        return h & (length-1);    }

按位取并,作用上相当于取模mod或者取余%。这意味着数组下标相同,并不表示hashCode相同。
比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。


Java中的HashMap也是一个线性的数组实现的,那一个线性的数组怎么实现按键值对来存取数据呢?

首先HashMap里面实现一个静态内部类Entry,其重要的属性有 hash , key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean:

static class Entry<K,V> implements Map.Entry<K,V> {        final K key;        V value;        Entry<K,V> next;        final int hash;                public final V setValue(V newValue) {            V oldValue = value;            value = newValue;            return oldValue;        }   public final boolean equals(Object o) {       if (!(o instanceof Map.Entry))             return false;         Map.Entry e = (Map.Entry)o;         Object k1 = getKey();         Object k2 = e.getKey();        if (k1 == k2 || (k1 != null && k1.equals(k2))) {                Object v1 = getValue();                Object v2 = e.getValue();          if (v1 == v2 || (v1 != null && v1.equals(v2)))                    return true;        }            return false;    }   public final int hashCode() {      return (key==null   ? 0 : key.hashCode()) ^             (value==null ? 0 : value.hashCode());   }        }

我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。

transient Entry[] table;

HashMap的存取实现

既然是线性数组,为什么能随机存取?

// 存储时:int hash = key.hashCode(); // 这个hashCode方法这里不详述,只要理解每个key的hash是一个固定的int值int index = hash % Entry[].length;Entry[index] = value;
// 取值时:int hash = key.hashCode();int index = hash % Entry[].length;return Entry[index];

put

疑问:如果两个key通过hash%Entry[].length得到的index相同,会不会有覆盖的危险?

这里HashMap里面用到链式数据结构的一个概念。
上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。
打个比方,第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?
HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;
这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。
也就是说数组中存储的是最后插入的元素。

public V put(K key, V value) {    if (key == null)        return putForNullKey(value); //null总是放在数组的第一个链表中    int hash = hash(key.hashCode());    int i = indexFor(hash, table.length);    //遍历链表   for (Entry<K,V> e = table[i]; e != null; e = e.next) {            Object k;         //如果key在链表中已存在,则替换为新value        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {                V oldValue = e.value;                e.value = value;                e.recordAccess(this);                return oldValue;            }        }        modCount++;        addEntry(hash, key, value, i);        return null;    }
void addEntry(int hash, K key, V value, int bucketIndex) {    Entry<K,V> e = table[bucketIndex];    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //参数e, 是Entry.next    //如果size超过threshold,则扩充table大小。再散列    if (size++ >= threshold)        resize(2 * table.length);}

当然HashMap里面也包含一些优化方面的实现,这里也说一下。比如:Entry[]的长度一定后,随着map里面数据的越来越长,这样同一个index的链就会很长,会不会影响性能?
HashMap里面设置一个因子,随着map的size越来越大,Entry[]会以一定的规则加长长度。


get
public V get(Object key) {        if (key == null)            return getForNullKey();        int hash = hash(key.hashCode());        //先定位到数组元素,再遍历该元素处的链表        for (Entry<K,V> e = table[indexFor(hash, table.length)];            e != null;            e = e.next) {            Object k;            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))                return e.value;        }        return null;}

null key的存取

null key总是存放在Entry[]数组的第一个元素。

private V putForNullKey(V value) {        for (Entry<K,V> e = table[0]; e != null; e = e.next) {            if (e.key == null) {                V oldValue = e.value;                e.value = value;                e.recordAccess(this);                return oldValue;            }        }        modCount++;        addEntry(0, null, value, 0);        return null;    } private V getForNullKey() {     for (Entry<K,V> e = table[0]; e != null; e = e.next)      {            if (e.key == null)                return e.value;     }        return null;}

table初始大小
public HashMap(int initialCapacity, float loadFactor) {        .....        // Find a power of 2 >= initialCapacity        int capacity = 1;        while (capacity < initialCapacity)            capacity <<= 1;        this.loadFactor = loadFactor;        threshold = (int)(capacity * loadFactor);        table = new Entry[capacity];        init();    }

注意table初始大小并不是构造函数中的initialCapacity!而是 >= initialCapacity的2的n次幂!!!!


解决hash冲突的办法

开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
再哈希法
链地址法
建立一个公共溢出区

Java中hashmap的解决办法就是采用的链地址法。

再散列rehash过程

当哈希表的容量超过默认容量时,必须调整table的大小。当容量已经达到最大可能值时,那么该方法就将容量调整到Integer.MAX_VALUE返回,这时,需要创建一张新表,将原表的映射到新表中。

    void resize(int newCapacity) {        Entry[] oldTable = table;        int oldCapacity = oldTable.length;        if (oldCapacity == MAXIMUM_CAPACITY) {            threshold = Integer.MAX_VALUE;            return;        }        Entry[] newTable = new Entry[newCapacity];        transfer(newTable);        table = newTable;        threshold = (int)(newCapacity * loadFactor);    }    void transfer(Entry[] newTable) {        Entry[] src = table;        int newCapacity = newTable.length;        for (int j = 0; j < src.length; j++) {            Entry<K,V> e = src[j];            if (e != null) {                src[j] = null;                do {                   Entry<K,V> next = e.next;                   //重新计算index                   int i = indexFor(e.hash, newCapacity);                    e.next = newTable[i];                    newTable[i] = e;                    e = next;                } while (e != null);            }        }    }
1 0
原创粉丝点击