1. 程式人生 > >網際網路公司面試經——你不得不知道的雜湊表

網際網路公司面試經——你不得不知道的雜湊表

前言

    雜湊表,又名散列表。是非常常用的一種資料結構,C#的Hashtable、字典,Java的HashMap,Redis的Hash,其底層實現都是散列表。而在一些網際網路公司的面試中,更是技術面試官們必問的一道題目。本文將簡單瞭解雜湊表(散列表)這種資料結構。

一、散列表

1.1 散列表

    散列表(雜湊表),其思想主要是基於陣列支援按照下標隨機訪問資料時間複雜度為O(1)的特性。可是說是陣列的一種擴充套件。假設,我們為了方便記錄某高校數學專業的所有學生的資訊。要求可以按照學號(學號格式為:入學時間+年級+專業+專業內自增序號,如2011 1101 0001)能夠快速找到某個學生的資訊。這個時候我們可以取學號的自增序號部分,即後四位作為陣列的索引下標,把學生相應的資訊儲存到對應的空間內即可。

    如上圖所示,我們把學號作為key,通過擷取學號後四位的函式後計算後得到索引下標,將資料儲存到陣列中。當我們按照鍵值(學號)查詢時,只需要再次計算出索引下標,然後取出相應資料即可。以上便是雜湊思想。

1.2 雜湊函式

    上面的例子中,擷取學號後四位的函式即是一個簡單的雜湊函式。

//雜湊函式 虛擬碼 
int Hash(string key) {
  // 獲取後四位字元
  string hashValue =int.parse(key.Substring(key.Length-4, 4));
  // 將後兩位字元轉換為整數
  return hashValue;
}

在這裡雜湊函式的作用就是講key值對映成陣列的索引下標。關於雜湊函式的設計方法有很多,如:直接定址法、數字分析法、隨機數法等等。但即使是再優秀的設計方法也不能避免雜湊衝突。在散列表中雜湊函式不應設計太複雜。

1.3 雜湊衝突

    雜湊函式具有確定性和不確定性。

  • 確定性:雜湊的雜湊值不同,那麼雜湊的原始輸入也就不同。即:key1=key2,那麼hash(key1)=hash(key2)。
  • 不確定性:同一個雜湊值很有可能對應多個不同的原始輸入。即:key1≠key2,hash(key1)=hash(key2)。

雜湊衝突,即key1≠key2,hash(key1)=hash(key2)的情況。雜湊衝突是不可避免的,如果我們key的長度為100,而陣列的索引數量只有50,那麼再優秀的演算法也無法避免雜湊衝突。關於雜湊衝突也有很多解決辦法,這裡簡單複習兩種:開放定址法和連結串列法。

1.3.1 開放定址法

    開放定址法的核心思想是,如果出現了雜湊衝突,我們就重新探測一一個空閒位置,將其插入。比如,我們可以使用線性探測法。當我們往散列表中插入資料時,如果某個資料經過雜湊函式雜湊之後,儲存位置已經被佔用了,我們就從當前位置開始,依次往後查詢,看是否有空閒位置,如果遍歷到尾部都沒有找到空閒的位置,那麼我們就再從表頭開始找,直到找到為止。

    散列表中查詢元素的時候,我們通過雜湊函式求出要查詢元素的鍵值對應的雜湊值,然後比較陣列中下標為雜湊值的元素和要查詢的元素。如果相等,則說明就是我們要找的元素;否則就順序往後依次查詢。如果遍歷到陣列中的空閒位置還沒有找到,就說明要查詢的元素並沒有在散列表中。

    對於刪除操作稍微有些特別,不能單純地把要刪除的元素設定為空。因為在查詢的時候,一旦我們通過線性探測方法,找到一個空閒位置,我們就可以認定散列表中不存在這個資料。但是,如果這個空閒位置是我們後來刪除的,就會導致原來的查詢演算法失效。這裡我們可以將刪除的元素,特殊標記為 deleted。當線性探測查詢的時候,遇到標記為 deleted 的空間,並不是停下來,而是繼續往下探測。

    線性探測法存在很大問題。當散列表中插入的資料越來越多時,其雜湊衝突的可能性就越大,極端情況下甚至要探測整個散列表,因此最壞時間複雜度為O(N)。在開放定址法中,除了線性探測法,我們還可以二次探測和雙重雜湊等方式。

1.3.2 連結串列法(拉鍊法)

    簡單來講就是在衝突的位置拉一條連結串列來儲存資料。

    連結串列法是一種比較常用的雜湊衝突解決辦法,Redis使用的就是連結串列法來解決雜湊衝突。連結串列法的原理是:如果遇到衝突,他就會在原地址新建一個空間,然後以連結串列結點的形式插入到該空間。當插入的時候,我們只需要通過雜湊函式計算出對應的雜湊槽位,將其插入到對應連結串列中即可。

1.3.3 負載因子與rehash

    我們可以使用裝載因子來衡量散列表的“健康狀況”。

散列表的負載因子 = 填入表中的元素個數/散列表的長度

散列表負載因子越大,代表空閒位置越少,衝突也就越多,散列表的效能會下降。

    對於散列表來說,負載因子過大或過小都不好,負載因子過大,散列表的效能會下降。而負載因子過小,則會造成記憶體不能合理利用,從而形成記憶體浪費。因此我們為了保證負載因子維持在一個合理的範圍內,要對散列表的大小進行收縮或擴充套件,即rehash。散列表的rehash過程類似於陣列的收縮與擴容。

1.3.4 開放定址法與連結串列法比較

    對於開放定址法解決衝突的散列表,由於資料都儲存在陣列中,因此可以有效地利用 CPU 快取加快查詢速度(陣列佔用一塊連續的空間)。但是刪除資料的時候比較麻煩,需要特殊標記已經刪除掉的資料。而且,在開放定址法中,所有的資料都儲存在一個數組中,比起連結串列法來說,衝突的代價更高。所以,使用開放定址法解決衝突的散列表,負載因子的上限不能太大。這也導致這種方法比連結串列法更浪費記憶體空間。

    對於連結串列法解決衝突的散列表,對記憶體的利用率比開放定址法要高。因為連結串列結點可以在需要的時候再建立,並不需要像開放定址法那樣事先申請好。連結串列法比起開放定址法,對大裝載因子的容忍度更高。開放定址法只能適用裝載因子小於1的情況。接近1時,就可能會有大量的雜湊衝突,效能會下降很多。但是對於連結串列法來說,只要雜湊函式的值隨機均勻,即便裝載因子變成10,也就是連結串列的長度變長了而已,雖然查詢效率有所下降,但是比起順序查詢還是快很多。但是,連結串列因為要儲存指標,所以對於比較小的物件的儲存,是比較消耗記憶體的,而且連結串列中的結點是零散分佈在記憶體中的,不是連續的,所以對CPU快取是不友好的,這對於執行效率有一定的影響。

小結

    對於一些一線城市的網際網路公司,技術面試官比較喜歡考察一個人的基礎,像雜湊這種經典而又應用廣泛的資料結構更是老生常談之題目。大致提問方式無非以下幾種

  1. C#字典(java hashmap或者Redis hash)的底層實現方式
  2. 說一下什麼是雜湊表(散列表)
  3. 雜湊如何解決碰撞(雜湊如何解決衝突)

-----END-----

感謝大家閱讀,如有問題可在文章下方留言,我會在第一時間回覆