一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

「哈希表」是什么? 有哪些常用的解決沖突的方法?

 半佛肉夾饃 2023-10-20 發(fā)布于河南

很多人談起“哈希表”,就會直接聚焦到hash函數(shù)、“散列”/“雜湊”之類不明所以的大詞上,搞的初學者一頭霧水,完全不明白這東西是干嘛用的,更不知道什么時候該用它、怎么用它、出現(xiàn)問題如何解決。

那么,這里我就從問題開始,一步步把哈希表的來龍去脈剖析清楚。

想象一下:有一天,你開車到某商場去買東西。買完東西,你想不起自己的車在哪了。

這家商場非常非常大,樓下停車場少說停了上萬臺車;而你的健忘癥又比較厲害……

那么,問題來了:你怎樣才能找到自己的車?

一個理想的情形是,你從頭到尾一行一行一輛一輛按順序走一遍就找到了;運氣最差時,你搜遍樓下N輛車,發(fā)現(xiàn)你的車在末尾——拿術(shù)語說,這個復(fù)雜度是O(N)。

能不能更快一些呢?

很簡單,假如所有車按車牌號順序排列,你直接往停車場中間走就行了;如果你的車牌號大于中間那輛車,那么你就往停車場后半部分的中間走,否則就往前半場走……依此類推。

如此一來,你最多只需要走ln(N)次“中間”,就能找到你的車了。黑話叫復(fù)雜度O(ln N)。

還能不能更給力一些?

可以。

假設(shè)這個停車場非常非常大,大到可以給每個車牌號分配一個固定的停車位;那么只要你把自己的車牌號報給看門老頭,他拎著你的衣領(lǐng)子往后一丟,你就“吧唧”一下掉自己車頂上了——嗯,你看,一車一位,就是這么任性。

這就叫“查找復(fù)雜度O(1)”。

如果用程序?qū)崿F(xiàn)的話,就是這么一個數(shù)組:

car park[MAX_CAR_NUM]

第一個場景,你要直接以一個循環(huán)遍歷park中的每個元素。

第二個場景,你只需先訪問MAX_CAR_NUM除以2的那個位置,再根據(jù)車牌號大小訪問數(shù)組前半拉或者后半拉中間的元素即可。

第三個場景,你的車牌號就是數(shù)組下標,所以你只需直接訪問park[CAR_NUMBER]即可。

那么,第三個設(shè)計是不是完全解決問題了呢?

并不是。

很容易看出,第三個方案需要一個超級大的存儲空間。

這個空間得有多大呢?

它必須大到足以和過去未來的一切有效車牌號一一對應(yīng),你才可能做到“直接按號訪問”。

假設(shè)車牌號共8位,每位可以使用26個英文字母或10個阿拉伯數(shù)字,那么不同的車牌號共有36^8=2821109907456種。

哪怕每輛車只需一個字節(jié)的存儲空間,這也是接近3T的空間!

而事實上,哪怕最大的超市,修一個夠停一萬輛車的停車場也都太夸張了。

你看,這完全行不通啊。

那么,有沒有辦法在得到O(1)的查找效率的同時、又不付出太大的空間代價呢?

沒錯,的確是有的。這就是哈希表。

哈希表是怎么玩的呢?

很簡單,我們把你的車牌號看作一個8位36進制的數(shù)字;為了方便,我們可以把它轉(zhuǎn)換成十進制。

那么,你的車牌號就是一個不大于2821109907456的數(shù)字。

現(xiàn)在,我們把你的車牌號除以一萬,只取余數(shù)——你看,你的車牌號是不是就和0~10000之間的數(shù)字對應(yīng)起來了?

很好,你的車就停在這個數(shù)字對應(yīng)的停車位上,過去開就是了——O(1)的查找效率!

這個“把你的車牌號映射進0~10000之間”的操作,就是所謂的“散列”“雜湊”“哈?!被蛘遠ash(當然,實踐上,為了盡量減少沖突,哈希表的空間大小會盡量取質(zhì)數(shù))。

相對于“以key為下標直接訪問的數(shù)組”,哈希表是“時間換空間”;相對于二分法查找,哈希表又是“以空間換時間”。這種“中庸”的定位使得它在許多場合極為好用。

等等,你發(fā)覺不對:我的車尾號3456,我朋友的車也是這個尾號。我們總不能停在同一個位置吧?

