Hash表的原理

哈希的概念:Hash,通常翻譯作「散列」,也有直接音譯爲「哈希」的,就是把任意長度的輸入(又叫作預映射, pre-image),經過散列算法,變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間一般遠小於輸入的空間,不一樣的輸入可能會散列成相同的輸出,而不可能從散列值來惟一的肯定輸入值。簡單的說就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數。java

哈希的用途:Hash主要用於信息安全領域中加密算法,它把一些不一樣長度的信息轉化成雜亂的128位的編碼,這些編碼值叫作HASH值. 也能夠說,Hash就是找到一種數據內容和數據存放地址之間的映射關係。算法

哈希表的概念:哈希表(Hash table,也叫散列表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它經過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫作散列函數(哈希函數),存放記錄的數組叫作散列表。數組的各個欄叫作槽(buckets或者slots)。數組

哈希表的模型以下所示:安全

哈希表的過程:key通過hash函數做用後獲得一個槽的索引(index),槽中保存着咱們想要獲取的值(value)。數據結構

由於哈希表是基於數組實現的,因此能夠實現隨機存取,訪問速度極快。可是在有的時候可能會發生不一樣的key通過哈希函數計算後獲得同一個槽的索引號的狀況(機率很低)。這種狀況稱爲衝突(碰撞)。若是碰撞發生了,採用單純的數組實現哈希表顯然不現實,必須加以解決。對於碰撞的解決方案是採用「拉鍊法」(open hashing)。函數

拉鍊法模型以下:性能

在拉鍊法模型中:槽,也就是數組的每一欄,存儲的再也不是value值,而是一個鏈表的頭指針。發生衝突的元素都放在同一張鏈表中,默認按照插入順序依次進行鏈表的頭插入。在這種狀況下,哈希表就像是一個「鏈表的數組」。它仍然能夠實現快速的訪問,同時也解決了衝突。編碼

不過若是衝突發生的很是頻繁,那麼鏈表長度會很長。不妨考慮極端的狀況,全部元素都集中在一個槽中,那麼整個哈希表就變成了一個鏈表!這種狀況下,插入和刪除操做效率極低,顯然不是咱們想看到的,因此一個好的哈希函數必需要求儘可能減小衝突發生的機率,也就是要求數據分佈儘可能均勻。加密

在哈希表長度必定的狀況下,數據分佈均勻的目標是經過哈希算法(散列方法)實現的。翻譯

散列方法主要有:

一、除法散列法 :公式: index =hashcode % length

 可是因爲位運算速度遠快於求模運算,因此通常使用按位與運算進行求模,公式爲:index = hashcode &(length-1)。不過這種方法要求length必須爲2的整數次方時,兩個公式才相等。由於當length爲2的整數次方時,length-1的二進制表示所有爲1,因此跟hashcode進行按位與運算能夠獲得槽索引,範圍爲[0,length)。

二、平方散列法 

求index是很是頻繁的操做,而乘法的運算要比除法來得省時,因此咱們考慮把除法換成乘法和一個位移操做。公式: 

index = (hashcode * hashcode) >> 28   (右移,除以2^28。記法:左移變大,是乘。右移變小,是除)

這種方法若是hashcode值不大的話,其平方值也不會很大,那麼其二進制高位幾乎全爲0。最後通過位移運算的結果確定爲0。那麼hashcode不大的狀況下,所有獲得索引號爲0,這種衝突顯然不想看到。因此要求hashcode必須足夠大。

三、斐波那契(Fibonacci)散列法

平方散列法的缺點是顯而易見的,因此咱們能不能找出一個理想的乘數,而不是拿hashcode自己看成乘數呢?答案是確定的。

對於16位整數而言,這個乘數是40503。

對於32位整數而言,這個乘數是2654435769。

對於64位整數而言,這個乘數是11400714819323198485。

這幾個「理想乘數」是如何得出來的呢?這跟一個法則有關,叫黃金分割法則,而描述黃金分割法則的最經典表達式無疑就是著名的斐波那契數列,即如此形式的序列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946,…。是否是以爲很神奇,可能這就是數學之美吧。

 

經過採用適當的散列方法,咱們能夠控制數據儘可能均勻地分佈在槽中。可是不妨再考慮一個問題:若是一個哈希表被建立了,剛開始全部的槽都是空的。這時候傳入一部分數據,數據經過哈希函數應該是能夠均勻分佈在數組的各個槽中的。偶爾會有小几率的數據發生衝突,被存儲在同一個鏈表中,問題不大。可是隨着數據的增多,空槽的數量愈來愈少,發生衝突的機率愈來愈大。爲了解決這個問題,咱們引入了負載因子和再哈希的概念。

再哈希:指的是當槽的利用率(已使用槽與總槽數的比值)達到負載因子時,哈希表會就地擴容,具體過程爲調用resize()方法,將哈希表的容量變爲原來的兩倍。以後對全部的數據從新進行散列過程,存儲到相應的位置。

負載因子:再哈希發生的閾值。

要注意的是,再哈希的工做量是很大的,由於要對全部數據進行散列過程。因此,哈希表的長度和負載因子選取要合適。在負載因子必定的狀況下,若是長度太小,再哈希就會頻繁發生,這會嚴重影響性能;若是長度設置過大,雖然再哈希發生的頻率很低,可是會浪費空間。同理,負載因子若是選取過大,那麼在再哈希發生以前,就會產生大量的衝突(由於槽位基本已滿);若是負載因子選取太小,那麼再哈希就會頻繁發生,也會影響性能。通常默認長度爲16,負載因子爲0.75。

哈希表的應用:java.util.HashMap類就是基於哈希表實現的。當經過HashMap對象查找某個key對應的value值過程爲:先將傳入的鍵key經過hashCode()方法獲得哈希值hash,再經過哈希函數獲得槽的索引號,該索引處存儲的是指向某一個鏈表的引用。繼續經過equals方法遍歷比較鏈表上的每個對象,便可定位到最終的鍵值對應的Entry對象(鍵值對)。因此,HashMap類底層其實就是維護一張哈希表。

相關文章
相關標籤/搜索