一、散列函數
散列是在記錄的存儲位置和它的關鍵字之間建立一個確定的對應關係f,使得每個關鍵字key對應一個存儲位置f(key)。建立了關鍵字與存儲位置的映射關係,公式如下:
存儲位置 =f(關鍵字)
設所有可能出現的關鍵字集合記爲U(簡稱全集),實際發生(即實際存儲)的關鍵字集合記爲K(|K|比|U|小得多)。
散列方法是使用函數f將U映射到表T[0..m-1]的下標上(m=O(|U|))。這樣以U中關鍵字爲自變量,以f爲函數的運算結果就是相應結點的存儲地址。從而達到在O(1)時間內就可完成查找。
其中:
- f:U(可能出現的全集)→{0,1,2,…,m-1} ,通常稱f爲散列函數(Hash Function)。散列函數f的作用是壓縮待處理的下標範圍,使待處理的|U|個值減少到m個值,從而降低空間開銷。
- T爲散列表(Hash Table)。
- f(Ki)(Ki∈U)是關鍵字爲Ki結點存儲地址(亦稱散列值或散列地址)。
- 將結點按其關鍵字的散列地址存儲到散列表中的過程稱爲散列(Hashing)
二、直接尋址法
散列的概念屬於查找,它不以關鍵字的比較爲基本操作,採用直接尋址技術。
在最壞情況下,散列表的查找和刪除的最壞時間代價爲O(n),但是在實際中,散列表的性能是很好的,在一些合理的假設下,散列表的查找和刪除操作的平均時間代價爲O(1)。
在說直接尋址法之前,先舉個例子:
現在有10個門,並且從1到10標好了號碼,你手中有5號門的鑰匙,那如何打開第五號門呢?肯定不是一個一個的去試,而是直接去開第5號門。
python的列表中也有通過key找到數據的思想,列表是可以直接訪問的,通過列表的起始地址加上一個偏移量得到目標數據的存儲地址。
list_a = [1,2,3,4]
list_a[2]
>>>3
這種方法是很快的,可以在O(1)時間內完成。直接尋址表就藉助了列表這種可直接訪問的優勢。
如果我們現在要對0-100歲的人口數字統計表,那麼我們對年齡這個關鍵字就可以直接用年齡的數字作爲地址。此時f(key) = key。
地址 | 年齡 | 人數 |
00 | 0 | 500萬 |
01 | 1 | 600萬 |
02 | 2 | 450萬 |
... | ... | ... |
20 | 20 | 1500萬 |
... | ... | ... |
這個時候,我們可以得出這麼個哈希函數:
f(0) = 0,f(1) = 1,……,f(20) = 20。
這個是根據我們自己設定的直接定址來的。人數我們可以不管,我們關心的是如何通過關鍵字找到地址。
如果我們現在要統計的是80後出生年份的人口數,那麼我們對出生年份這個關鍵字可以用年份減去1980來作爲地址。此時f (key) = key-1980。
地址 | 出生年份 | 人數 |
00 | 1980 | 500萬 |
01 | 1981 | 600萬 |
02 | 1982 | 450萬 |
... | ... | ... |
20 | 2000 | 1500萬 |
... | ... | ... |
假如今年是2000年,那麼1980年出生的人就是20歲了,此時 f(2000) = 2000 - 1980,可以找得到地址20,地址20裏保存了數據“人數500萬”。
也就是說,我們可以取關鍵字的某個線性函數值爲散列地址,即:
f(key) = a × key + b
這樣的散列函數優點就是簡單、均勻,也不會產生衝突,但問題是這需要事先知道關鍵字的分佈情況,適合査找表較小且連續的情況。由於這樣的限制,在現實應用中,直接尋址法雖然簡單,但卻並不常用。
三、除留餘數法
除留餘數法此方法爲最常用的構造散列函數方法。對於散列表長爲m的散列函數公式爲:
f( key ) = key mod p ( p ≤ m )
mod是取模(求餘數)的意思。事實上,這方法不僅可以對關鍵字直接取模,也可在摺疊、平方取中後再取模。
一個例子
-
很顯然,本方法的關鍵就在於選擇合適的p, p如果選得不好,就可能會容易產生同義詞。下面我們來舉個例子看看:
有一個關鍵字,它有12個記錄,現在我們要針對它設計一個散列表。如果採用除留餘數法,那麼可以先嚐試將散列函數設計爲如下方法:
f(key) = key mod 12
比如29 mod 12 = 5,所以它存儲在下標爲5的位置。
下標 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
關鍵字 | 12 | 25 | 38 | 15 | 16 | 29 | 78 | 67 | 56 | 21 | 22 | 47 |
不過這也是存在衝突的可能的,因爲12 = 2×6 = 3×4。如果關鍵字中有像18(3×6)、30(5×6)、42(7×6)等數字,它們的餘數都爲6,這就和78所對應的下標位置衝突了。
甚至極端一些,對於下圖的關鍵字,如果我們讓p爲12的話,就可能出現下面的情況,所有的關鍵字都得到了0這個地址數,這未免也太糟糕了點。
下標 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
關鍵字 | 12 | 24 | 36 | 48 | 60 | 72 | 84 | 96 | 108 | 120 | 132 | 144 |
但是我們如果不選用p=12來做除留餘數法,而選用p=11,則結果如下:
下標 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 0 | 1 |
關鍵字 | 12 | 24 | 36 | 48 | 60 | 72 | 84 | 96 | 108 | 120 | 132 | 144 |
這個時候就只有12和144有衝突,相對來說,就要好很多了。
如何合理選取p值
使用除留餘數法的一個經驗是,若散列表表長爲m,通常p爲小於或等於表長(最好接近m)的最小質數或不包含小於20質因子的合數。
這句話怎麼理解呢?再舉個例子:某散列表的長度爲100,散列函數H(k)=k%P,則P通常情況下最好選擇哪個呢?
A、91 B、93 C、97 D、99
實踐證明,當P取小於哈希表長的最大質數時,產生的哈希函數較好。所以選97,因爲它是離長度值最近的最大質數。