你這個方案有瑕疵啊!

沒錯,hash可能會把不同的數(shù)據(jù)映射到同一個點上,術(shù)語稱其為“碰撞”。

由于hash自身的基本原理,碰撞是不可避免的。

怎么解決這個“碰撞”問題呢?

幾種解決思路:

1、臨時加個“立體車庫”,哪里碰撞往哪放。于是車子就可以在同一位置“撂起來”存了。這叫“開鏈表法”。

2、車庫面積肯定是夠的。3456號被人占了,你存3457不就好了!

換句話說,過去的散列函數(shù)是 (車牌號 模除 10000),發(fā)現(xiàn)碰撞了就換散列函數(shù) (車牌號加1 模除 10000)試一試——這叫“再散列法”。

3、再修個小車庫,碰撞了的停小車庫去(小車庫可以隨便停,也可以搞一套別的機制)

總之,如此一來,我們就同時得到了“O(1)的查找效率”和“可接受的空間消耗”。

任何時候,當你有“數(shù)量有限”但“不同索引數(shù)量極大”的一些數(shù)據(jù),必需極高的訪問效率同時又不想無端消耗太多的存儲空間時,你就可以考慮使用哈希表了。

當然,請注意,因為沖突的存在,哈希表雖然有著優(yōu)異的平均訪問時間(常數(shù)訪問效率!);但它的“最大訪問時間”卻是沒有保證的——你可能一個微秒甚至幾個納秒就拿到了數(shù)據(jù),也可能幾十個毫秒了還在鏈表上狂奔。因此實時性要求嚴格的場合,用它前需要謹慎考慮。

知道了哈希表的設(shè)計思路,我們就可以進入稍微困難一些的部分了。

我們已經(jīng)知道,所謂“哈希表”,實際上是我們把對象(value)的“鍵值(key)”轉(zhuǎn)換成了“數(shù)組下標”;然后就可以借助這個下標一步到位的找到對應(yīng)對象(value)了。

但這中間有“瑕疵”存在:和身份證號和公民一一對應(yīng)不同,鍵值和下標并不是一對一的關(guān)系。就好像你的車牌尾號是23456而你朋友是53456,結(jié)果把你們安排到同一個停車位一樣。

很容易想到,很多數(shù)據(jù)的鍵值(key)分布存在一定的規(guī)律。

比如,身份證其中4位是你的出生年份,這四位其實只能當兩位用;再如,手機號碼前3位只可能是135、132、153等少數(shù)數(shù)字……

那么,如果我們的“鍵值轉(zhuǎn)換數(shù)組下標的函數(shù)(也就是哈希函數(shù))”選擇不當(比如給手機前幾位、身份證的出生年份字段這些相對缺乏變化的數(shù)據(jù)過高權(quán)值),就很容易使得“碰撞”頻繁出現(xiàn)。

這就對哈希函數(shù)的選擇提出了要求。

但是哈希函數(shù)本身也不應(yīng)該過于復(fù)雜,不然每次計算耗時太久——O(1)雖然是常數(shù)時間;但如果時間常數(shù)太長,它可能就不如O(lnN)查找算法快。

要知道,在一百萬數(shù)據(jù)里面做二分法搜索,最差時也不過需要20次搜索而已;如果你的哈希函數(shù)本身需要的計算時間已經(jīng)超過了這個限度,那么改用二分法顯然是個更為理智的選擇:不僅更快,還更省空間。

工程問題,向來是需要根據(jù)實際情況靈活選擇、做出合理折衷的。

扯遠了。

繼續(xù)說哈希函數(shù)。

很顯然,用于哈希表的哈希函數(shù)可不能是MD5或者sha1系列函數(shù),太慢;但也不能直接模除一個整數(shù),太容易出現(xiàn)沖突。

簡單說就是:哈希表用到的哈希函數(shù),一方面要能盡量把key均勻散布在表空間中(從而盡量減少沖突),另一方面又要有盡量快的計算速度。

這類函數(shù)有很多種,稍微搜一搜就能找到很多。

無論如何,原理所限,哈希表中碰撞無法絕對避免。

當碰撞發(fā)生時,就不得不使用開鏈表法或再散列法存儲沖突數(shù)據(jù);而這必將影響哈希表的性能。

