數據結構與算法-----9.散列表:

1.概念:散列表,又稱爲哈希表。

2.散列思想:散列表使用的是數組支持利用下標隨機訪問數據的特性,散列表是數組的一種擴展,由數組演化而來,可以說:沒有   數組就沒有散列表。

3.爲什麼需要使用散列表這種數據結構:

因爲當我們存儲結構簡單的數據時,可以存入數組中,然後通過下標索引來快速查找。但是如果存儲的是例如:zhansan----10,lisi----20,這種一一映射的對應關係時,就不能直接存儲在數組中,但是可以對數組稍加修改,通過某種方法,將對應關係中的一類數據轉化成數組的下標,然後將另一類數據當成元素來存儲。所以就變成這種key---value的結構。下標就是key,元素就是value。所以這種將key轉化爲數組下標的方法就叫做Hash函數(或散列函數)。

哈希表的查找效率之所以高效,就是因爲它利用了數據根據下標隨機訪問元素的時間複雜度爲O(1)這種特性。

4.散列函數:

散列函數在散列表中起着非常重要的作用,散列函數設計的好,那麼可以降低哈希衝突的概率,也就提高了hash表的性能。

散列函數設計的基本要求:

(1)因爲數據下標是從0開始的,所以通過散列函數計算出的值,一定是一個非負數。

(2)如果key1 == key2,那麼hash(key1) == hash(key2)。

(3)如果key1 != key2,那麼hash(1) != hash(key2)。(理想狀態下)

但是第三點是不可能實現的,如果實現了,那麼就不可能出現Hash衝突了。之所以出現了hash衝突,就是因爲key值不同,但是計算出來的hash值相同了。

5.散列衝突問題及解決:

散列衝突問題無法避免,通常解決該問題的方法有兩種:(1)開放尋址法,(2)鏈表法。

開放尋址法核心思想:如果發生了散列衝突,就重新探測一個空閒位置,將元素插入。

1)線性探測法:(插入元素操作)跟據hash函數計算出當前元素在散列表中的對應位置後,如果當前位置爲空,那麼存儲元素。如果不爲空,那麼認爲發生hash衝突,需要從當前位置順序遍歷散列表,直到找到空閒位置爲止。

(查找元素操作)跟插入操作類似,根據hash函數定位數據下標,判斷下標對應的key是否與要查找的key相等,相等返回,如果不相等,那麼從當前位置順序向後查找,如果遇到空閒位置還沒有找到,那麼就認爲當前哈希表中沒有當前元素。

所以會有一個問題發生,當我們從哈希表中刪除一個元素之後,這時候再次查詢,有可能出現需要查找的元素剛好在被刪除元素的後面,那麼就會出現找不到的情況。所以解決該問題的方法是:在哈希表中刪除一個元素之後,需要做一個標記,比如標記爲delete,那麼當碰到這個標記時,可以繼續查找。

如果採用線性探測法解決哈希衝突問題,當哈希表中的數據越來越多時,空閒的位置越來越少,那麼發生衝突的概率就會越來越大,最壞的情況下從O(1)退變成爲O(n)。

除了線性探測法之外,還有另外兩種探測方法:二次探測法,雙重散列法。

二次探測法:線性探測爲每次移動一位,0,1,2,3.......,二次探測是每次移動的步數爲原來的二次方,0,1^2,2^2,3^2......

雙重散列法:如果第一次計算出的散列值發生衝突,那麼就用第二個散列函數計算,直到找到空閒位置。

6.鏈表法解決hash衝突:

鏈表法是更加常用的解決hash衝突的方法,在散列表中的每個槽後面,都有一個鏈表,對於散列值相同的元素,都會以鏈表的形式存放。使用鏈表法解決hash衝突,插入操作,時間複雜度爲O(1),查找和刪除操作的時間複雜度與鏈表的長度有關。

如何選擇解決哈希衝突的方法:

