java基礎——哈希衝突

轉發http://baijiahao.baidu.com/s?id=1586706502510642738&wfr=spider&for=pc

在Java編程語言中,最基本的結構就是兩種,一種是數組,一種是模擬指針(引用),所有的數據結構都可以用這兩個基本結構構造,HashMap也一樣。

HashMap的底層實現原理:
這裏寫圖片描述
在JAVA中,每個對象都有一個散列碼,它是由Object類的hashCode()方法計算得到的(當然也可以覆蓋Object的hashCode())。而我們可以在散列碼的基礎上,定義一個哈希函數,再對哈希函數計算出的結果求餘,最終得到該對象在哈希表的位置。

HashMap 採用一種所謂的“Hash 算法”來決定每個元素的存儲位置。當程序執行 map.put(String,Obect)方法 時,系統將調用String的 hashCode() 方法得到其 hashCode 值——每個 Java 對象都有 hashCode() 方法,都可通過該方法獲得它的 hashCode 值。得到這個對象的 hashCode 值之後,系統會根據該 hashCode 值來決定該元素的存儲位置。

哈希衝突的產生及解決辦法:

1、產生衝突

這裏寫圖片描述
上圖就是一個散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。

當向HashMap中put數據的時候,首先要判斷當前確定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那麼新值覆蓋原來的舊值,並返回舊值。

如果存在相同的hashcode,那麼他們確定的索引位置就相同,這時判斷他們的key是否相同,如果不相同,這時就是產生了Hash衝突。

2、解決衝突

HashMap裏面沒有出現hash衝突時,沒有形成單鏈表時,hashmap查找元素很快,get()方法能夠直接定位到元素,但是出現單鏈表後,單個bucket 裏存儲的不是一個 Entry,而是一個 Entry 鏈,系統只能必須按順序遍歷每個 Entry,直到找到想搜索的 Entry 爲止——如果恰好要搜索的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最早放入該 bucket 中),那系統必須循環到最後才能找到該元素。

當系統決定存儲 HashMap 中的 key-value 對時,完全沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算並決定每個 Entry 的存儲位置。我們完全可以把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的存儲位置之後,value 隨之保存在那裏即可。
這裏寫圖片描述
Hashmap裏面的bucket出現了單鏈表的形式,散列表要解決的一個問題就是散列值的衝突問題,通常的解決方法如下:

1、鏈地址法

它有一個桶的概念:對於Entry數組而言,將相同hash值的對象組織成一個鏈表放在hash值對應的槽位。在鏈表中的每個元素纔是真正的。而一個鏈表,就是一個桶!因此HashMap最多可以有Entry.length個桶。

2、開放地址法

開放定址法有兩種處理方式:一種是線性探測,另一種是平方探測。

線性探測:依次探測衝突位置的下一個位置。如,在哈希表的位置2處發生了衝突,則探測位置3處是否被使用了,若被使用了,則探測位置4……直至下一個被探測的位置爲空(意味着還有位置可以插入元素—插入成功)或者探測了N-1(N爲哈希表的長度)個元素又回到了原始的衝突位置處(意味着已經沒有位置可供新元素插入了—插入失敗)

因此,插入一個元素時,最壞情況下的時間複雜度爲O(N),因爲它有可能探測了N-1個元素!

平方探測:以平方大小來遞增下一次待探測的位置。如,在哈希表位置2處發生了衝突,則探測 (1^2=1)位置3(2+1),若位置3被使用了,則探測(2^2=4) 位置6(2+4),若位置6被使用了,則探測(3^2=9)位置11(2+9=11)……平方探測法有一個特點:對於任何一個給定的素數N(假設哈希表的長度設置爲素數),當計算( h(k) + i ^2 ) MOD N 時,隨着 i 的增長,得到的結果是循環的。

因此,當平方探測重複探測了某一個位置時,說明探測失敗即已經沒有位置可供新元素插入了,儘管此時哈希表並沒有滿。

平方探測是跳着探測的,它忽略了一些位置,而這些位置可能是空的。即在哈希表仍未滿的情況下,已經不能再插入新元素了

最壞情況下,平方探測需要檢測 N/2個位置,因此插入一個元素的最壞時間複雜度爲O(N)。

3、再散列法

建立多個hash函數,若是當發生hash衝突的時候,使用下一個hash函數,直到找到可以存放元素的位置。

4、建立公共溢出區

將哈希表分爲基本表和溢出表,將與基本表發生衝突的元素放入溢出表中。

底層的hashMap是由數組和鏈表來實現的,就是上面說的鏈地址法。首先當插入的時候,會根據key的hash值然後計算出相應的數組下標,計算方法是index = hashcode%table.length,(這個下標就是上面提到的bucket),當這個下標上面已經存在元素的時候那麼就會形成鏈表,將後插入的元素放到尾端,若是下標上面沒有存在元素的話,那麼將直接將元素放到這個位置上。

當進行查詢的時候,同樣會根據key的hash值先計算相應的下標,然後到相應的位置上進行查找,若是這個下標上面有很多元素的話,那麼將在這個鏈表上一直查找直到找到對應的元素。

關於Hash的更多問題

1、哈希過程爲什麼需要先根據hashCode得到一個值(又稱散列碼),然後再對該值求餘呢?

在JAVA中,Object類的hashCode()方法返回的是由調用對象的內存地址導出的一個值,也即,當沒有覆蓋Object類中的equals() 和 hashCode()時,只有當兩個對象的內存地址一樣時,才認爲兩個對象是相等的。這顯然不符合實際情況,比如Person類有 String id、String name…..顯然在現實中是根據id(身份證)不同來判斷兩個人不同。因此,需要進一步根據hashCode()值來封裝(如上面的 hash(Object k)方法),返回一個合理的散列碼。

2、那爲什麼又需要對得到的散列碼求餘呢?

底層是用數組來存儲的,而我們得到的散列碼可能很大(事實上散列碼的範圍非常廣)而內存是有限的,不能分配爲數組分配一塊很大很大的空間,因此,存儲的數組空間相對較小。從而需要把所有的散列碼都 “約束” 到這個有效的數組空間中。—-這也是導致衝突的根源

3、爲什麼使用HashMap查找是O(1)呢?

T value = hashmap.get(key)

①get(key)時,一步計算出該key所對應的底層數組array的 index (相當於上面 hash(Object k ) 和 indexFor(int h, int length) 這兩個函數完成的功能)

②value = array[index]

因此,就認爲查找的複雜度爲O(1)。

4、HashMap中的兩個變量及作用

int threshold:當HashMap中的元素個數超過threshold時,就會重新調整哈希的大小。

float loadFactor:loadFactor 默認是0.75,指定threshold,一般情況下,哈希表的大小乘以0.75等於threshold。

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