数据结构基础:哈希表(HashMap)原理分析

转自:http://blog.csdn.net/kingmicrosoft/article/details/49805339

前言:

 数组的特点是:寻址容易,插入和删除困难;

 链表的特点是:寻址困难,插入和删除容易;

我们可以构造一种结合两种优点的链表散列数据结构,可以理解为链表的数组,HashMap就是基于其实现的。

 

1.哈希表的缺点有和优点

优点:

相对数组可以节省存储空间;

插入和寻址都很快;

在散列表中,查找一个元素的时间和链表中是相同的,都为O(n),但是在实践中散列表效率是很高的,查找一个元素的期望的时间为O(1);

缺点:

它是基于数组的数组创建完后扩展比较难所以当哈希表被填满的时候,性能会下降很多;所以,最好是知道表中要存储多少数据;


2. 理解寻址

在理解Hashmap之前,先理解哈寻址


直接寻址方式:

 

 

 

 

 哈希寻址:

 

 

 

关键字是k的元素被散列到槽h(k);

 


所以现在就剩下几个问题:


1.如何哈希化


//JDK源码

 final int hash(Object k) {

        int h = hashSeed;

        if (0 != h && k instanceof String) {

            return sun.misc.Hashing.stringHash32((String) k);

        }

 

        h ^= k.hashCode();

 

        // This function ensures that hashCodes that differ only by

        // constant multiples at each bit position have a bounded

        // number of collisions (approximately 8 at default load factor).

        h ^= (h >>> 20) ^ (h >>> 12);

        return h ^ (h >>> 7) ^ (h >>> 4);

    }

 

哈希函数的设计其实很有讲究, 目标就是尽量减少冲突,同时把寻址控制在一定范围内;

 具体的理论现在还理解不了, 源码的分析可以参考:

http://pengranxiang.iteye.com/blog/543893


 

2.如何解决冲突

  指定的数组大小是需要存储的数据量的两倍,因此,可能有一半的单元是空的.

当冲突发生

方法一:找到数组的一个空位,把数据插入,称为开放地址法;

 

方法二:创建一个存放链表的数组数组内不直接存放数据,这样当冲突发生,新的数据项直接接到这个数组下标所指的链表中;(链地址法)

 

2.1 开放地址法:

一种简单的就是:当要插入的数据的位置是1234, 如果位置被占了那么就看看1235, 以此类推,直到找到空位这样的方式叫线性探测;

当然,还有其他更好的改进的探测方法,就不仔细说了;

 

 

2.2 链地址法:

在链地址法中,如果需要在N个单元的数组中存放大于N个数据,因此装填因子大于1;

装填因子为2/3左右的时候,开发地址法的哈希表效率会下降很多而链地址法当因子为大于1,且对性能影响不是很大;

当然如果链表中有许多项存储时间会变长因为存储特定的数据需要搜索链表一半的长度;

 

2.3 JDK的链地址法具体实现

 (这部分原文是来自 http://xiaolu123456.iteye.com/blog/1485349)


public V put(K key, V value) {  

        if (key == null)  

            return putForNullKey(value);  

        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;  

            /*判断当前确定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那么新值覆盖原来的旧值,并返回旧值。  

            如果存在相同的hashcode,那么他们确定的索引位置就相同,这时判断他们的key是否相同,如果不相同,这时就是产生了hash冲突。  

            Hash冲突后,那么HashMap的单个bucket里存储的不是一个 Entry,而是一个 Entry 链。  

            系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),  

            那系统必须循环到最后才能找到该元素。  

*/

            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  

                V oldValue = e.value;  

                e.value = value;  

                return oldValue;  

            }  

        }  

        modCount++;  

        addEntry(hash, key, value, i);  

        return null;  

    }  

 


理解HASHMAP冲突最重要的一句话冲突是不可避免的,所以要去解决但是要尽最大努力,减少冲突的机会;

个人的理解是:减少冲突一方面是体现在哈希函数的设计上另外,作为使用者也要注意下容量是否合适;

 

HashMapAPI里面有一句:

通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。

 

3. 哈希表怎么扩容

上面提到了默认的加载因子为0.75, 那么什么时候JDK里面的Hashmap数组会扩容扩多大?

在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。

如果 初始容量*加载因子<大数据条目,则会发生扩容操作。 

//JDK源码 

[java] view plain copy
 print?
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.         if ((size >= threshold) && (null != table[bucketIndex])) {  
  3.             resize(2 * table.length);  
  4.             hash = (null != key) ? hash(key) : 0;  
  5.             bucketIndex = indexFor(hash, table.length);  
  6.         }  
  7.   
  8.         createEntry(hash, key, value, bucketIndex);  
  9.     }  
[java] view plain copy
 print?
  1.  * @param newCapacity the new capacity, MUST be a power of two;  
  2.      *        must be greater than current capacity unless current  
  3.      *        capacity is MAXIMUM_CAPACITY (in which case value  
  4.      *        is irrelevant).  
  5.      */  
  6.     void resize(int newCapacity) {  
  7.         Entry[] oldTable = table;  
  8.         int oldCapacity = oldTable.length;  
  9.         if (oldCapacity == MAXIMUM_CAPACITY) {  
  10.             threshold = Integer.MAX_VALUE;  
  11.             return;  
  12.         }  
  13.   
  14.   
  15.         Entry[] newTable = new Entry[newCapacity];  
  16.         transfer(newTable, initHashSeedAsNeeded(newCapacity));  
  17.         table = newTable;  
  18.         threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);  
  19.     }  


每次在原来的基础上增大1(table.lenght*2)


所以在使用的过程中, 合理使用扩容.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章