介紹hash散列表數據結構的個人理解
文章目錄
概要
- code語言:java
- 測試環境:win、java8
- 參考書籍:《數據結構與算法分析java語言描述》 原書第三版
- 參考鏈接:暫無
hash概述
本文只用個人理解的語言來描述,因爲與其copy或者照敲書上的文字,這樣做不如貼上鍊接地址或者書名
- 散列表,散列,見文思意:把數據散開排列在一張數據表中
- 一種用於以常數、平均時間進行插入、刪除和查找的技術。
- 一些crud操作需要進行元素間耦合操作比如排序的就沒辦法支持。再簡單點說就是元素之間相互獨立,無序。
- 結構就可以想象身份證號與一個人的對應表關係。只不過身份證表在中國來說巨大。
- hash表結構數據如何存放是根據一個函數來確定的。即所有需要存放到hash表的數據都同通過這個函數計算出一個值,這個結果值就相當於這個數據住進hash表的門牌號,類比成身份證號也行。這個函數叫做hash函數
- 上述概述即表明得到一個hash結構需要幾個東西:hash函數、裝填因子、hash表。而決定一個hash結構性能好壞的比較重要的一個東西就是hash函數。原因後面重點說明
- 若兩個數據經過hash函數算出來的結果相同,這就叫哈希衝突。
- 解決hash衝突的方案最簡單的兩種:分離鏈接法、開放定址法
裝填因子
- 概念:loadFactor = hash表中元素個數 / 該表大小
- 用途:決定是否再散列的一個因素
hash函數
一個前提1:任何計算都需要消耗時間和空間,這是設計一個算法(函數)需要考量的
hash函數演繹
這裏用數學的取模11運算來作最簡單的hash函數來說明,公式:hashcode = x % 3;
- 這裏取模3,爲什麼?假如我們不知道存入hash表個數,那麼取模數應該是一個素數,這樣可以減少hash衝突,這個能明白吧。然後實際上素數應該越大越好,但是我說過的前提1是需要考慮的,所以這裏就暫時用3來說明問題。
- 當然如果知道了存入數量,那取模數量大小就可以了。
假如我們要存入1~5的數
經過函數計算結果:
- 1 % 3 = 1
- 2 % 3 = 2
- 3 % 3 = 0
- 4 % 3 = 1
- 5 % 3 = 2
那他們存入hash表如下(根據結果作爲key,數據放到value中):如下發生了hash衝突,1,4放到了key=1的位置,2,5放到了key=2的位置。
key | value |
---|---|
0 | 3 |
1 | 1,4 |
2 | 2,5 |
以下說明經查閱HashSet結構的contains方法實現的方式。
- 如果我們要查詢值3是否在這個hash中時,經過hash函數得到key=0,然後發現hash表中有key=0的key,就說明3存在。
- 那當我們要查詢值爲7是否在這個hash中時,也是經過這個hash函數先得到key=1,然後發現有key=1的key,就說明7存在。實際上7不存在,這就是hash衝突帶來的問題
ok,以上就說明了hash函數在構造hash結構和對hash結構操作時的用法
爲什麼說hash函數決定一個hash結構性能好壞
- 如上一節演繹過程,取模3的hash函數對於只存3個數,其實沒有任何問題。但是如果需要存超過3個數,那麼就發生hash衝突,當然可以解決,後面會介紹解決方式。
- 發生了hash衝突,就會導致原本常數耗時的對hash結構的crud操作會變得充滿不確定性;對hash結構大小也會影響其改變(常用解決hash衝突的其中一種方式就需要擴展空間,而此過程將會較大影響hash性能,包括時間和空間)
- 並且有如前提1所述,函數運行有耗時有耗空間,這也會影響對hash結構的操作性能
- hash函數還有一個特點就是一經確定,就不能改變,否則之前所有的數據都需要重新分配空間並且只能手動
解決hash衝突
分離鏈接法
發生hash衝突使用分離鏈接法將會增加空間使用
- 使用一個鏈表來保存value值,即一個key對應一個鏈表,鏈表中的元素都是hash的key相同的數據,使用的是雙向鏈接
- 除了使用鏈表,其他存儲數據的結構都可以替代例如一個二叉查找樹或者另一個散列表
- 使用分離鏈接法時,影響hash表性能的還有一個因素是裝填因子,決定分離鏈接鏈表擴展大小,影響hash表的crud操作
分離鏈接法缺點
- 使用鏈表,給新單元分配地址需要時間,並且根據第二種數據結構類型,分配地址的算法還是不一樣的
開放定址法
核心思想:嘗試另外一些單元,知道找出空的單元的算法。目的是儘可能讓所有的數據都一一對應放入表內
線性探測法
根據這個核心思想需要一個探測方案,也就是探測函數。這裏就是f(i)=i。表示當前位置衝突了,我就往後找空單元
案例:還是上述的hashcode = x % 3;存1,2,3,4,5
- 1,2,3分別存入key爲1,2,0位置
- 存4時由於經過hash函數key=1,且hash表中1位置已經被佔了,再根據探測函數:找下一個位置是key=2,發現又被佔了;就繼續找下一個位置key=3,發現是空單元就可以存入。
- 存5時經過hash函數key=2,且hash表中2的位置已經被佔了,再根據探測函數:找下一個位置是key=3,發現又被佔了;就繼續找下一個位置key=4,發現空單元就可以存入。
key | value |
---|---|
0 | 3 |
1 | 1 |
2 | 2 |
3 | 4 |
4 | 5 |
線性探測法特點
- 只要表足夠大,總能找到一個空單元
- 花費時間多
- 存在一種情況,即使表相對較空,佔據的單元也會形成一些區塊,這樣的結果稱爲一次聚集
- 就是說散列到區塊中的任何關鍵字都需要多次探測才能能解決衝突。
- 由於容易發生一次聚集,對與crud操作效率還是有一定影響
平方探測法
- 此方法是爲了消除線性探測中一次聚集問題的方案;f(i) = i平方;
- 平方探測對於線性探測只是下一次探測的步長呈平方在變化
- 一個定理:如果使用平方探測,並且表的大小是素數,那麼當表至少有一半是空的時候,那麼我們能保證弄能夠插入一個新元素。
- 解決了一次聚集同樣會發生二次聚集,只是量少,探測次數更少而已,但仍然需要解決
雙散列
- 雙散列也是一個探測方案。探測函數:f(i)=i*hash(x)
- 即下一步探測位置需要經過hash(x)來決定,此時hash(x)函數就非常需要慎重選
再散列
擴展散列表大小,把所有數據重新散列,什麼時候執行再散列,由裝填因子決定,假如裝填因子爲0.5,那麼散列表達到一半時就會發生再散列。
- 再散列也可以選擇策略爲由失敗裝填hash時觸發
- 開銷非常大的操作
- 先把舊的一一散列到一個更大的hash表中,然後新的替換舊的
標準庫中的散列表(常用集合)
- HashSet、HashMap、ConcurrentHashMap。若是對象作key時應提供equals和hashcode方法,最好進行重寫
- HashSet、HashMap、ConcurrentHashMap解決衝突的方法是分離鏈接法
- 不可變的類如String使用閃存散列碼的方式避免重複計算。
散列表性質
- 合理的裝填因子和合適的散列函數時,可以期望插入、刪除和查找的平均時間花銷是O(1)
- 經典散列法:完美散列、布穀鳥散列、跳房子散列
- 散列函數必須可在常數時間內計算,與表項個數無關
- 散列函數必須將各項均勻分佈在數組單元中
- 常用於以常數平均時間實現insert和查找操作
- 當關鍵字不是短的串或整數時,需要仔細選擇散列函數
- 對於分離鏈接散列法,裝填因子不大時性能影響不明顯,但還是應該接近於1.
- 對於探測散列法,除非完全不可避免,否則裝填因子不應該超過0.5
- 有序的輸入可能使二叉樹運行的很差,平衡查找樹實現的代價很高,因此,不需要有序的信息以及對輸入是否被排序存入,那麼就應該選擇散列表
散列表豐富的應用介紹
- 編譯器使用散列表跟蹤源代碼中生命的變量,這種數據結構被叫做符號表。因爲標識符不很長,故散列函數能被迅速算出,因此是這種場景的理想應用
- 緩存已經計算過的結果
- 在線拼寫檢查程序,即詞典預先存到散列表,校驗時單次可以被常數時間校驗
- 等等等。。。
結束語,hash散列表裏面的科學方案僅在本文表述的冰山一角,對其分析實際上還是挺困難的,並且還有很多未解決的問題。本文若有描述不準確的地方,首先感謝指出,並渴求指出,幫忙糾正!