很容易想到,如果哈希表很大、里面卻沒存幾條數(shù)據(jù),那么它出現(xiàn)沖突(碰撞)的幾率就會很?。环粗?,如果哈希表已經(jīng)接近滿了,那么每條新加入的數(shù)據(jù)都會產(chǎn)生碰撞。

換句話說,在哈希函數(shù)選擇合理的前提下,想要減少碰撞,就只能擴大哈希表占用的空間。

哈希表實際所存數(shù)據(jù)量和哈希表最大容量之間的比值,叫做哈希表的“加載因子”。

加載因子越小,沖突的概率就越低,但浪費大量空間;加載因子越高,沖突概率越大,但空間浪費就越少。這是一個需要根據(jù)工程實踐靈活選擇的折衷值。很多語言提供的hash表允許你主動調(diào)節(jié)這個值。

一般來說,一個較為平衡的加載因子大約是0.7~0.8左右。這樣既不會浪費太多空間,也不至于出現(xiàn)太多沖突。

另一方面,因為哈希表使用的哈希函數(shù)較為簡單,因此對惡意的攻擊者來說,他可以精心構(gòu)造一大堆數(shù)據(jù)提交給你——所有這些數(shù)據(jù)散列后全都存在一個格子里。

我們前面提到過,當遇到這種沖突/碰撞時,為了避免彼此覆蓋,這些數(shù)據(jù)就要存在鏈表中(或者再散列后存在同一個哈希表中)。

當這些數(shù)據(jù)被存進鏈表時,對它們的訪問效率將降到O(N)——因為鏈表搜索效率只有O(N)。

之前就發(fā)生過這種攻擊,包括Java在內(nèi)的許多種語言全部落馬。

解決方案也很簡單:

1、提高哈希函數(shù)復(fù)雜度,想辦法加入隨機性(相當于每次使用一個不同的哈希函數(shù)),避免被人輕易捕捉到弱點

2、不要用開鏈表法存儲沖突數(shù)據(jù),采用“再散列法”,并且使用不同的哈希函數(shù)再散列、還可以把沖突數(shù)據(jù)存入另一個表——要構(gòu)造同時讓兩個以上不同的哈希函數(shù)沖突的攻擊數(shù)據(jù),難度就大得多了。

總之,哈希表是用途廣泛的一種數(shù)據(jù)結(jié)構(gòu),也是很多編程語言提供的基礎(chǔ)服務(wù)之一(比如python的dict)。

你完全可以傻瓜式的使用它,無須搞懂它的一切;但想要把它用精、用好,你還是需要真正理解它的來龍去脈——千萬不要 像某種最好的語言的作者那樣,拿函數(shù)名長度當哈希值(哎呀一不小心又黑人了,頂鍋逃……

想揍人你們找他:賀師?。篜HP黑系列之二:PHP 為什么函數(shù)命名是如此不一致?

RECOMMEND

- 點個在看你最好看 -

    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    国产传媒一区二区三区| 黑人巨大精品欧美一区二区区| 久久福利视频在线观看| 扒开腿狂躁女人爽出白浆av| 免费在线成人午夜视频| 欧美午夜色视频国产精品| 日韩在线精品视频观看| 久久中文字人妻熟女小妇| 亚洲女同一区二区另类| 老司机精品福利视频在线播放| 在线视频三区日本精品| 91精品国自产拍老熟女露脸| 一区二区三区日本高清| 激情三级在线观看视频| 亚洲欧美日本成人在线| 午夜福利直播在线视频| 在线观看国产午夜福利| 国产高清精品福利私拍| 色婷婷久久五月中文字幕| 成人精品欧美一级乱黄| 成人免费视频免费观看| 亚洲一区二区精品久久av| 老鸭窝老鸭窝一区二区| 亚洲午夜av久久久精品| 精品精品国产自在久久高清| 亚洲国产成人爱av在线播放下载| 国产精品涩涩成人一区二区三区| 欧美成人国产精品高清| 99久久精品久久免费| 日韩中文字幕视频在线高清版| 日韩在线中文字幕不卡| 果冻传媒精选麻豆白晶晶| 美女被后入福利在线观看| 成人亚洲国产精品一区不卡| 好骚国产99在线中文| 日韩中文字幕狠狠人妻| 色一情一乱一区二区三区码| 激情内射亚洲一区二区三区| 国产又粗又猛又长又大| 国产成人亚洲综合色就色| 精品人妻久久一品二品三品|