圖解數據結構(04) -- 哈希表

1、什麼是哈希表

哈希表(hash table),這種數據結構提供了鍵(Key)和值 (Value)的映射關係;只要給出一個Key,就可以高效查找到它所匹配的Value,時間複雜度接近於O(1)
在這裏插入圖片描述

2、哈希函數

散列表在本質上也是一個數組,可是數組只能根據下標,像a[0]、a[1]、a[2]、a[3]、a[4]這樣來訪問,而散列表的Key則是以字符串類型爲主的,例如以學生的學號作爲Key,輸入002123,查詢到李四;或者以單詞爲Key,輸入by,查詢到數字46……所以需要一個“中轉站”,通過某種方式,把Key和數組下標進行轉換,這個中轉站就叫作哈希函數。
在這裏插入圖片描述

哈希函數的實現

以Java的常用集合HashMap爲例,來講解哈希函數在Java中的實現:
在Java及大多數面向對象的語言中,每一個對象都有屬於自己的hashcode,這個hashcode是區分不同對象的重要標識,無論對象自身的類型是什麼,它們的 hashcode都是一個整型變量。
既然都是整型變量,想要轉化成數組的下標簡單的轉化方式就是按照數組長度進行取模運算

index = HashCode (Key) % Array.length

通過哈希函數**可以把字符串或其他類型的Key,轉化成數組的下標 index;**例如給出一個長度爲8的數組,則當 key=001121時,index = HashCode (“001121”) % Array.length = 1420036703 % 8 = 7
而當key=this時,index = HashCode (“this”) % Array.length = 3559070 % 8 = 6

3、哈希表的讀寫操作

寫操作(put)

寫操作就是在散列表中插入新的鍵值對,如調用 hashMap.put(“002931”, “王五”),意思是插入一組Key爲002931、 Value爲王五的鍵值對;具體步驟如下:

  • 第1步,通過哈希函數,把Key轉化成數組下標5;
  • 第2步,如果數組下標5對應的位置沒有元素,就把這個Entry填充到數組下標5的位置;
    在這裏插入圖片描述
    由於數組的長度是有限的,當插入的Entry越來越多時,不同的Key通過哈希函數獲得的下標有可能是相同的;例如002936這個Key對應的數組下標是2; 002947這個Key對應的數組下標也是2;這種情況稱爲哈希衝突
    在這裏插入圖片描述
    哈希衝突是無法避免的,解決哈希衝突的方法主要有兩種,一種是開放尋址法,一種是鏈表法;
  • 開放尋址法
    當一個Key通過哈希函數獲得對應的數組下標已被佔用時,我們可以“另謀高就”,尋找下一個空檔位置,以上面的情況爲例,Entry6通過哈希函數得到下標 2,該下標在數組中已經有了其他元素,那麼就向後移動1位,看看數組下標3的位置是否有空:
    在這裏插入圖片描述
    很不巧,下標3也已經被佔用,那麼就再向後移動1位,看看數組下標4的位置是否有空:
    在這裏插入圖片描述
    幸運的是,數組下標4的位置還沒有被佔用,因此把Entry6存入數組下標4的位置:
    在這裏插入圖片描述
    這就是開放尋址法的基本思路!在Java中,ThreadLocal 所使用的就是開放尋址法。
  • 鏈表法
    鏈表法被應用在了Java的集合類HashMap當中,HashMap數組的每一個元素不僅是一個Entry對象,還是一個鏈表的頭節點,每 一個Entry對象通過 next 指針指向它的下一個Entry節點,當新來的 Entry映射到與之衝突的數組位置時,只需要插入到對應的鏈表中即可。
    在這裏插入圖片描述

讀操作(get)

讀操作就是通過給定的Key,在散列表中查找對應的Value;例如調用 hashMap.get(“002936”),意思是查找Key爲002936的Entry在散列表中所對應的值,步驟如下:

  • 第1步,通過哈希函數,把Key轉化成數組下標2;
  • 第2步,找到數組下標2所對應的元素,如果這個元素的Key是002936,那麼就找到了;如果這個Key不是002936也沒關係,由於數組的每個元素都與一個鏈表對應,可以順着鏈表慢慢往下找,看看能否找到與Key相匹配的節點。
    在這裏插入圖片描述
    在上圖中,首先查到的節點Entry6的Key是002947,和待查找的Key002936不符;接着定位到鏈表下一個節點Entry1,發現Entry1的Key 002936正是要尋找的,所以返回Entry1的Value即可。

擴容(resize)

什麼時候需要進行擴容呢?
當經過多次元素插入,散列表達到一定飽和度時,Key映射位置發生衝突的概率會逐漸提高;大量元素擁擠在相同的數組下標位置,形成很長的鏈表, 對後續插入操作和查詢操作的性能都有很大影響
在這裏插入圖片描述
這種情況下,散列表就需要擴展它的長度,也就是進行擴容;對於JDK中的散列表實現類HashMap來說,影響其擴容的因素有兩個:

  • Capacity,即HashMap的當前長度;
  • LoadFactor,即HashMap的負載因子,默認值爲0.75f

衡量HashMap需要進行擴容的條件:HashMap.Size >= Capacity × LoadFactor
注意:擴容不是簡單地把散列表的長度擴大,而是經歷了下面兩個步驟:

  • 1.擴容,創建一個新的Entry空數組,長度是原數組的2倍;
  • 2.重新Hash,遍歷原Entry數組,把所有的Entry重新Hash到新數組中;
  • 爲什麼要重新Hash呢?因爲長度擴大以後,Hash的規則也隨之改變
    經過擴容,原本擁擠的散列表重新變得稀疏,原有的Entry也重新得到了儘可能均勻的分配,擴容前HashMap如下:
    在這裏插入圖片描述
    擴容後的 HashMap 如下:
    在這裏插入圖片描述

關於HashMap的實現,JDK 8和以前的版本有着很大的不同。當多個Entry被Hash到同一個數組下標位置時,爲了提升插入和查找的效率,HashMap 會把Entry的鏈表轉化爲紅黑樹這種數據結構

4、總結

  • 什麼是數組
    數組是由有限個相同類型的變量所組成的有序集合,它的物理存儲方式是順序存儲,訪問方式是隨機訪問;利用下標查找數組元素的時間複雜度是O(1),中間插入、刪除數組元素的時間複雜度是O(n)
  • 什麼是鏈表
    鏈表是一種鏈式數據結構,由若干節點組成,每個節點包含指向下一節點的指針,鏈表的物理存儲方式是隨機存儲,訪問方式是順序訪問。查找鏈表節點的時間複雜度是O(n),中間插入、刪除節點的時間複雜度是O(1)
  • 什麼是棧
    棧是一種線性邏輯結構,可以用數組實現,也可以用鏈表實現。棧包含入棧和出棧操作,遵循先入後出的原則(FILO)。
  • 什麼是隊列
    隊列也是一種線性邏輯結構,可以用數組實現,也可以用鏈表實現。隊列包含 入隊和出隊操作,遵循先入先出的原則(FIFO)。
  • 什麼是哈希表
    哈希表是存儲Key-Value映射的集合;對於某一個Key,散列表可以在接近O(1)的時間內進行讀寫操作。散列表通過哈希函數實現Key和數組下標的 轉換,通過開放尋址法和鏈表法來解決哈希衝突。
    —————————————————————————————————————————————
    內容來源:《漫畫算法》
發佈了270 篇原創文章 · 獲贊 210 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章