哈希表,又名散列表。是很是經常使用的一種數據結構,C#的Hashtable、字典,Java的HashMap,Redis的Hash,其底層實現都是散列表。而在一些互聯網公司的面試中,更是技術面試官們必問的一道題目。本文將簡單瞭解哈希表(散列表)這種數據結構。html
散列表(哈希表),其思想主要是基於數組支持按照下標隨機訪問數據時間複雜度爲O(1)的特性。但是說是數組的一種擴展。假設,咱們爲了方便記錄某高校數學專業的全部學生的信息。要求能夠按照學號(學號格式爲:入學時間+年級+專業+專業內自增序號,如2011 1101 0001)可以快速找到某個學生的信息。這個時候咱們能夠取學號的自增序號部分,即後四位做爲數組的索引下標,把學生相應的信息存儲到對應的空間內便可。java
如上圖所示,咱們把學號做爲key,經過截取學號後四位的函數後計算後獲得索引下標,將數據存儲到數組中。當咱們按照鍵值(學號)查找時,只須要再次計算出索引下標,而後取出相應數據便可。以上即是散列思想。面試
上面的例子中,截取學號後四位的函數便是一個簡單的散列函數。算法
//散列函數 僞代碼 int Hash(string key) { // 獲取後四位字符 string hashValue =int.parse(key.Substring(key.Length-4, 4)); // 將後兩位字符轉換爲整數 return hashValue; }
在這裏散列函數的做用就是講key值映射成數組的索引下標。關於散列函數的設計方法有不少,如:直接尋址法、數字分析法、隨機數法等等。但即便是再優秀的設計方法也不能避免散列衝突。在散列表中散列函數不該設計太複雜。c#
散列函數具備肯定性和不肯定性。數組
散列衝突,即key1≠key2,hash(key1)=hash(key2)的狀況。散列衝突是不可避免的,若是咱們key的長度爲100,而數組的索引數量只有50,那麼再優秀的算法也沒法避免散列衝突。關於散列衝突也有不少解決辦法,這裏簡單複習兩種:開放尋址法和鏈表法。緩存
開放尋址法的核心思想是,若是出現了散列衝突,咱們就從新探測一一個空閒位置,將其插入。好比,咱們可使用線性探測法。當咱們往散列表中插入數據時,若是某個數據通過散列函數散列以後,存儲位置已經被佔用了,咱們就從當前位置開始,依次日後查找,看是否有空閒位置,若是遍歷到尾部都沒有找到空閒的位置,那麼咱們就再從表頭開始找,直到找到爲止。數據結構
散列表中查找元素的時候,咱們經過散列函數求出要查找元素的鍵值對應的散列值,而後比較數組中下標爲散列值的元素和要查找的元素。若是相等,則說明就是咱們要找的元素;不然就順序日後依次查找。若是遍歷到數組中的空閒位置尚未找到,就說明要查找的元素並無在散列表中。函數
對於刪除操做稍微有些特別,不能單純地把要刪除的元素設置爲空。由於在查找的時候,一旦咱們經過線性探測方法,找到一個空閒位置,咱們就能夠認定散列表中不存在這個數據。可是,若是這個空閒位置是咱們後來刪除的,就會致使原來的查找算法失效。這裏咱們能夠將刪除的元素,特殊標記爲 deleted。當線性探測查找的時候,遇到標記爲 deleted 的空間,並非停下來,而是繼續往下探測。性能
線性探測法存在很大問題。當散列表中插入的數據愈來愈多時,其散列衝突的可能性就越大,極端狀況下甚至要探測整個散列表,所以最壞時間複雜度爲O(N)。在開放尋址法中,除了線性探測法,咱們還能夠二次探測和雙重散列等方式。
簡單來說就是在衝突的位置拉一條鏈表來存儲數據。
鏈表法是一種比較經常使用的散列衝突解決辦法,Redis使用的就是鏈表法來解決散列衝突。鏈表法的原理是:若是遇到衝突,他就會在原地址新建一個空間,而後以鏈表結點的形式插入到該空間。當插入的時候,咱們只須要經過散列函數計算出對應的散列槽位,將其插入到對應鏈表中便可。
咱們可使用裝載因子來衡量散列表的「健康情況」。
散列表的負載因子 = 填入表中的元素個數/散列表的長度
散列表負載因子越大,表明空閒位置越少,衝突也就越多,散列表的性能會降低。
對於散列表來講,負載因子過大或太小都很差,負載因子過大,散列表的性能會降低。而負載因子太小,則會形成內存不能合理利用,從而造成內存浪費。所以咱們爲了保證負載因子維持在一個合理的範圍內,要對散列表的大小進行收縮或擴展,即rehash。散列表的rehash過程相似於數組的收縮與擴容。
對於開放尋址法解決衝突的散列表,因爲數據都存儲在數組中,所以能夠有效地利用 CPU 緩存加快查詢速度(數組佔用一塊連續的空間)。可是刪除數據的時候比較麻煩,須要特殊標記已經刪除掉的數據。並且,在開放尋址法中,全部的數據都存儲在一個數組中,比起鏈表法來講,衝突的代價更高。因此,使用開放尋址法解決衝突的散列表,負載因子的上限不能太大。這也致使這種方法比鏈表法更浪費內存空間。
對於鏈表法解決衝突的散列表,對內存的利用率比開放尋址法要高。由於鏈表結點能夠在須要的時候再建立,並不須要像開放尋址法那樣事先申請好。鏈表法比起開放尋址法,對大裝載因子的容忍度更高。開放尋址法只能適用裝載因子小於1的狀況。接近1時,就可能會有大量的散列衝突,性能會降低不少。可是對於鏈表法來講,只要散列函數的值隨機均勻,即使裝載因子變成10,也就是鏈表的長度變長了而已,雖然查找效率有所降低,可是比起順序查找仍是快不少。可是,鏈表由於要存儲指針,因此對於比較小的對象的存儲,是比較消耗內存的,並且鏈表中的結點是零散分佈在內存中的,不是連續的,因此對CPU緩存是不友好的,這對於執行效率有必定的影響。
對於一些一線城市的互聯網公司,技術面試官比較喜歡考察一我的的基礎,像哈希這種經典而又應用普遍的數據結構更是老生常談之題目。大體提問方式無非如下幾種
感謝你們閱讀,若有問題可在文章下方留言,我會在第一時間回覆!