一.什麼是散列java
散列使用一個散列函數,將一個鍵映射到一個索引上。
散列很是高效。使用散列將耗費O(1)時間來查找、插入、及刪除一個元素。數組
映射表是一種用散列實現的數據結構,映射表是一種存儲條目的容器,每一個條目包含兩個部分:一個鍵(key)和一個值(value)。鍵又稱爲搜索鍵用於查找對應的值。
映射表(map)又稱爲字典(dictionary)、散列表(hash table)或者關聯數組(associate array)。數據結構
Java合集框架定義了java.util.Map接口,三個具體的的實現爲java.util.HashMap、java.util.linkedHashMap以及java.util.TreeMap。java.util.HashMap使用散列實現,java.util.linkedHashMap使用linkedList,java.util.TreeMap使用紅黑樹。多線程
二.散列函數和散列碼
框架
1.散列函數
將鍵映射到散列表的索引上的函數稱爲散列函數(hash function)。理想的,將每一個搜索的鍵映射到散列表中的不一樣索引上,這樣的函數稱爲完美散列函數。然而,很難找到一個完美的散列函數,當兩個或更多的鍵映射到一個散列值上的時候,咱們稱之爲產生了一個衝突(collision)。函數
典型的散列函數首先將搜索鍵轉換成爲一個整數值,稱之爲散列碼。而後將散列碼壓縮爲散列表中的索引。this
2.equals和hashCode
Java的根類Object具備hashCode方法,返回一個整數的散列碼。默認的,該返回值是一個該對象的內存地址。hashCode通常有以下約定:
1)當equals方法被重寫時,應該重寫hashCode方法,以保證兩個相等的對象返回一樣的散列碼。
2)程序執行中,若是對象的數據沒有被修改,則屢次調用hashCode將返回一樣的整數。
3)兩個不相等的對象可能具備一樣的散列碼,可是應該在實現hashCode方法時避免太多這樣的情形出現。spa
3.基本數據類型的散列碼
對於byte、short、int、char類型,簡單講它們轉爲int,這些類型中的任何一個不一樣的搜索鍵將有不一樣的散列嗎。
對於float,使用Float.floatToIntBits(key)做爲散列碼,方法返回一個int值。該值得比特表示和浮點數f的比特表示相同。
對於long類型的搜索鍵,簡單地轉爲int不是很好的選擇,由於沒法反應前面32高位的不一樣。考慮到這種狀況,將64比特分爲兩部分,並執行異或操做將兩部分結合,這個過程稱爲摺疊(folding)。
一個long類型鍵的散列碼爲:線程
int hashCode=(int)(key^(key>>32));
對於double類型的搜索鍵,首先使用Double.doubleToLongBits方法轉爲long值,再執行摺疊操做。code
4.字符串類型的散列碼
一個比較直觀的方法是將全部字符的unicode求和做爲字符串的散列碼。這個方法可能有較大的衝突,也沒法區分dog與dgo。
一個更好的方法是考慮字符的位置,而後產生散列碼:
這裏Si爲s.charAt(i),這個方法被稱爲多項式散列碼。計算時,對於長的字符串會致使溢出,但Java中會忽略溢出。要最小化衝突,關鍵是選擇合適的b,實驗顯示,b較好的取值爲31,33,37,39,41。
java.lang.String.java中的多項式散列實現:
/** * Returns a hash code for this string. The hash code for a * {@code String} object is computed as * <blockquote><pre> * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] * </pre></blockquote> * using {@code int} arithmetic, where {@code s[i]} is the * <i>i</i>th character of the string, {@code n} is the length of * the string, and {@code ^} indicates exponentiation. * (The hash value of the empty string is zero.) * * @return a hash code value for this object. */
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
5.壓縮散列碼
鍵的散列碼多是一個很大的整數,超過了散列表索引的範圍,所以須要將它縮小。假設散列表的索引處於0到N-1之間,經常使用的壓縮作法是:
h(hashCode)=hashCode%N
保證索引均勻擴展,選擇N大於2的素數。
上面的式子等價於:
h(hashCode)=hashCode&(N-1)
爲保證散列是均勻分佈的,java.util.HashMap的實現中採用了補充的散列函數與主散列函數一塊兒使用:
private static int supplementalHash(int h){ h^=(h>>>20)^(h>>>12); return h^(h>>>7)^(h>>>4); }
完整的散列函數:
h(hashCode)= supplementalHash(hashCode)%N
這個式子與下面式子同樣:
h(hashCode)= supplementalHash(hashCode) &(N-1)
三. 開放地址法
當兩個鍵映射到散列表中的同一個索引,衝突發送,一般有2種方法處理衝突:開放地址法與鏈地址法。
開放地址法有如下幾個變體。
1.線性探測
線性探測法在發生衝突時,按順序找到下一個可用的位置。連續衝突時繼續按序檢查,直到找到可用位置。
當探測到表的終點時,則返回表的起點。所以,散列表被當成是循環的。
線性探測法容易致使散列表中連續的單元組被佔用。這樣的每一個組稱爲一個簇(cluster)。
2.二次探測
線性探測法從索引k的位置檢查連續單元,二次探測法則從索引爲(k+j^2)%N位置開始檢查,其中j>=0。
二次探測法避免了線性的成簇問題,但有本身自己的成簇問題,稱爲二次成簇,即產生衝突的條目將採用一樣的探測序列。
3.再哈希法
避免成簇問題的另外一個方法是再哈希法。
四. 鏈地址法
鏈地址法將具備相同的散列索引的條目都放在一個位置,每一個位置使用一個桶來放置這些條目。
鏈地址法是種普遍使用的方法。
(在JDK中有很多基於此方法的實現,如jdk1.7的hasMap實現方案,在多線程環境中發生鏈循環等錯誤,jdk1.8中重寫了鏈的處理,修正了此錯誤等故事。)
五. 裝填因子和再散列
裝填因子(load factor)衡量一個散列表有多滿。若是裝填因子超出,則增長散列表的大小,並從新裝載條目到一個新的更大的散列表中,這稱爲再散列。
裝填因子=條目數n/容量N,若是散列表滿了,裝填因子=1
當裝填因子接近1時,衝突的可能性就增大。通常對於開發地址法,裝填因子須要控制在0.5下,鏈地址法一般維持在0.9下。
在java.util.HashMap的實現中,採用了裝填因子0.75的閾值。一旦超過閾值,就須要增長散列表的大小,並進行再散列。再散列的代價比較大,爲避免頻繁的再散列,一旦擴容時應該至少將散列表的大小翻倍。
/** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */
static final int MAXIMUM_CAPACITY = 1 << 30; /** * The load factor used when none specified in constructor. */
static final float DEFAULT_LOAD_FACTOR = 0.75f;