聊聊哈希表

概述

哈希表名字源於 Hash,也可以叫作散列表。哈希表是一種可以根據鍵(Key)直接訪問數據在內存儲存位置的數據結構。它通過計算出一個鍵值的函數,將所需查詢的數據映射到表中一個位置來讓人訪問,這加快了查找速度。這個映射函數稱哈希函數也叫散列函數,存放記錄的數組稱做 散列表 或者 哈希表

本文旨在解釋哈希表的由來和基本原理,不做深入探討,正所謂萬丈高樓平地起,瞭解基礎數據結構才能走向更高深的算法世界。

常用數據結構的查找時間複雜度

  • 數組

    數組在存儲數據時是按順序存儲的,並且存儲數據的內存也是連續的,所以數組內的數據,可以通過索引值直接取出得到,按位置查找的話,查找的時間複雜度是0(1)。按條件查找的時間複雜度是O(n),有序數組可以使用二分查找法,此時查找的時間複雜度可以降到O(logn)。

  • 鏈表

    鏈表在存儲數據時不是按順序存儲的,並且存儲數據的內存也是不連續的,所以鏈表內的數據,不能通過索引值直接取出得到,按位置查找的話,查找的時間複雜度是0(n),按條件查找的時間複雜度也是O(n)。

  • 二叉樹

    如果是普通二叉樹,查找的時間複雜度 O(n)。如果是二叉查找樹,則可以在 O(logn) 的時間複雜度內完成查找動作。

  • 哈希表

    可以提供非常快速的插入-刪除-查找操作,無論多少數據,插入和刪除只需要接近常量的時間即O(1)。

哈希表的核心思想

我們先來看看數組的查找操作:數組是通過數據的索引(index)來取出數值的,例如要找出數組A中索引值爲i的元素,那麼直接通過 a[i] 就可以取出這個數據。可以看出 數組實現了“數據地址 = f (index)”的映射關係。

如果用哈希表的邏輯來理解的話,這裏的 f () 就是一個哈希函數。它完成了索引值到實際地址的映射,這就讓數組可以快速完成基於索引值的查找。然而,數組的侷限性在於,它只能基於數據的索引去查找,而不能基於數據的數值去查找。

哈希表的設計採用了 函數映射 的思想,將記錄的存儲位置與記錄的關鍵字關聯起來。這樣的設計方式,能夠根據記錄的關鍵字快速定位到想要查找的記錄,而且不需要與表中存在的記錄的關鍵字比較後再來進行查找(比如二分查找)。

哈希表的核心思想就是實現了“數據地址 = f (關鍵字)”的映射關係,這樣就可以快速完成基於數據的數值的查找了。

  • 總結一下

數組實現了“地址 = f (index)”的映射關係。字典實現“地址 = f (關鍵字)”的映射關係

哈希函數

Hash 函數設計的好壞會直接影響到對哈希表的操作效率

什麼是哈希函數

哈希函數就是將鍵轉化爲數組索引的過程,這個函數應該易於計算且能夠均與分佈所有的鍵。

假如,我們要對手機通訊錄進行存儲,並要根據姓名找出一個人的手機號碼,如下所示:

張一:155555555

張二:166666666

張三:177777777

張四:188888888

一個可行的方法是,定義包含姓名、手機號碼的結構體,再通過線性表的方式把 4 個聯繫人的信息存起來。當要判斷“張四”是否在鏈表中,或者想要查找到張四的手機號碼時,就需要從線性表的頭結點開始遍歷。依次將每個結點中的姓名字段,同“張四”進行比較。直到查找成功或者全部遍歷一次爲止。這種做法的時間複雜度爲 O(n)。

如果要降低時間複雜度,就需要藉助哈希表的思路,構建姓名到地址的映射函數“地址 = f (姓名)”。這樣我們就可以通過這個函數直接計算出”張四“的存儲位置,在 O(1) 時間複雜度內就可以完成數據的查找。

通過這個例子,不難看出 Hash 函數設計的好壞會直接影響到對哈希表的操作效率。 假如對上面的例子採用的 Hash 函數爲姓名的每個字的拼音開頭大寫字母的 ASCII 碼之和。即:

address (張一) = ASCII (Z) + ASCII (Y) = 90 + 89 = 179;

address (張二) = ASCII (Z) + ASCII (E) = 90 + 69 = 159;

address (張三) = ASCII (Z) + ASCII (S) = 90 + 83 = 173;

address (張四) = ASCII (Z) + ASCII (S) = 90 + 83 = 173;

我們發現這個哈希函數存在一個非常致命的問題,那就是 f ( 張三) 和 f (張四) 都是 173。這種現象稱作哈希衝突,是需要在設計哈希函數時進行規避的。

從本質上來看,哈希衝突只能儘可能減少,不能完全避免。這是因爲輸入數據的關鍵字是個開放集合。只要輸入的數據量夠多、分佈夠廣,就完全有可能發生衝突。因此哈希表需要設計合理的哈希函數,並且對沖突有一套處理機制。

設計哈希函數

哈希函數能使對一個數據序列的訪問更加迅速有效,通過散列函數,數據元素將被更快定位。

一些常用的設計哈希函數的方法:

  • 直接定址法

    取關鍵字或關鍵字的某個線性函數值爲散列地址。即hash(k) = k或者hash(k) = a*k + b,a、b爲常數

  • 數字分析法

    假設關鍵字集合中的每個關鍵字 key 都是由 s 位數字組成(k1,k2,…,Ks),從中提取分佈均勻的若干位組成哈希地址。上面張一、張二、張三、張四的手機號信息存儲,就是使用的這種方法。

解決哈希衝突

