散列表(Hash table,也叫哈希表),是根據鍵(Key)而直接訪問在內存存儲位置的數據結構。也就是說,它經過計算一個關於鍵值的函數,將所需查詢的數據映射到表中一個位置來訪問記錄,這加快了查找速度。 這個映射函數稱作散列函數,存放記錄的數組稱作散列表。java
首先來講hash函數,java中對象都已一個hashCode()方法,那爲何還須要hash函數呢?hashCode是在jdk中是有符號int類型,這個一個很大的範圍,若是散列表的數組能覆蓋全部int值的話,就不須要hash函數了,固然內存不容許咱們維護這麼大的散列表。這時咱們須要hash函數將原始hashCode映射到一個很小的數組上去。意思就是將超大超長或不定長的整形數據轉換爲惟一(理想狀況,對於不一樣對象hash值應該不相同)的定長的hash值,常見的作法是取模法,也是jdk中的實現方式。算法
1 static final int hash(Object key) { 2 int h; 3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 4 } 5 6 static int indexFor(int h, int length) { 7 return h & (length-1); 8 }
第一個hash函數有人稱之爲「擾動函數」,第二個indexFor函數在jdk8中去掉了,函數內的代碼合併到了putVal中,我的認爲這兩個函數合併起來是一個完整的hash函數。
h & (length-1) 這段代碼的做用其實就是取模,假設數組初始化長度爲16,那麼length-1的結果爲15,對應二進制爲00001111,若是咱們有一個大小爲20的key,對應二進制爲00010100,與運算後結果爲00000100,對應十進制爲4。
這裏數組的長度必須爲2的次冪。因爲對key進行了取模運算,因此咱們知道當length=16的時候,咱們會捨棄調掉key高位的值,只保留了低4位。原本int是32位,只是用低4位衝突是否是太容易發生了?
因此第一個「擾動函數」的做用出現了,這個函數將key自己高16和低16位作了異或運算。 數組
儘管實現瞭如此有效的散列算法,但只是將不一樣對象之間hash碰撞的機率下降了,仍是不能徹底保證不發生hash衝突,所以要繼續使用hash表的優勢就要解決hash衝突的問題。數據結構
用開放定址法解決衝突的作法是:當衝突發生時,使用某種探查(亦稱探測)技術在散列表中造成一個探查(測)序列。沿此序列逐個單元地查找,直到找到給定的關鍵字,或者碰到一個開放的地址(即該地址單元爲空)爲止(若要插入,在探查到開放的地址,則可將待插入的新結點存人該地址單元)。查找時探查到開放的 地址則代表表中無待查的關鍵字,即查找失敗。函數
當哈希表愈來愈滿時彙集愈來愈嚴重,這致使產生很是長的探測長度,後續的數據插入將會很是費時。一般數據超過三分之二滿時性能降低嚴重,所以設計哈希表關鍵確保不會超過這個數據容量的一半,最多不超過三分之二。性能
按照造成探查序列的方法不一樣,可將開放定址法區分爲線性探查法、線性補償探測法、隨機探測等。spa
(1)線性探查法(Linear Probing)設計
d,d+l,d+2,…,m-1,0,1,…,d-1
即:探查時從地址d開始,首先探查T[d],而後依次探查T[d+1],…,直到T[m-1],此後又循環到T[0],T[1],…,直到探查到T[d-1]爲止。指針
①若當前探查的單元爲空,則表示查找成功(如果插入則將key寫入其中);
②若當前探查的單元中含有key,則查找成功,但對於插入意味着失敗;
③若探查到T[d-1]時仍未發現空單元,則不管是查找仍是插入均意味着失敗(此時表滿,須要擴容)。code
hi=(h(key)+i)%m 0≤i≤m-1 //i=1
①哈希表容量不能徹底利用,而且擴容將會是災難的,須要刪除之前標記過的元素並須要重新計算全部元素的位置,在頻繁的刪除和插入時效率變得很低。
②按上述算法創建起來的哈希表,刪除工做很是困難。假如要從哈希表 HT 中刪除一個記錄,按理應將這個記錄所在位置置爲空,但咱們不能這樣作,而只能標上已被刪除的標記,不然,將會影響之後的查找。
③線性探測法很容易產生堆聚現象。所謂堆聚現象,就是存入哈希表的記錄在表中連成一片。按照線性探測法處理衝突,若是生成哈希地址的連續序列愈長(即不一樣關鍵字值的哈希地址相鄰在一塊兒愈長),則當新的記錄加入該表時,與這個序列發生衝突的可能性愈大。所以,哈希地址的較長連續序列比較短連續序列生長得快,這就意味着,一旦出現堆聚(伴隨着衝突),就將引發進一步的堆聚。
(2)線性補償探測法
線性補償探測法的基本思想是將線性探測的步長從 1 改成 Q ,即將上述算法中的 hi=(h(key)+i)%m改成:hi=(h(key)+Q)%m,這個Q是根據必定的增加率變化的(一、四、9...),這樣使得數據分佈的足夠散亂,不容易出現聚堆現象,並且要求 Q 的變化能使全表獲得完整的掃描,以便能探測到哈希表中的全部單元(固然還有其餘的線性在散列算法規則,這裏只討論該種方式的再散列)。
(3)隨機探測
隨機探測的基本思想是將線性探測的步長從常數改成隨機數,即令:hi=(h(key)+RN)%m ,其中 RN 是一個隨機數。在實際程序中應預先用隨機數發生器產生一個隨機序列,將此序列做爲依次探測的步長。這樣就能使不一樣的關鍵字具備不一樣的探測次序,從而能夠避免或減小堆聚。基於與線性探測法相同的理由,在線性補償探測法和隨機探測法中,刪除一個記錄後也要打上刪除標記。
拉鍊法解決衝突的作法是將全部關鍵字爲同義詞的結點連接在同一個單鏈表中。若選定的散列表長度爲m,則可將散列表定義爲一個由m個頭指針組成的指針數 組T[0..m-1]。凡是散列地址爲i的結點,均插入到以T[i]爲頭指針的單鏈表中。T中各份量的初值均應爲空指針。在拉鍊法中,裝填因子α能夠大於 1,但通常均取α≤1。
①拉鍊法處理衝突簡單,且無堆積現象,即非同義詞決不會發生衝突,所以平均查找長度較短;
②因爲拉鍊法中各鏈表上的結點空間是動態申請的,故它更適合於造表前沒法肯定表長的狀況;
③開放定址法爲減小衝突,要求裝填因子α較小,故當結點規模較大時會浪費不少空間。而拉鍊法中可取α≥1,且結點較大時,拉鍊法中增長的指針域可忽略不計,所以節省空間;
④在用拉鍊法構造的散列表中,刪除結點的操做易於實現。只要簡單地刪去鏈表上相應的結點便可。而對開放地址法構造的散列表,刪除結點不能簡單地將被刪結 點的空間置爲空,不然將截斷在它以後填人散列表的同義詞結點的查找路徑。這是由於各類開放地址法中,空地址單元(即開放地址)都是查找失敗的條件。所以在 用開放地址法處理衝突的散列表上執行刪除操做,只能在被刪結點上作刪除標記,而不能真正刪除結點。
當發生hash衝突時,須要生成鏈表由此須要額外佔用空間,而且須要花必定的時間和精力維護衝突鏈表。在擴容的時候須要把全部元素進行從新hash並分配地址,算法較爲複雜繁瑣。
再hash法,就是算hashcode的方法不止一個,一個要是算出來重複啦,再用另外一個算法去算。使用必定的算法邏輯的到一種在當前狀況不會發生hash衝突的hash算法。
創建一個公共溢出區域,就是把衝突的都放在另外一個地方,不在表裏面。具體實現不作探討了(不經常使用)。