哈希的概念:Hash,一般翻譯做“散列”,也有直接音譯為“哈希”的,就是把任意長度的輸入(又叫做預映射, pre-image),通過散列算法,變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小於輸入的空間,不同的輸入可能會散列成相同的輸出,而不可能從散列值來唯一的確定輸入值。簡單的說就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數。
哈希的用途:Hash主要用於信息安全領域中加密算法,它把一些不同長度的信息轉化成雜亂的128位的編碼,這些編碼值叫做HASH值. 也可以說,Hash就是找到一種數據內容和數據存放地址之間的映射關系。
哈希表的概念:哈希表(Hash table,也叫散列表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數(哈希函數),存放記錄的數組叫做散列表。數組的各個欄叫做槽(buckets或者slots)。
哈希表的模型如下所示:
哈希表的過程:key經過hash函數作用后得到一個槽的索引(index),槽中保存着我們想要獲取的值(value)。
因為哈希表是基於數組實現的,所以可以實現隨機存取,訪問速度極快。但是在有的時候可能會發生不同的key經過哈希函數計算后得到同一個槽的索引號的情況(概率很低)。這種情況稱為沖突(碰撞)。如果碰撞發生了,采用單純的數組實現哈希表顯然不現實,必須加以解決。對於碰撞的解決方案是采用“拉鏈法”(open hashing)。
拉鏈法模型如下:
在拉鏈法模型中:槽,也就是數組的每一欄,存儲的不再是value值,而是一個鏈表的頭指針。發生沖突的元素都放在同一張鏈表中,默認按照插入順序依次進行鏈表的頭插入。在這種情況下,哈希表就像是一個“鏈表的數組”。它仍然可以實現快速的訪問,同時也解決了沖突。
不過如果沖突發生的非常頻繁,那么鏈表長度會很長。不妨考慮極端的情況,所有元素都集中在一個槽中,那么整個哈希表就變成了一個鏈表!這種情況下,插入和刪除操作效率極低,顯然不是我們想看到的,所以一個好的哈希函數必須要求盡量減少沖突發生的概率,也就是要求數據分布盡量均勻。
在哈希表長度一定的情況下,數據分布均勻的目標是通過哈希算法(散列方法)實現的。
散列方法主要有:
1、除法散列法 :公式: index =hashcode % length
但是由於位運算速度遠快於求模運算,所以一般使用按位與運算進行求模,公式為:index = hashcode &(length-1)。不過這種方法要求length必須為2的整數次方時,兩個公式才相等。因為當length為2的整數次方時,length-1的二進制表示全部為1,所以跟hashcode進行按位與運算可以得到槽索引,范圍為[0,length)。
2、平方散列法
求index是非常頻繁的操作,而乘法的運算要比除法來得省時,所以我們考慮把除法換成乘法和一個位移操作。公式:
index = (hashcode * hashcode) >> 28 (右移,除以2^28。記法:左移變大,是乘。右移變小,是除)
這種方法如果hashcode值不大的話,其平方值也不會很大,那么其二進制高位幾乎全為0。最后經過位移運算的結果肯定為0。那么hashcode不大的情況下,全部得到索引號為0,這種沖突顯然不想看到。所以要求hashcode必須足夠大。
3、斐波那契(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類底層其實就是維護一張哈希表。