開放尋址法:(優點)它是將散列表中所有的數據都存放在數組中,可以充分利用CPU的緩存來提高查詢效率。(缺點)因爲數據全部放在數組中,所以衝突的代價更高。所以當數據量小,擴容因子小的時候,適合使用該方法。Java中的ThreadLocalMap中就是使用該方法。

鏈表法:(缺點)由於鏈表需要額外存儲指針,所以需要佔用額外的空間。鏈表在內存中的地址是不連續的,所以不能充分利用CPU緩存的特性來加速查詢。(優點)如果存儲的是大對象,那麼指針對應的內存佔用可以忽略不記,鏈表法對於內存的使用率比較高,而且當生成的鏈表比較長的時候,可以將鏈表變體稱爲紅黑樹等數據結構,加快查詢的效率

所以基於鏈表解決衝突的散列表適合解決承裝大對象,數據量大的情景。

7.儘量防止發生哈希衝突的方法:

(1)無論採取什麼方法,當哈希表中的空閒位置太少時,都會增加哈希衝突的概率,所以爲了儘量防止哈希衝突,那麼需要對哈希表進行擴容,增加空閒位置的數量,什麼時候擴容需要一個界限,這個界限就是擴容因子。

擴容因子 = 添入散列表中元素的數量 / 散列表的長度

擴容因子越大,空閒位置越少,衝突越多,性能越低。

(2)將散列函數儘可能設計的合理,因爲如果散列函數設計的不合理,那麼會增加哈希衝突的概率,從而降低了散列表的查詢效率。

極端情況下,如果對數據進行精心的設計,每個數據經過散列函數得到的散列值都一樣,那麼它們就會每次落入同一個槽中,如果我們採用鏈表的形式解決哈希衝突,那麼散列表就會退化稱爲鏈表,查詢效率從O(1)變成O(n)。如果散列表中有10萬條數據,退化後的查詢效率就降低了10萬倍。如果之前查詢一條數據耗時0.1s,那麼現在就是10000s,可能導致系統無法響應,從而達到DOS(拒絕服務攻擊)的目的。

8.如何合理的設計散列函數和對散列表進行動態的擴容:

設計散列函數的基本要求:(1)不能太複雜,因爲太複雜需要計算的時間更長。(2)通過散列函數計算出來的散列值要儘可能的隨機,並且均勻分佈,這樣即使發生了哈希衝突,也能夠均勻的落在每一個槽中,避免單一鏈表過長。

動態擴容:當擴容因子達到一定的界限時,就會觸發動態擴容機制,默認擴大爲原來的兩倍。

動態擴容的原理是:重新申請兩倍大小的新空間,然後將散列表中的數據搬移到新的散列表中。所以在散列表中插入一個數據,最好情況下,直接插入即可,爲O(1),最壞情況下,需要啓動擴容操作,時間複雜度爲O(n),通過均攤法,平均時間複雜度還是O(1)。

如何避免擴容的低效性:如果散列表中的數據大小有1GB,當發生擴容操作時,將舊的散列表中的數據搬移到新的散列表中,需要耗費相當長的時間。避免低效擴容的做法是:擴容的時候先不全部搬移數據,每次插入一個新數據的時候,從舊的散列表中搬移一個數據到新的散列表中,當有查詢操作的時候,先從舊的散列函數中查詢,如果查詢不到,再從新的散列表中查詢。這樣就避免了一次搬移全部數據消耗大量的時間。

9.爲什麼好多數據結構都是將散列表和鏈表組合在一起使用:

例如LRU緩存功能:如果簡單的使用單向鏈表來實現,那麼它的查找的時間複雜度爲O(n),如果通過散列表+雙向鏈表的方式來實現LRU,那麼在查找的時候,通過散列表O(1),然後再通過雙向鏈表來進行插入和刪除操作,所以無論查詢,插入,刪除,都是O(1)。

發佈了42 篇原創文章 · 獲贊 10 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章