數據結構(二)--散列

什麼是散列

散列首先是一個數組,所以它的訪問速度快,它與數組的不同在於可以通過一個關鍵字訪問數據,這個關鍵字與數組下標之間的映射關係有散列函數提供。
在這裏插入圖片描述

散列函數

所以說,散列函數對於一個散列是非常重要的,它的第一個主要功能就是將關鍵字映射到數組下標。比較簡單的散列函數,比如關鍵字是正整數,那麼散列函數可以是將關鍵字與數組長度進行取餘操作,獲取到下標值。或者關鍵字是字符串,我們通過某個方法,比如ASCII碼將字符串轉換爲數字,然後再進行取餘操作。

散列函數第二個主要功能是將關鍵字映射得到的下標儘量分散,比如我們有一個長度爲8的散列,最好的情況是插入8個元素分別在不同的下標,最壞的情況是8個元素的關鍵字都得到相同的下標,這樣就造成了hash衝突。所以爲了減少hash衝突,散列函數需要儘量的分散,下面是《數據結構與算法分析》中提出的比較好的散列函數

public static int hash(String key, int tableSize) {
	int hashVal = 0;
	// 遍歷每一個字符
	for(int i = 0; i < key.length(); i++) {
		hashVal = 37 * hashVal + key.charAt(i);
	}
	
	hashVal %= tableSize;
	// 這裏算出來的值可能溢出爲負
	if(hashVal < 0)
		hashVal += tableSize;

	return hashVal;
}

在這裏插入圖片描述

hash衝突

由於數組長度是有限的,所以散列函數再完美也是無法避免hash衝突的。那麼接下來的問題就是如何解決hash衝突。

分離鏈表法

分離鏈表法是將衝突的元素按照鏈表存儲在數組中,也就是散列表存儲一個鏈表數組。分離鏈表法實現簡單,缺點是在鏈表長度過長會導致查找耗時,所以需要在長度達到一定程度的時候做些處理,在java中,HashMap使用分離鏈表法解決hash衝突,當鏈表長度超過閾值時,會將鏈表轉換爲樹形結構存儲。

代碼見https://github.com/serpmelon/java_pk/blob/master/src/main/java/com/togo/java/data/structure/hashing/SeparateChainingHashTable.java

探測散列表

在分離鏈表法中,我們實現了另一種數據結構(鏈表)來解決hash衝突,在某些情況下我們可能不希望再實現另一種數據結構,那麼我們只能在原表中尋找空的單元,所以探測散列表一般比分離鏈表法的表更大。
hi(x) = (hash(x) + f(i)) mod tableSize; f(i)是解約衝突的方法。

線性探測法

在線性探測法中,f(i)是i的線性函數,比如f(i) = i;這就相當於逐個位置查找,直到找到空單元。所以只要表足夠大,肯定是可以找打空單元的。但是由於f(i)線性的關係,散列後的單元會形成一個區塊,這樣會導致,在表空間還比較空的時候,散列到區塊中的任何關鍵字都要經過多次操作才能解決衝突,這就是一次聚集

平方探測法

平方探測就是形如f(x) = i^2; 在一定程度上解決了一次聚集,可能會導致二次聚集;平方探測需要注意一點,如果哈希表的大小比較特殊,可能導致還有空間但是無法找到位置的情況,因爲
它不像線性探測可以遍歷整張表,平方可能會始終在某幾個位置來回跳轉。所以如下定理很重要:
如果使用平方探測,且表的大小是素數,那麼當表至少有一半是空的時候,總能夠插入一個新的元素。

雙散列

雙散列就是f(x)=hasn2(x),與前面兩個沒有本質區別,只是函數的差別,它對於散列表的要求也是素數。

再散列

當容量達到一定程度的時候需要擴容,擴容後元素肯定沒辦法匹配之前的位置,所以需要再散列。
再散列的花銷很大,但是發生的次數比較少,一般再散列是再裝填因子達到某個值時觸發(hashmap是0.75)。
以實際情況舉例,比如Java中的HashMap,rehash的大概步驟如下(只考慮有值的情況):
1、設置擴容後的數組大小,如果已經爲Integer.MAX則不擴容;其他情況數組大小擴大兩倍;
2、遍歷數組將數組中的元素放置在新數組中,HashMap使用的是分離鏈表法,所以也需要對於元素的鏈表進行遍歷。HashMap對於鏈表有個優化,當鏈表長度過長(默認是8)會將鏈表轉換爲紅黑樹,所以遍歷的時候需要考慮這兩種情況。

//TODO 聽過更好的rehash策略,可以不用移動舊數據,後面補充。

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