日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

19 | 散列表(中):如何打造一个工业级水平的散列表?

發(fā)布時間:2023/12/10 编程问答 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 19 | 散列表(中):如何打造一个工业级水平的散列表? 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

問題引入:如何實(shí)現(xiàn)一個工業(yè)級的散列表?

主要要求:

  • 設(shè)計(jì)一個合適的散列函數(shù);
  • 定義裝載因子閾值,并且設(shè)計(jì)動態(tài)擴(kuò)容策略;
  • 選擇合適的散列沖突解決方法。
  • 對于動態(tài)散列表來說,不管我們?nèi)绾卧O(shè)計(jì)散列函數(shù),選擇什么樣的散列沖突解決方法。隨著數(shù)據(jù)的不斷增加,散列表總會出現(xiàn)裝載因子過高的情況。這個時候,我們就需要啟動動態(tài)擴(kuò)容。

    散列表碰撞攻擊原理

    如果精心構(gòu)造數(shù)據(jù),使得所有的數(shù)據(jù)經(jīng)過散列函數(shù)之后,都散列到同一個槽里。當(dāng)使用的是基于鏈表的沖突解決方法,散列表就會退化為鏈表,查詢的時間復(fù)雜度就從 O(1) 急劇退化為 O(n)。如果散列表中有 10 萬個數(shù)據(jù),退化后的散列表查詢的效率就下降了 10 萬倍。如果之前運(yùn)行 100 次查詢只需要 0.1 秒,那現(xiàn)在就需要 1 萬秒。因?yàn)椴樵儾僮飨拇罅?/span> CPU 或者線程資源,導(dǎo)致系統(tǒng)無法響應(yīng)其他請求,從而達(dá)到拒絕服務(wù)攻擊(DoS)的目的這

    如何設(shè)計(jì)一個可以應(yīng)對各種異常情況的工業(yè)級散列表,來避免在散列沖突的情況下,散列表性能的急劇下降,并且能抵抗散列碰撞攻擊?

    好的散列函數(shù):

    • 首先,不能太復(fù)雜。過于復(fù)雜的散列函數(shù)會消耗很多計(jì)算時間,影響到散列表性能。
    • 其次,散列函數(shù)生成的值要盡可能隨機(jī)并且均勻分布,避免或者最小化散列沖突,并且散列到每個槽里的數(shù)據(jù)也會比較平均,不會出現(xiàn)某個槽內(nèi)數(shù)據(jù)特別多的情況。

    裝載因子過大了怎么辦?

    • 對于動態(tài)散列表來說,數(shù)據(jù)集合是頻繁變動的,無法預(yù)估將要加入的數(shù)據(jù)個數(shù),無法事先申請一個足夠大的散列表。隨著數(shù)據(jù)加入,裝載因子慢慢變大。當(dāng)裝載因子大到一定程度之后,散列沖突就會變得不可接受。這個時候如何處理呢?—— “動態(tài)擴(kuò)容”嗎?
    • 如何做數(shù)組、棧、隊(duì)列的動態(tài)擴(kuò)容的。針對散列表,當(dāng)裝載因子過大時,也可以進(jìn)行動態(tài)擴(kuò)容,重新申請一個更大的散列表,將數(shù)據(jù)搬移到這個新散列表中。假設(shè)每次擴(kuò)容我們都申請一個原來散列表大小兩倍的空間。那經(jīng)過擴(kuò)容之后,新散列表的裝載因子就下降為原來的一半。
    • 針對數(shù)組的擴(kuò)容,數(shù)據(jù)搬移操作比較簡單。但是,針對散列表的擴(kuò)容,數(shù)據(jù)搬移操作要復(fù)雜很多。因?yàn)樯⒘斜淼拇笮∽兞?#xff0c;數(shù)據(jù)的存儲位置也變了,所以我們需要通過散列函數(shù)重新計(jì)算每個數(shù)據(jù)的存儲位置。

    對于支持動態(tài)擴(kuò)容的散列表,插入操作的時間復(fù)雜度是多少呢?插入一個數(shù)據(jù),最好情況下,不需要擴(kuò)容,最好時間復(fù)雜度是 O(1)。最壞情況下,散列表裝載因子過高,啟動擴(kuò)容,重新申請內(nèi)存空間,重新計(jì)算哈希位置,并且搬移數(shù)據(jù),所以時間復(fù)雜度是 O(n)。用攤還分析法,均攤情況下,時間復(fù)雜度接近最好情況,就是 O(1)。實(shí)際上,對于動態(tài)散列表,隨著數(shù)據(jù)的刪除,散列表中的數(shù)據(jù)會越來越少,空閑空間會越來越多。

    • 當(dāng)散列表裝載因子超過某個閾值時需要進(jìn)行擴(kuò)容。裝載因子閾值需要選擇得當(dāng)。如果太大,會導(dǎo)致沖突過多;如果太小,會導(dǎo)致內(nèi)存浪費(fèi)嚴(yán)重。裝載因子閾值的設(shè)置要權(quán)衡時間、空間復(fù)雜度。如果內(nèi)存空間不緊張,對執(zhí)行效率要求很高,可以降低負(fù)載因子的閾值;相反,如果內(nèi)存空間緊張,對執(zhí)行效率要求又不高,可以增加負(fù)載因子的值,甚至可以大于 1

    如何避免低效的擴(kuò)容?

    大部分情況下,動態(tài)擴(kuò)容的散列表插入一個數(shù)據(jù)都很快,但是在特殊情況下,當(dāng)裝載因子已經(jīng)到達(dá)閾值,需要先進(jìn)行擴(kuò)容,再插入數(shù)據(jù)。這個時候,插入數(shù)據(jù)就會變得很慢,甚至?xí)o法接受。如果散列表當(dāng)前大小為 1GB,要想擴(kuò)容為原來的兩倍大小,那就需要對 1GB 的數(shù)據(jù)重新計(jì)算哈希值,并且從原來的散列表搬移到新的散列表很耗時

    如果我們的業(yè)務(wù)代碼直接服務(wù)于用戶,盡管大部分情況下,插入一個數(shù)據(jù)的操作都很快,但是,極個別非常慢的插入操作,也會讓用戶崩潰。這個時候,“一次性”擴(kuò)容的機(jī)制就不合適了。為了解決一次性擴(kuò)容耗時過多的情況,將擴(kuò)容操作穿插在插入操作的過程中,分批完成。當(dāng)裝載因子觸達(dá)閾值之后,只申請新空間,但并不將老的數(shù)據(jù)搬移到新散列表中當(dāng)有新數(shù)據(jù)要插入時,將新數(shù)據(jù)插入新散列表中,并且從老的散列表中拿出一個數(shù)據(jù)放入到新散列表。每次插入一個數(shù)據(jù)到散列表,都重復(fù)上面的過程。經(jīng)過多次插入操作之后,老散列表中數(shù)據(jù)就一點(diǎn)一點(diǎn)全部搬移到新散列表中。這樣沒有了集中的一次性數(shù)據(jù)搬移,插入操作就都變得很快了

    如何選擇沖突解決方法?

    開放尋址法和鏈表法在實(shí)際的軟件開發(fā)中都非常常用。Java LinkedHashMap 就采用了鏈表法解決沖突,ThreadLocalMap 是通過線性探測的開放尋址法來解決沖突。

    這兩種沖突解決方法各有什么優(yōu)勢和劣勢,又各自適用哪些場景?

    1. 開放尋址法

    優(yōu)點(diǎn):

    • 開放尋址法不需要拉很多鏈表。散列表中的數(shù)據(jù)都存儲在數(shù)組中,可以有效地利用 CPU 緩存加快查詢速度。
    • 序列化起來比較簡單。鏈表法包含指針,序列化起來就沒那么容易。序列化很多場合都會用到。

    缺點(diǎn):

    • 用開放尋址法解決沖突的散列表,刪除數(shù)據(jù)的時候比較麻煩,需要特殊標(biāo)記已經(jīng)刪除掉的數(shù)據(jù)。在開放尋址法中,所有的數(shù)據(jù)都存儲在一個數(shù)組中,比起鏈表法來說沖突的代價(jià)更高。
    • 使用開放尋址法解決沖突的散列表,裝載因子的上限不能太大。比鏈表法更浪費(fèi)內(nèi)存空間。

    總結(jié)一下“”當(dāng)數(shù)據(jù)量比較小、裝載因子小的時候,適合采用開放尋址法。這也是 Java 中的ThreadLocalMap使用開放尋址法解決散列沖突的原因。

    2. 鏈表法:

    優(yōu)點(diǎn):

    • 鏈表法對內(nèi)存的利用率比開放尋址法要高。鏈表結(jié)點(diǎn)可以在需要的時候再創(chuàng)建,并不需要事先申請。這也是鏈表優(yōu)于數(shù)組的地方。
    • 鏈表法比起開放尋址法,對大裝載因子的容忍度更高。開放尋址法只能適用裝載因子小于 1 的情況。接近 1 時,就可能會有大量的散列沖突,導(dǎo)致大量的探測、再散列等,性能會下降很多。鏈表法只要散列函數(shù)的值隨機(jī)均勻,即便裝載因子變成 10,也就是鏈表的長度變長,雖然查找效率有所下降,但是比起順序查找還是快很多。

    缺點(diǎn):

    • 鏈表因?yàn)橐鎯χ羔?#xff0c;所以對于比較小的對象的存儲,是比較消耗內(nèi)存的,還有可能會讓內(nèi)存的消耗翻倍。
    • 鏈表中的結(jié)點(diǎn)是零散分布在內(nèi)存中,不是連續(xù)的,對 CPU 緩存是不友好的,這方面對于執(zhí)行效率也有一定的影響。

    當(dāng)然,如果我們存儲的是大對象,存儲的對象的大小遠(yuǎn)遠(yuǎn)大于一個指針的大小(4個字節(jié)或者 8 個字節(jié)),那鏈表中指針的內(nèi)存消耗在大對象面前就可以忽略了。實(shí)際上對鏈表法稍加改造,可以實(shí)現(xiàn)一個更加高效的散列表。將鏈表法中的鏈表改造為其他高效的動態(tài)數(shù)據(jù)結(jié)構(gòu),比如跳表、紅黑樹。這樣,即便出現(xiàn)散列沖突,極端情況下,所有的數(shù)據(jù)都散列到同一個桶內(nèi),那最終退化成的散列表的查找時間也只不過是 O(logn)。這樣也就有效避免了前面講到的散列碰撞攻擊。

    工業(yè)級散列表舉例分析-hashmap

    Java 中的 HashMa是怎么應(yīng)用:

    1. 初始大小HashMap 默認(rèn)的初始大小是 16,默認(rèn)值是可以設(shè)置的,如果事先知道大概的數(shù)據(jù)量有多大,可以通過修改默認(rèn)初始大小,減少動態(tài)擴(kuò)容的次數(shù),這樣會大大提高 HashMap 的性能。

    2. 裝載因子和動態(tài)擴(kuò)容最大裝載因子默認(rèn)是 0.75,當(dāng) HashMap 中元素個數(shù)超過 0.75*capacitycapacity 表示散列表的容量)的時候,就會啟動擴(kuò)容,每次擴(kuò)容都會擴(kuò)容為原來的兩倍大小。

    3. 散列沖突解決方法HashMap 底層采用鏈表法來解決沖突。即使負(fù)載因子和散列函數(shù)設(shè)計(jì)得再合理,也免不了會出現(xiàn)拉鏈過長的情況,一旦出現(xiàn)拉鏈過長,則會嚴(yán)重影響 HashMap 的性能。在 JDK1.8 版本中,對 HashMap 做進(jìn)一步優(yōu)化引入了紅黑樹。而當(dāng)鏈表長度太長(默認(rèn)超過 8)時,鏈表就轉(zhuǎn)換為紅黑樹。可以利用紅黑樹快速增刪改查的特點(diǎn),提高 HashMap 的性能。當(dāng)紅黑樹結(jié)點(diǎn)個數(shù)少于 8 個的時候,又會將紅黑樹轉(zhuǎn)化為鏈表。因?yàn)樵跀?shù)據(jù)量較小的情況下,紅黑樹要維護(hù)平衡,比起鏈表來,性能上的優(yōu)勢并不明顯。

    4. 散列函數(shù)散列函數(shù)的設(shè)計(jì)并不復(fù)雜,追求的是簡單高效、分布均勻。

    int hash(Object key)

    {

    ?int h = key.hashCode()

    return (h ^ (h >>> 16)) & (capicity -1); //capicity表示散列表的大小

    }

    其中,hashCode() 返回的是 Java 對象的 hash code

    創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎

    總結(jié)

    以上是生活随笔為你收集整理的19 | 散列表(中):如何打造一个工业级水平的散列表?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。