【學點數據結構和算法】04-散列表

寫在前面: 博主是一名軟件工程系大數據應用開發專業大二的學生,暱稱來源於《愛麗絲夢遊仙境》中的Alice和自己的暱稱。作爲一名互聯網小白,寫博客一方面是爲了記錄自己的學習歷程,一方面是希望能夠幫助到很多和自己一樣處於起步階段的萌新。由於水平有限,博客中難免會有一些錯誤,有紕漏之處懇請各位大佬不吝賜教!個人小站:http://alices.ibilibili.xyz/ , 博客主頁:https://alice.blog.csdn.net/
儘管當前水平可能不及各位大佬,但我還是希望自己能夠做得更好,因爲一天的生活就是一生的縮影。我希望在最美的年華,做最好的自己

        前面已經陸陸續續寫了幾篇介紹數據結構的博客,包含數組,鏈表,棧和隊列…本篇博客,我們再來學習一種有趣的數據結構——散列表

在這裏插入圖片描述


概念

        首先我們需要明白什麼是哈希表?

哈希表(Hash table,也叫散列表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。

        只要給出一個Key,就可以高效查找到它所匹配的Value,時間複雜度接近 於O(1)。

在這裏插入圖片描述
        從散列表通過Key來查找Value的方式,我們不難得出,散列表在本質上也是一個數組

        但是看到這裏,就有朋友想問了,數組不是隻能通過下標(數字索引)來進行訪問元素嗎?散列表的key值是以字符串類型爲主的,那是怎麼將它們關聯在一起的呢???
在這裏插入圖片描述

哈希函數

        其實有這樣的疑惑是很正常的。

        我們在上面概念介紹時,談到過一個概念,叫做"散列函數",其實它就是我們所熟知的哈希(Hash)函數。

        我們正是通過哈希函數,把Key和數組下標進行轉換。
在這裏插入圖片描述
        在不同的語言中,哈希函數的實現方式是不一樣的。這裏以Java的常用集合HashMap 爲例,來看一看哈希函數在Java中的實現。

        在Java及大多數面向對象的語言中,每一個對象都有屬於自己的hashcode,這個hashcode是區分不同對象的重要標識。無論對象自身的類型是什麼,它們的hashcode都是一個整型變量

        既然hashcode和數組都是整型變量,那麼它們之間轉換就很容易實現。事實上,它們之間的轉換遵循下面的公式,也就是所謂的哈希函數
在這裏插入圖片描述
        通過哈希函數,我們可以把字符串或其他類型的Key,轉化成數組的下標index。

        如給出一個長度爲8的數組,則當
在這裏插入圖片描述
而當key=this時,
        看到這裏,大家是否有一種恍然大悟的感覺呢!我們如果想要往散列表中寫入元素,實際上就是先對key值求hash,然後與數組長度求餘得到一個數值,然後把元素放入到對應數值下標的數組中即可。
在這裏插入圖片描述

哈希衝突

        但是,是否有考慮過有這麼一種情況?

        由於數組的長度是有限的,當插入的元素越來越多時,不同的Key通過哈希函數獲得的下標有可能是相同的。例如002936這個Key對應的數組下標是2;002947這個Key 對應的數組下標也是2。

        這個時候,顯然key爲002947的value無法插入到下標爲2的數組中。
在這裏插入圖片描述
        這種情況,就叫作哈希衝突

        解決哈希衝突的方法,主要有鏈表法開放尋址法

開放尋址

        開放尋址法的原理很簡單,當一個Key通過哈希函數獲得對應的數組下標已被佔用 時,我們可以“另謀高就”,尋找下一個空檔位置。
在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述
        這就是開放尋址法的基本思路。當然,在遇到哈希衝突時,尋址方式有很多種,並不一定只是簡單地尋找當前元素的後一個元素,這裏只是舉一個簡單的示例而已。

        在Java中,ThreadLocal所使用的就是開放尋址法
        

鏈表法

        接下來,重點講一下解決哈希衝突的另一種方法——鏈表法。這種方法被應用在了Java的集合類HashMap當中。

        HashMap數組的每一個元素不僅是一個Entry對象,還是一個鏈表的頭節點。每一個 Entry對象通過next指針指向它的下一個Entry節點。當新來的Entry映射到與之衝突的數組位置時,只需要插入到對應的鏈表中即可。

在這裏插入圖片描述

擴容

        在講解數組時,曾經介紹過數組的擴容。既然散列表是基於數組實現的,那麼散列表 也要涉及擴容的問題。

        首先,什麼時候需要進行擴容呢?

        當經過多次元素插入,散列表達到一定飽和度時,Key映射位置發生衝突的概率會逐 漸提高。這樣一來,大量元素擁擠在相同的數組下標位置,形成很長的鏈表,對後續插入操作和查詢操作的性能都有很大影響。
在這裏插入圖片描述
        這時,散列表就需要擴展它的長度,也就是進行擴容。

        對於JDK中的散列表實現類HashMap來說,影響其擴容的因素有兩個。

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

        衡量HashMap需要進行擴容的條件如下:
在這裏插入圖片描述
        事實上,擴容不是簡單地把散列表的長度擴大,而是經歷了下面兩個步驟:

        1、擴容,創建一個新的Entry空數組,長度是原數組的2倍。

        2.重新Hash,遍歷原Entry數組,把所有的Entry重新Hash到新數組中。爲什麼要重 新Hash呢?因爲長度擴大以後,Hash的規則也隨之改變。

        經過擴容,原本擁擠的散列表重新變得稀疏,原有的Entry也重新得到了儘可能均勻的分配。

        擴容前的HashMap
在這裏插入圖片描述
        擴容後的HashMap
在這裏插入圖片描述
        以上就是散列表各種基本操作的原理。由於HashMap的實現代碼相對比較複雜,有興趣的讀者可以在JDK中直接閱讀HashMap類的源碼。

        需要注意的是,關於HashMap的實現,JDK 8和以前的版本有着很大的不同。當多個 Entry被Hash到同一個數組下標位置時,爲了提升插入和查找的效率,HashMap會把Entry 的鏈表轉化爲紅黑樹這種數據結構。建議讀者把兩個版本的實現都認真地看一看,這會讓你受益匪淺。


        本篇博客中說明和彩圖大部分來源於《漫畫算法》,應本書作者要求,加上本書公衆號《程序員小灰》二維碼。
在這裏插入圖片描述
        感興趣的朋友可以去購買正版實體書,確實不錯,非常適合小白入門。
在這裏插入圖片描述

總結

散列表也叫哈希表,是存儲Key-Value映射的集合。對於某一個Key,散列表可以在接近O(1)的時間內進行讀寫操作。散列表通過哈希函數實現Key和數組下標的轉換,通過開放尋址法鏈表法來解決哈希衝突。

        如果本文對您有所幫助,不妨點個贊支持一下博主🙏

        希望我們都能在學習的道路上越走越遠😉
在這裏插入圖片描述

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