數據結構基礎:哈希表(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)


所以在使用的過程中, 合理使用擴容.

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