散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它經過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫作散列函數,存放記錄的數組叫作散列表。java
從這個定義就能夠得知,散列表實際上存儲數據的仍然是數組。程序員
假設咱們要存儲一組電話簿:算法
Bob 13113113111
Lany 13113113113
Mary 13113113112數組
數組查詢數據靠的是數組下標,當咱們要查詢Mary的電話號碼,那得先從下標0開始查詢,取出來發現是Bob,再取下標1的數據,取出來也不對,再取下標2的數據,取出來纔是Mary的數據,這樣遍歷了整個數組才找到Mary的電話號碼。數據結構
設計者的想法就是將Bob、Lany、Mary設計成key,可以用key來直接取出value值,而且不用遍歷整組數據。函數
實現方式也是選擇用數組來實現,利用數組高效查詢的特性,在存取數據的時候使用了一箇中轉,將key轉化成數組下標。性能
這個中轉站,也就是哈希函數,這是一個加密函數,能夠將任何數據加密成一串整型數字,雖然是數字,但通常狀況下都是以十六進制來表示的,因此咱們日常看到的都是字符串的樣子。this
哈希算法有不少種,目前應用最普遍的差很少是MD四、MD五、SHA1等加密算法。加密
將哈希映射到數組下標的實現方式也有不少,直接定址法、數字分析法、平方取中法、摺疊法等等,在不一樣的語言中,實現方式是不太同樣的。spa
最多見的實現就是將哈希函數的結果對數組長度進行取模,獲得一個0到數組長度之間的數字,將此數字做爲下標。
存電話簿:
Bob 13113113111
Lany 13113113113
Mary 13113113112
咱們先建立一個長度爲10的數組:
假設 Bob的哈希值爲51246,那麼51246%10=6,就將Bob的電話號碼放到下標爲6的空間中。
Mary的哈希值爲26154,那麼取模後獲得的值爲4,就將Mary的電話號碼放到下標爲4的空間中。
可是因爲數組的長度是有限的,系統建立數組的時候也沒法預測程序員要存儲的數據有多大,因此當添加的數據愈來愈多的時候,數組的空間就會逐漸填滿,即便沒填滿,取模後下標相同的概率也會愈來愈大。
好比Lany哈希值爲33724,取模後值也是:
這種狀況,就叫哈希衝突。
不管哈希函數設計有多麼精細,都會產生衝突現象,也就是2個key的處理結果映射在了同一位置上,避免衝突的辦法也有多種。
多哈希法,設計多種哈希函數。
開放地址法,若是哈希衝突,馬上計算出一個候補地址嘗試存儲數據,若是仍有衝突便繼續計算下一個候補地址。
拉鍊法,若是哈希衝突,則在目標地址中將數據換成鏈表存儲,不管多少數據,均可以在鏈表末尾進行鏈接。
這裏咱們以拉鍊法來理解:
查找和添加也是同樣的,將key進行哈希處理,取模,獲得的值就是數組下標。
若是我取Lany的電話號碼呢?
在上面咱們已經知道了Lany和Mary是有哈希衝突的,在內存中已經拉出了一個動態鏈表,當我要查詢Lany的電話號碼的時候,找到了arr[4]這個元素,因爲arr[4]這個元素與鏈表鏈頭相對應,因此就順着鏈表往下找。
可能你們會奇怪,由於我也奇怪,它是怎麼肯定哪個元素是Lany的電話號碼呢?
這裏能夠看一段Java中HashMap的源碼:
JDK8,HashMap.java 第285行:
Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }
數組中的每一個元素,存的並非單純的咱們想存的數據,而是存了一個Node對象,同時將hash、key、和next也一塊兒存了。
當添加的數據多了(HashMap初始長度只有16),散列表達到必定的飽和度,出現哈希衝突的概率會逐漸增高,可能會致使大量數據擠壓在同一個數組下標上,造成長鏈,鏈表的查詢無疑的低效率的,嚴重影響整個散列表的插入讀取性能。
因此散列表必然會有一個擴容的操做:新建立一個2兩倍長的空數組,再遍歷原數組,將全部的節點從新進行哈希處理,後將新節點放入新數組。