上面這些常用方法都有可能會出現哈希衝突。產生衝突很正常,解決它就可以了

常用的方法,有以下兩種:

第一,開放定址法

當一個關鍵字和另一個關鍵字發生衝突時,使用某種探測技術在哈希表中形成一個探測序列,然後沿着這個探測序列依次查找下去。當碰到一個空的單元時,則插入其中。

常用的探測方法是線性探測法。 比如有一組關鍵字 {55,11,1,23,68,14,37,19,86},採用的哈希函數爲 key mod 11。

插入這組關鍵字到哈希表

  • h(55)=0,0的位置沒數據,就把55插入0的位置
  • h(11)=0,0的位置已經有數據0了,往後探測發現1的位置沒數據,就把11插入1的位置
  • h(1)=1,此時1的位置已經有數據11了,往後探測,2的位置沒數據,就把1插入2的位置
  • h(23)=1,此時1的位置已經有數據11了,往後探測直到3的位置沒數據,就把23插入3的位置
  • h(68)=2,此時2的位置已經有數據1了,往後探測直到3的位置沒數據,就把68插入4的位置
  • h(14)=3,此時3的位置已經有數據23了,往後探測直到3的位置沒數據,就把14插入5的位置
  • h(37)=4,此時4的位置已經有數據68了,往後探測直到3的位置沒數據,就把37插入6的位置
  • h(19)=8,此時8的位置沒數據,直接把19插入8的位置
  • h(86)=9,此時9的位置沒數據,直接把86插入9的位置

查找數據:

比如查找68,h(68)=2,根據哈希表,2的位置的數據是1,這時候再去2的下一位取數據,發現是23,再往下一位,發現位置4的數據是68,此時就完成了查找。

第二,鏈地址法

將哈希地址相同的記錄存儲在一張線性鏈表中。

例如,有一組關鍵字 {55,11,01,23,68,14,37,19,86},採用的哈希函數爲 key mod 11。如下圖所示:

第一次發生衝突的位置是0,這時候將11插入0位置的鏈表的尾部即可,23類似。

案例

例 1,將關鍵字序列 {7, 8, 30, 11, 18, 9, 14} 存儲到哈希表中。哈希函數爲: H (key) = (key * 3) % 7,處理衝突採用線性探測法。

接下來,我們分析一下建立哈希表和查找關鍵字的細節過程。

首先,我們嘗試建立哈希表,求出這個哈希地址:

H (7) = (7 * 3) % 7 = 0

H (8) = (8 * 3) % 7 = 3

H (30) = 6

H (11) = 5

H (18) = 5

H (9) = 6

H (14) = 0

按關鍵字序列順序依次向哈希表中填入,發生衝突後按照“線性探測”探測到第一個空位置填入。

再來看一下查找的流程:

1,查找 7。輸入 7,計算得到 H (7) = 0,根據哈希表,在 0 的位置,得到結果爲 7,跟待匹配的關鍵字一樣,則完成查找。

2,查找 18。輸入 18,計算得到 H (18) = 5,根據哈希表,在 5 的位置,得到結果爲 11,跟待匹配的關鍵字不一樣(11 不等於 18)。因此,往後挪移一位,在 6 的位置,得到結果爲 30,跟待匹配的關鍵字不一樣(11 不等於 30)。因此,繼續往後挪移一位,在 7 的位置,得到結果爲 18,跟待匹配的關鍵字一樣,完成查找。

例 2,假設有一個在線系統,可以實時接收用戶提交的字符串型關鍵字,並實時返回給用戶累積至今這個關鍵字被提交的次數。

例如,用戶輸入"abc",系統返回 1。用戶再輸入"qwt",系統返回 1。用戶再輸入"iot",系統返回 1。用戶再輸入"abc",系統返回 2。用戶再輸入"abc",系統返回 3。

一種解決方法是,用一個數組保存用戶提交過的所有關鍵字。當接收到一個新的關鍵字後,插入到數組中,並且統計這個關鍵字出現的次數。

根據數組的知識可以計算出,插入到最後的動作,時間複雜度是 O(1)。但統計出現次數必須要全部數據遍歷一遍,時間複雜度是 O(n)。隨着數據越來越多,這個在線系統的處理時間將會越來越長。顯然,這不是一個好的方法。

如果採用哈希表,則可以利用哈希表新增、查找的常數級時間複雜度,在 O(1) 時間複雜度內完成響應。預先定義好哈希表後(可以採用 Map < String, Integer > d = new HashMap <> (); )對於關鍵字(用變量 key_str 保存),判斷 d 中是否存在 key_str 的記錄。

如果存在,則把它對應的value(用來記錄出現的頻次)加 1;

如果不存在,則把它添加到 d 中,對應的 value 賦值爲 1。最後,打印處 key_str 對應的 value,即累積出現的頻次。

總結

哈希表的優點:

1,在查找方面,哈希表完成了關鍵字到地址的映射,可以在常數級時間複雜度內通過關鍵字查找到數據。

2,它可以提供非常快速的插入-刪除-查找操作,無論多少數據,插入和刪除只需要接近常量的時間。

哈希表的缺點:

1,哈希表中的數據是沒有順序概念的,所以不能以一種固定的方式(比如從小到大)來遍歷其中的元素。在數據處理順序敏感的問題時,選擇哈希表並不是個好的處理方法。

2,哈希表中的 key 是不允許重複的,在重複性非常高的數據中,哈希表也不是個好的選擇。

哈希表利用了數組可以通過地址直接取值的特性以實現O(1)時間複雜度完成查找,這纔是哈希表的核心之所在。

參考

利用鏈地址法實現 hash表

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章