微信搜索:碼農StayUp
主頁地址:https://gozhuyinglong.github.io
源碼分享:https://github.com/gozhuyinglong/blog-demosjava
散列表(Hash Table)也叫哈希表,是根據給定關鍵字(Key)來計算出該關鍵字在表中存儲地址的數據結構。也就是說,散列表創建了關鍵字與存儲地址之間的一種直接映射關係,將關鍵字映射到表中記錄的地址,這加快了查找速度。node
使用函數表達式來表示,應爲:hash(key)=v,其中key爲關鍵字,hash()爲散列函數,v爲散列地址。git
一個比較通俗的例子,就是咱們手機中的通信錄。通信錄用於存儲聯繫人的姓名及電話號碼信息,它是一個按照姓名首字母進行順序排列的表。好比咱們找「嬴政」就能夠經過字母「Y」進行快速定位,並找到「嬴政」。github
本章節介紹了散列表的概念及相關術語,並以一個通信錄的例子來加深對散列表的瞭解。但還有一些問題等待咱們解決,那就是「散列衝突」。咱們一方面能夠經過精心設計散列函數來儘可能減小衝突的次數,另外一方面仍須要提供解決衝突的方法。後面章節會詳細介紹散列函數的設計和解決衝突的方法。算法
散列函數的設計原則應該遵循計算簡單、散列地址分佈均勻。下面咱們介紹幾種經常使用的散列函數的構造方法。數組
取關鍵字或關鍵字的某個線性函數值爲散列地址。即hash(key) = key 或 hash(key) = a * key + b,其中a、b 爲常數。這種散列函數也叫作自身函數。微信
以下圖中,咱們要獲取某個崗位級別的信息,就可使用級別減去1來做爲地址,即:hash(key) = key - 1網絡
直接定址法的優勢是簡單、均勻也不會產生衝突。但因爲這是一種拿空間換時間的方式,因此適合查找表較小且連續的狀況。好比年齡、崗位級別等。數據結構
經過分析一組數據,這些數據中可能出現的關鍵字都是事先知道的,則能夠取關鍵字的若干數位組成散列地址。取出的這部分數據要在其數位上出現的頻率小,這樣出現衝突的概率就會很小。函數
好比手機號碼,其前3位爲網絡識別號,中間4位爲地區編碼,然後4位纔是真正的用戶號碼。那麼在某個公司中,其員工的手機號碼前7位出現衝突的機率會很大,然後4位出現衝突的機率會很小,則能夠取後四位作爲散列地址。
取關鍵字平方後的中間幾位做爲散列地址。該方法是一個產生僞隨機數的方法,由馮·諾伊曼在1946年提出。
將關鍵字分割成位數相同的幾部分(最後一部分的位數能夠不一樣),而後取這幾部分的疊加和(捨去進位)做爲散列地址。這種方法比較適用於關鍵字位數不少的狀況。
好比關鍵字爲 1234567890,散列表的表長爲3位:
(1)將關鍵字分爲4組: 123|456|789|0
(2)將它們疊加求和:123 + 456 + 789 + 0 = 1368
(3)捨去進位,得:368 ,即爲最終散列地址
假定散列表的表長爲m,取一個不大於m但最接近或等於m的質數p,經過關鍵字對p取模運算來轉換爲散列地址。散列函數爲:hash(key) = key % p,其中p $\leq$ m。
不只能夠對關鍵字直接取模,也能夠在摺疊法、平方取中法等運算後取模。對p的選擇很重要,若選擇很差很容易產生衝突。
解決衝突的方法有幾種,這裏咱們將討論其中最簡單的兩種:開放定址法和鏈地址法(拉鍊法)。
將產生衝突的散列地址作爲自變量,經過某種衝突解決函數獲得一個新的空閒散列地址。即當產生衝突後,尋找下一個空閒的散列地址。根據尋找的方式不一樣又分爲幾種方法,下面來介紹。
當產生衝突時, 順序探測表中下一地址,直探測到一個空閒(自由)的地址,將記錄插入。若一直探測到表尾地址m-1,則下一探測地址爲表首地址0。當表滿的時候,則探測失敗。
好比下圖散列表的散列函數使用除留餘數法(m=10, p=10)。那麼咱們順序插入十二、1三、24時順利插入到對應地址中,再插入3四、44時會產生衝突,使用線性探測法,繼續探測下一地址,將插入到空閒地址上。
線性探測法會形成大量元素在相鄰的散列地址上「彙集」起來,使得咱們要不斷處理衝突,這大大下降查找和存入效率。
平方探測法是消除線性探測法中的彙集問題的一種衝突解決方法。設發生衝突的地址爲d,那麼使用平方探測法獲得的新的地址序列爲d $\pm$ 1^二、d $\pm$ $2^2$、d $\pm$ $3^2$...d $\pm$ $key^2$(key $\leq$ m/2)。
仍是剛纔那個例子,當插入34時,在下標爲4的位置產生衝突,咱們先計算一下在該位置探測的序列: 4 + $1^2$ = 五、4 - $1^2$ = 三、4 + $2^2$ = 八、4 - $2^2$ = 0,即:五、二、八、0,因此34應該被放到5的位置。而44應該放到8的位置。
平方探測法的缺點是不能探測到散列表上全部的地址,但至少能探測到一半地址。
僞隨機探測法是當發生地址衝突時,地址增量爲僞隨機數序列。
僞隨機數是說,若是咱們設置隨機種子相同,則不斷調用隨機函數能夠生成不會重複的數列,咱們在查找時,用一樣的隨機種子,它每次獲得的數列是相同的,相同的地址固然能夠獲得相同的散列地址。
雙散列顧名思義是使用了兩個散列函數,當執行第一個散列函數獲得的地址發生衝突時,則執行第二個散列函數來計算該關鍵字的地址增量。
一種常見的算法是:(hash1(key) + i * hash2(key)) mod m,其中i爲衝突次數,hash1()爲第一個散列函數,hash2()爲第二個散列函數,m爲散列表大小。當發生衝突後,咱們經過重複增長步長i來尋址。
仍是以上面的例子爲例。第一個散列函數爲hash1(key) = key mod 10,第二個散列函數設爲hash2(key) = key mod 3。
經過上面公式,能夠計算出關鍵字34的散列地址: (34 mod 10 + 1 * (34 mod 3)) mod 10 = (4 + 1 * 1) mod 10 = 5。而關鍵字44的散列地址爲:(44 mod 10 + 2 * (44 mod 3)) mod 10 = (4 + 2 * 2) mod 10 = 8
鏈地址法又稱拉鍊法,是利用鏈表來解決衝突的一種方法,即把具備相同散列地址的關鍵字記錄存入到同一個單鏈表中,該鏈表稱爲同義詞鏈表。每個散列地址都有一個對應的鏈表。
仍是上面的例子,使用鏈地址法的存儲模型以下圖所示。
咱們使用鏈地址法來實現上例中散列表,其散列函數使用除留餘數法。
由於鏈表不是本篇重點,因此這裏設計一個簡單的節點類。關於鏈表的相關知識請參閱:《 鏈表(單鏈表、雙鏈表、環形鏈表) 》
class Node { private final int key; // 關鍵字 private Node next; // 下一節點 public Node(int key) { this.key = key; } public int getKey() { return key; } public Node getNext() { return next; } public void setNext(Node next) { this.next = next; } }
散列表類使用數組存儲表數據,其指向鏈表的頭節點,其散列函數使用除留餘數法。
本類主要實現了散列表的添加關鍵字、刪除關鍵字和匹配關鍵字。
class HashTable { private final int size; // 散列表大小 private final Node[] table; // 散列表 private HashTable(int size) { this.size = size; this.table = new Node[size]; } /** * 散列函數 - 除留餘數法 * * @param key * @return */ private int hash(int key) { return key % size; } /** * 添加關鍵字 * * @param key */ public void add(int key) { int hashAddress = hash(key); if (table[hashAddress] == null) { table[hashAddress] = new Node(key); return; } add(table[hashAddress], key); } /** * 添加關鍵字 - 遞歸 * * @param node * @param key */ private void add(Node node, int key) { if (node.getNext() == null) { node.setNext(new Node(key)); return; } add(node.getNext(), key); } /** * 匹配關鍵字 * * @param key * @return */ public boolean contains(int key) { int hashAddress = hash(key); Node headNode = table[hashAddress]; if (headNode == null) { return false; } return contains(headNode, key); } /** * 匹配關鍵字 - 遞歸 * * @param node * @param key * @return */ private boolean contains(Node node, int key) { if (node == null) { return false; } if (node.getKey() == key) { return true; } return contains(node.getNext(), key); } /** * 移除關鍵字 * * @param key */ public void remove(int key) { int hashAddress = hash(key); Node headNode = table[hashAddress]; if (headNode == null) { return; } if (headNode.getKey() == key) { table[hashAddress] = headNode.getNext(); return; } remove(headNode, key); } /** * 移除關鍵字 - 遞歸 * * @param node * @param key */ private void remove(Node node, int key) { if (node.getNext() == null) { return; } if (node.getNext().getKey() == key) { node.setNext(node.getNext().getNext()); return; } remove(node.getNext(), key); } }
完整代碼請訪問個人Github,若對你有幫助,歡迎給個Star,謝謝!