在一陣列中,我們放入成對key
和value
的數值,key
為球員背號,value
為球員名稱,假設要查找四號球員,用下列兩種方式查找會遇到一些問題
array
位置,直接使用它們的鍵值作為索引,稱作Direct Address Table
,使用這種方式查找速度非常快,但如果數據總數與最大值之間有很大的差距,會導致記憶體空間浪費為解決以上問題我們可以使用雜湊表
雜湊表是資料結構的一種,主要用來進行有效率的數據搜尋。
雜湊表它將鍵映射到值。它使用雜湊函數Hash Function
將鍵映射到數組中的桶(Bucket)
,從而使查找,插入和刪除操作的時間複雜度達到,如下圖所示。它們被廣泛用於多種計算機軟件,特別是關聯數組(associative arrays)
、數據庫索引(database indexing)
、緩存(caches)
和集合(sets)
。
在實現雜湊表時,需要考慮具體的應用場景,選擇合適的雜湊函數和衝突解決方法,以達到最好的性能和空間效率。
Hash Table
儲存資料的位置,每一個位置對應一個Bucket Address
。雜湊的過程如下
(圖片來自於Division Modulo Method - Hashing Technique)
Division Method
相對其他雜湊函數簡單易用,計算速度較快
(圖片來自於Division Modulo Method - Hashing Technique)
Division
的方法就是將Key
值去除以Hash Table
長度得到的餘數,就是Hash Table
的Key
值。
例如key
值等於99
除以Hash Table
長度為10
,餘數就等於9
除留餘數法相對簡單,但是容易發生雜湊衝突。為了減少雜湊衝突的概率,可以使用其他雜湊函數,例如接下接下來要說的乘法雜湊法(Multiplication Method)
。同時,為了解決雜湊衝突,還可以採用開放定址法、鏈結法等衝突解決方法。
2
的平方,因為會被整除,較容易造成很多collision
key
值不是數字是英文名稱,我們須先把名稱轉成一些數字,若英文名稱相近,會造成很多collision
相對其他雜湊函數易於實現,並改善了雜湊衝突
(圖片來自於Lecture 11 oct 7 Goals)
基本原理是通過將關鍵字與某個常數相乘再取小數部分來得到雜湊地址,具體來說,對於一個關鍵字k
,假設雜湊表的大小為m
,常數A
為一個介於0
和1
之間的常數,則使用Multiplication Method
計算雜湊地址h(k)
的公式為:
其中,floor
表示向下取整函數, 表示 k
與 A
的乘積除以 1
的餘數(即小數部分),m
為雜湊表的大小。由於 的結果始終在之間,因此乘以 m
後可以得到一個介於 0
和 m-1
之間的整數,作為雜湊地址。
Multiplication Method
的優點是相對簡單且雜湊地址的分佈比較均勻,可以有效地降低雜湊衝突的概率。不過,選擇合適的常數A
對於雜湊表的性能影響很大,需要根據實際情況進行調整。此外,如果雜湊表的大小不是 2
的次方,可能會影響雜湊地址的均勻性。
在實際應用中,Multiplication Method
常用於解決靜態雜湊表的構建問題,即在雜湊表初始化之後,不再需要頻繁地進行插入和刪除操作的情況。而對於動態雜湊表,由於元素數量的不斷變化,需要採用更加複雜的雜湊函數來解決雜湊衝突的問題
A
:在使用Multiplication Method
計算雜湊地址時,需要先確定乘數常數A,通常選擇一個介於0和1之間的無理數,盡量接近於的值,通常常用的乘數常數A
值为0.6180339887
。Action動作 | 平均 | 最壞 |
---|---|---|
訪問(Access) |
N/A | N/A |
搜尋(Search) |
||
插入(Insertion) |
||
刪除(Deletion) |
公式:
使用除法計算雜湊地址,並通過insert()
方法將 key value
在雜湊表中,使用search()
方法根據 key
查找對應的值。
由於Division Method
的簡單性,即使雜湊表的大小不是2
的次方,也能夠快速計算雜湊地址。然而,如果雜湊表的大小不是質數,可能會導致雜湊衝突增多,需要採取一些方法來解決這個問題。
公式:
使用Multiplication Method
計算雜湊地址,並通過insert()
方法將key value
在雜湊表中,使用search()
方法根據 key
查找對應的值。
由於雜湊地址的均勻性,即使鍵的值不連續,也能夠快速查找到對應的值。
轉換的所產生的數字要夠隨機,不然容易產生衝突
注意,這個實作僅供參考,實際應用中需要考慮更多的因素,
字串轉換有許多方式,範例使用轉換 Unicode
方式
divideHash
方法接受一個字串,使用 division Method
將其轉換為雜湊值multiplicationHash
方法接受一個字串,使用 multiplication method
將其轉換為雜湊值。常數 A
選用黃金比例為 0.6180339887
,,可以較好地分散雜湊值。set
方法接受一個鍵和值,將其存儲在雜湊表中。get
方法接受一個鍵,返回與該鍵關聯的值,如果該鍵不存在,則返回 null
。printAll
方法輸出 Hash table
值
(圖片來自Hash Tables, Hashing and Collision Handling
使用一個Linked List
,來存儲相應數據,當hash
遇到衝突的時候依次添加到Linked List
的後面進行處理
注意,這個實作僅供參考,實際應用中需要考慮更多的因素
我們同樣使用了Division Method
計算雜湊地址,我們鏈結法解決雜湊衝突。在insert()
方法中,我們判斷對應位置是否已經存在一個Linked List
。如果不存在,直接將新節點存儲在該位置上。如果存在一個Linked List
,就遍歷Linked List
,如果找到對應的鍵值,就更新對應的值。否則,在Linked List
的末尾插入一個新節點。在search()方法中,我們同樣需要遍歷對應位置的Linked List
,找到對應的鍵值對。如果Linked List
為空,說明對應的鍵值對不存在。如果Linked List
不為空,則遍歷Linked List
中的每個鍵值對,如果找到對應的鍵值,就返回對應的值,否則返回null。
(圖片來自於Open Addressing)
開放定址法比其他解決雜湊表衝突的方法更節省空間,例如上面提到的鏈結法,其中表中的每個索引都包含key value
對的Linked List
。使用開放定址,無需為Linked List
分配內存,這在內存受限的環境中非常有利。
在開放定址中,當發生衝突時,算法會在表中探測可用的槽(Slot)
,找出第二個候補位置,如果滿了再往下尋找,直到找到空的位址,有幾種方法可以探測下一個位址,
例如上述圖片的線性探測法(Linear Probing)
算法通過按順序檢查索引來搜索下一個可用槽(Slot)
。另一種方法是二次探測(quadratic probing)
,此算法檢查由探測數量的二次函數偏移的索引。第三種方法是雙重散列(double hashing)
,其中算法使用第二個散列函數來確定要探測的下一個索引。
但是,如果雜湊表變得太滿,開放定址也會導致性能下降。隨著表中佔用槽的數量增加,衝突次數也增加,算法必須探測更多索引以找到可用槽。這會導致更長的查找時間和性能下降。
為了解決這個問題,普遍做法是在負載因數 (Load factor)(表中佔用的槽數與槽總數的比率),超過某個閾值時調整雜湊表的大小。調整大小以及創建一個更大的新表,將舊表中的所有key value對重新散列到新表中,然後使用新表進行後續操作。
注意,這個實作僅供參考,實際應用中需要考慮更多的因素
_hash()
函數使用了一種簡單的雜湊函數,稱為「多項式捲動雜湊(Polynomial rolling hash)」,它將鍵中的每個字元轉換為一個整數,並將它們加總起來。線性探測法使用 while 迴圈在哈希表中尋找可用槽位。如果目標槽位已經被佔據,則在哈希表中移動到下一個槽位,直到找到一個可用槽位或搜尋完整個哈希表。這個實現還提供了 keys()
和 values()
方法來分別返回哈希表中所有鍵和所有值的陣列。