雜湊表
散列表(Hash table),是根據鍵(Key)而直接訪問在記憶體儲存位置的數據結構。也就是說,它通過計算出一個鍵值的函數,將所需查詢的數據映射到表中一個位置來讓人訪問,這加快了查找速度。這個映射函數稱做散列函數,存放記錄的數組稱做散列表。
一個通俗的例子是,為了查找電話簿中某人的號碼,可以創建一個按照人名首字母順序排列的表(即建立人名到首字母的一個函數關係),在首字母為W的表中查找「王」姓的電話號碼,顯然比直接查找就要快得多。這裡使用人名作為關鍵字,「取首字母」是這個例子中散列函數的函數法則,存放首字母的表對應散列表。關鍵字和函數法則理論上可以任意確定。
可以將散列表理解為一串按順序放的數組,數組的下標是從key經過計算得出,數組每個位置存放 value。這裡有很多將key轉換為下標的函數,比如取模,md5等。可以在哈希表可視化頁面 直觀操作,理解這裡的數據結構。
基本概念
- 若關鍵字為 ,則其值存放在 的存儲位置上。由此,不需比較便可直接取得所查記錄。稱這個對應關係 為散列函數,按這個思想建立的表為散列表。
- 對不同的關鍵字可能得到同一散列地址,即 ,而 ,這種現象稱為衝突(英語:Collision)。具有相同函數值的關鍵字對該散列函數來說稱做同義詞。綜上所述,根據散列函數 和處理衝突的方法將一組關鍵字映射到一個有限的連續的地址集(區間)上,並以關鍵字在地址集中的「像」作為記錄在表中的存儲位置,這種表便稱為散列表,這一映射過程稱為散列造表或散列,所得的存儲位置稱散列地址。
- 若對於關鍵字集合中的任一個關鍵字,經散列函數映象到地址集合中任何一個地址的概率是相等的,則稱此類散列函數為均勻散列函數(Uniform Hash function),這就使關鍵字經過散列函數得到一個「隨機的地址」,從而減少衝突。
構造散列函數
散列函數能使對一個數據序列的訪問過程更加迅速有效,通過散列函數,數據元素將被更快定位。
- 直接定址法:取關鍵字或關鍵字的某個線性函數值為散列地址。即 或 ,其中 為常數(這種散列函數叫做自身函數)
- 數字分析法:假設關鍵字是以r為基的數,並且哈希表中可能出現的關鍵字都是事先知道的,則可取關鍵字的若干數位組成哈希地址。
- 平方取中法:取關鍵字平方後的中間幾位為哈希地址。通常在選定哈希函數時不一定能知道關鍵字的全部情況,取其中的哪幾位也不一定合適,而一個數平方後的中間幾位數和數的每一位都相關,由此使隨機分布的關鍵字得到的哈希地址也是隨機的。取的位數由表長決定。
- 摺疊法:將關鍵字分割成位數相同的幾部分(最後一部分的位數可以不同),然後取這幾部分的疊加和(捨去進位)作為哈希地址。
- 隨機數法
- 除留餘數法:取關鍵字被某個不大於散列表表長m的數p除後所得的餘數為散列地址。即 , 。不僅可以對關鍵字直接取模,也可在摺疊法、平方取中法等運算之後取模。對p的選擇很重要,一般取素數或m,若p選擇不好,容易產生衝突。
處理衝突
為了知道衝突產生的相同散列函數地址所對應的關鍵字,必須選用另外的散列函數,或者對衝突結果進行處理。而不發生衝突的可能性是非常之小的,所以通常對衝突進行處理。常用方法有以下幾種:
- 開放定址法(open addressing): , ,其中 為散列函數, 為散列表長, 為增量序列, 為已發生衝突的次數。增量序列可有下列取法:
- 稱為線性探測(Linear Probing);即 ,或者為其他線性函數。相當於逐個探測存放地址的表,直到查找到一個空單元,把散列地址存放在該空單元。
- 稱為 平方探測(Quadratic Probing)。相對線性探測,相當於發生衝突時探測間隔 個單元的位置是否為空,如果為空,將地址存放進去。
- 偽隨機數序列,稱為 偽隨機探測。
顯示線性探測填裝一個散列表的過程:
- 關鍵字為{89,18,49,58,69}插入到一個散列表中的情況。此時線性探測的方法是取 。並假定取關鍵字除以10的餘數為散列函數法則。
散列地址 | 空表 | 插入89 | 插入18 | 插入49 | 插入58 | 插入69 |
---|---|---|---|---|---|---|
0 | 49 | 49 | 49 | |||
1 | 58 | 58 | ||||
2 | 69 | |||||
3 | ||||||
4 | ||||||
5 | ||||||
6 | ||||||
7 | ||||||
8 | 18 | 18 | 18 | 18 | ||
9 | 89 | 89 | 89 | 89 | 89 |
- 第一次衝突發生在填裝49的時候。地址為9的單元已經填裝了89這個關鍵字,所以取 ,往下查找一個單位,發現為空,所以將49填裝在地址為0的空單元。第二次衝突則發生在58上,取 ,往下查找3個單位,將58填裝在地址為1的空單元。69同理。
- 表的大小選取至關重要,此處選取10作為大小,發生衝突的幾率就比選擇質數11作為大小的可能性大。越是質數,mod取余就越可能均勻分布在表的各處。
聚集(Cluster,也翻譯做「堆積」)的意思是,在函數地址的表中,散列函數的結果不均勻地占據表的單元,形成區塊,造成線性探測產生一次聚集(primary clustering)和平方探測的二次聚集(secondary clustering),散列到區塊中的任何關鍵字需要查找多次試選單元才能插入表中,解決衝突,造成時間浪費。對於開放定址法,聚集會造成性能的災難性損失,是必須避免的。
- 雙散列。
- 建立一個公共溢出區。
例程
- 開放定址法:
查找空單元並插入:
- 分離連接法
查找效率
散列表的查找過程基本上和造表過程相同。一些關鍵碼可通過散列函數轉換的地址直接找到,另一些關鍵碼在散列函數得到的地址上產生了衝突,需要按處理衝突的方法進行查找。在介紹的三種處理衝突的方法中,產生衝突後的查找仍然是給定值與關鍵碼進行比較的過程。所以,對散列表查找效率的量度,依然用平均查找長度來衡量。
查找過程中,關鍵碼的比較次數,取決於產生衝突的多少,產生的衝突少,查找效率就高,產生的衝突多,查找效率就低。因此,影響產生衝突多少的因素,也就是影響查找效率的因素。影響產生衝突多少有以下三個因素:
- 散列函數是否均勻;
- 處理衝突的方法;
- 散列表的載荷因子(英語:load factor)。
載荷因子
散列表的載荷因子定義為: = 填入表中的元素個數 / 散列表的長度
是散列表裝滿程度的標誌因子。由於表長是定值, 與「填入表中的元素個數」成正比,所以, 越大,表明填入表中的元素越多,產生衝突的可能性就越大;反之, 越小,標明填入表中的元素越少,產生衝突的可能性就越小。實際上,散列表的平均查找長度是載荷因子 的函數,只是不同處理衝突的方法有不同的函數。
對於開放定址法,荷載因子是特別重要因素,應嚴格限制在0.7-0.8以下。超過0.8,查表時的CPU緩存不命中(cache missing)按照指數曲線上升。因此,一些採用開放定址法的hash庫,如Java的系統庫限制了荷載因子為0.75,超過此值將resize散列表。
舉例:Linux內核的bcache
Linux操作系統在物理文件系統與塊設備驅動程序之間引入了「緩衝區緩存」(Buffer
Cache,簡稱bcache)。當讀寫磁盤文件的數據,實際上都是對bcache操作,這大大提高了讀寫數據的速度。如果要讀寫的磁盤數據不在bcache中,即緩存不命中(miss),則把相應數據從磁盤加載到bcache中。一個緩存數據大小是與文件系統上一個邏輯塊的大小相對應的(例如1KiB字節),在bcache中每個緩存數據塊用struct buffer_head
記載其元信息:
整個bcache以struct buffer_head
為基本數據單元,組織為一個封閉定址(close addressing,即「單獨鍊表法」解決衝突)的散列表struct buffer_head * hash_table[NR_HASH];
散列函數的輸入關鍵字是b_blocknr(邏輯塊號)與b_dev(設備號)。計算hash值的散列函數表達式為:
- (b_dev ^ b_blocknr) % NR_HASH
其中NR_HASH是散列表的條目總數。發生「 衝突」的struct buffer_head
,以b_prev與b_next指針組成一個雙向(不循環)鍊表。bcache中所有的struct buffer_head
,包括使用中不空閒與未使用空閒的struct buffer_head
,以b_prev_free和b_next_free指針組成一個雙向循環鍊表free_list,其中未使用空閒的struct buffer_head
放在該鍊表的前部。
參考文獻
- ^ Data Structures and Algorithm Analysis in C (2nd edition).. [2017-02-16]. (原始內容存檔於2020-11-09).