數(shù)據(jù)結(jié)構(gòu)與算法之美筆記——散列表(上)

摘要:

散列表」(Hash Table)或「Hash 表」是基于數(shù)組擴(kuò)展的數(shù)據(jù)結(jié)構(gòu)屉佳,能夠?qū)?fù)雜信息通過「Hash 算法」生成「Hash 值」武花,以對應(yīng)數(shù)組下標(biāo),完成快速隨機訪問數(shù)據(jù)的功能专钉。

"我"從哪里來

我們已經(jīng)知道隨機訪問數(shù)組元素時間復(fù)雜度只有 O(1) ,效率極高炮沐,當(dāng)我們想利用數(shù)組的這個特性時就需要將元素下標(biāo)與存儲信息對應(yīng)大年。例如翔试,一個商店只有四件商品复旬,依次編號 0 至 3驹碍,這樣就可以將四件商品信息按照編號對應(yīng)下標(biāo)的方式存儲到數(shù)組中,依據(jù)編號就可以快速從數(shù)組中找到相應(yīng)商品信息怔球。

如果一段時間之后,商店盈利并且重新進(jìn)貨 100 件商品钧舌,商家想對大量商品在編號上區(qū)分類別,這時候需要使用類別編號加順序編號的方式標(biāo)識每件商品洼冻,這種編號變得復(fù)雜撞牢,并不能直接對應(yīng)數(shù)組下標(biāo),此時的商品編號又該如何對應(yīng)數(shù)組下標(biāo)以實現(xiàn)快速查找商品的功能播掷?這時候我們可以將類別編號去除之后按照順序編號對應(yīng)數(shù)組下標(biāo),同樣也能享受數(shù)組高效率隨機訪問的福利砰嘁。這個例子中,商品編號稱為「」或「關(guān)鍵字」斟冕,將鍵轉(zhuǎn)化為數(shù)組對應(yīng)下標(biāo)的方法就是「散列函數(shù)」或「Hash 函數(shù)」磕蛇,由散列函數(shù)生成的值叫做「散列值」或「Hash 值」秀撇,而這樣的數(shù)組就是散列表向族。

散列表的關(guān)鍵

從散列表的原理來看再扭,數(shù)據(jù)通過散列函數(shù)計算得到散列值是關(guān)鍵泛范,這個步驟中散列函數(shù)又是其中的核心紊撕,一個散列函數(shù)需要遵守以下三個原則柠傍。

  • 生成的散列值是非負(fù)整數(shù)
  • 如果 k_1=k_2惧笛,那么 hash(k_1)=hash(k_2)
  • 如果 k_1\neq{k_2}逞泄,那么 hash(k_1)\neq{hash(k_2)}

因為散列函數(shù)生成的散列值對應(yīng)數(shù)組下標(biāo)喷众,而數(shù)組下標(biāo)就是非負(fù)整數(shù)到千,所以需要滿足第一個原則憔四;兩個相等的數(shù)據(jù)經(jīng)過散列算法得到的散列值肯定相等般眉,否則利用散列值在散列表中查找數(shù)據(jù)就無從談起甸赃;至于第三個原則雖然在情理之中埠对,卻不那么容易做到鸠窗,即使是被廣泛運用的散列算法也會出現(xiàn)散列值沖突的情況稍计,導(dǎo)致無法滿足第三個原則臣嚣。

散列函數(shù)作為散列表的核心部分硅则,必然不能拖散列表的執(zhí)行效率后腿怎虫,畢竟散列表的查詢大审、插入和刪除操作都需要經(jīng)過散列函數(shù),所以散列函數(shù)不能太復(fù)雜根穷,執(zhí)行效率不能太低屿良。由于散列函數(shù)不可避免地都會出現(xiàn)散列沖突情況尘惧,散列函數(shù)要盡量降低散列沖突,使散列值能夠均勻地分布在散列表中。

如何解決散列沖突

解決散列沖突主要有「開放尋址」(open addressing)和「鏈表法」(chaining)兩類方法谅将。

開放尋址

開放尋址法是指插入操作時漾狼,當(dāng)生成的散列值對應(yīng)槽位已經(jīng)被其他數(shù)據(jù)占用重慢,就探測空閑位置供插入使用饥臂,其中探測方法又分為「線性探測」(Linear Probing)、「二次探測」(Quadratic Probing)和「雙重散列」(Double hashing)三種似踱。

三種探測方法

線性探測是其中較為簡單的一種隅熙,這種探測方式是當(dāng)遇到散列沖突的情況就順序查找(查找到數(shù)組尾部時轉(zhuǎn)向數(shù)組頭部繼續(xù)查找),直到查找到空槽將數(shù)據(jù)插入核芽。當(dāng)進(jìn)行查找操作時囚戚,也是同樣的操作,利用散列值從散列表中取出對應(yīng)元素轧简,與目標(biāo)數(shù)據(jù)比對,如果不相等就繼續(xù)順序查找,直到查找到對應(yīng)元素或遇到空槽為止,最壞情況下查找操作的時間復(fù)雜度可能會下降為 O(n)睹限。

散列表除了支持插入和查找操作外顺囊,當(dāng)然也支持刪除操作晕换,不過并不能將需刪除的元素置為空。如果刪除操作是將元素置為空的話敏释,查找操作遇到空槽就會結(jié)束蜂大,存儲在被刪除元素之后的數(shù)據(jù)就可能無法正確查找到澳叉,這時的刪除操作應(yīng)該使用標(biāo)記的方式所踊,而不是使用將元素置空,當(dāng)查找到被標(biāo)識已刪除的元素將繼續(xù)查找,而不是就此停止慈鸠。

線性探測是一次一個元素的探測督笆,二次探測就是使用都是線性探測的二次方步長探測。例如線性探測是 hash(key)+0, hash(key)+1, hash(key)+2,...嫂伞,那二次探測對應(yīng)的就是 hash(key)+0^2, hash(key)+1^2, hash(key)+2^2,...

雙重探測是當(dāng)?shù)谝粋€散列函數(shù)沖突時使用第二個散列函數(shù)運算散列值刊驴,利用這種方式探測躲惰。例如诡宗,當(dāng) hash_1(key) 沖突時蛀柴,就使用 hash_2(key) 計算散列值,如果再沖突就使用 hash_3(key) 計算散列值超燃,依此類推圣猎。

動態(tài)擴(kuò)容的困境

關(guān)于散列表的空位多少使用「裝載因子」(load factor)表示欠啤,裝載因子滿足數(shù)學(xué)關(guān)系 裝載因子=\frac{散列表已存儲數(shù)據(jù)量}{散列表可存儲數(shù)據(jù)總量}疾呻,也就是說裝載因子越大蟆肆,散列表的空閑空間越小,散列沖突的可能性也就越大赁温,一般我們會保持散列表有一定比例的空閑空間股囊。

為了保持散列表一定比例的空閑空間居灯,在裝載因子到達(dá)一定閾值時需要對散列表數(shù)據(jù)進(jìn)行搬移岩灭,但散列表搬移比較耗時。你可以試想下這樣的步驟珍德,在申請一個新的更大的散列表空間后蛔垢,需要將舊散列表的數(shù)據(jù)重新通過散列函數(shù)生成散列值巩梢,再存儲到新散列表中创泄,想想都覺得麻煩。

散列表搬移的操作肯定會降低散列表的操作效率括蝠,那能不能對這一過程進(jìn)行改進(jìn)验烧?其實可以將低效的擴(kuò)容操作分?jǐn)傊敛迦氩僮鳎?dāng)裝載因子達(dá)到閾值時不一次性進(jìn)行散列表搬移又跛,而是在每次插入操作時將一個舊散列表數(shù)據(jù)搬移至新散列表碍拆,這樣搬移操作的執(zhí)行效率得到了提高,插入操作的時間復(fù)雜度也依然能保持 O(1) 的高效慨蓝。當(dāng)新舊兩個散列表同時存在時查詢操作就要略作修改感混,需先在新散列表中查詢,如果沒有查找到目標(biāo)數(shù)據(jù)再到舊散列表中查找礼烈。

當(dāng)然弧满,如果你對內(nèi)存有更高效的利用要求,可以在裝載因子降低至某一閾值時對散列表進(jìn)行縮容處理此熬。

鏈表法

除了開放尋址之外庭呜,還可以使用鏈表法解決散列沖突的問題。散列值對應(yīng)的槽位并不直接存儲數(shù)據(jù)犀忱,而是將數(shù)據(jù)存儲在槽位對應(yīng)的鏈表上募谎,當(dāng)進(jìn)行查找操作時,根據(jù)散列函數(shù)計算的散列值找到對應(yīng)槽位阴汇,再在槽位對應(yīng)的鏈表上查找對應(yīng)數(shù)據(jù)数冬。

鏈表法操作的時間復(fù)雜度與散列表槽位和數(shù)據(jù)在槽位上的分布情況有關(guān),假設(shè)有 n 個數(shù)據(jù)均勻分布在 m 個槽位的散列表上搀庶,那鏈表法的時間復(fù)雜度為 O(\frac{n}{m})拐纱。鏈表法可以不用像開放尋址一樣關(guān)心裝載因子,但需要注意散列函數(shù)對散列值的計算哥倔,使鏈表結(jié)點能夠盡可能均勻地分布在散列表槽位上秸架,避免散列表退化為鏈表。有時黑客甚至?xí)闹圃鞌?shù)據(jù)咆蒿,利用散列函數(shù)制造散列沖突东抹,使數(shù)據(jù)集中某些槽位上,造成散列表性能的極度退化蜡秽。

面對這樣的惡意行為散列表只能坐以待斃嗎府阀?其實不然,當(dāng)槽位上的鏈表過長時芽突,可以將其改造成之前學(xué)習(xí)過的跳表等试浙,鏈表改造為跳表后查詢的時間復(fù)雜度也只是退化為 O(\log{n}),依然是可以接受的范圍寞蚌。

鏈表法在存儲利用上比開放尋址更加高效田巴,不用提前申請存儲空間钠糊,當(dāng)有新數(shù)據(jù)時申請一個新的結(jié)點就行。而且鏈表法對裝載因子也不那么敏感壹哺,裝載因子的增高也只是意味著槽位對應(yīng)的鏈表更長而已抄伍,鏈表增長也有將鏈表改造為跳表等結(jié)構(gòu)的應(yīng)對策略,所以鏈表法在裝載因子超過 1 的情況下都可保持高效管宵。

開放尋址 VS 鏈表法

開放尋址不存在像鏈表法一樣有鏈表過長而導(dǎo)致效率降低的煩惱截珍,不過裝載因子是開放尋址的晴雨表,裝載因子過高會造成散列沖突機率的上升箩朴,開放尋址就需要不斷探測空閑位置岗喉,算法的執(zhí)行成本會不斷被提高。而且在刪除操作時只能將數(shù)據(jù)先標(biāo)記為刪除炸庞,對于頻繁增刪的數(shù)據(jù)效率會受到影響钱床。

當(dāng)然也可以在這種風(fēng)險出現(xiàn)前進(jìn)行散列表的動態(tài)擴(kuò)容,不過這樣就會出現(xiàn)大量空閑的存儲空間埠居,導(dǎo)致存儲的利用效率過低查牌,這種現(xiàn)象在數(shù)據(jù)量越大的情況下越明顯。所以開放尋址比較適用于數(shù)據(jù)量較小的情況滥壕。

鏈表法對于散列沖突的處理更加靈活纸颜,同時對存儲空間的利用效率也更高,但鏈表結(jié)點除了存儲數(shù)據(jù)外還需要存儲指針捏浊,如果存儲數(shù)據(jù)較小指針占用的存儲甚至?xí)?dǎo)致整體存儲翻倍的情況懂衩,但存儲數(shù)據(jù)較大時指針占用的存儲也就可以忽略不計,所以鏈表法較適合存儲數(shù)據(jù)對象較大金踪,但頻繁的增刪操作不會對鏈表法造成明顯的影響。因為這樣的特點牵敷,鏈表法更加適合大數(shù)據(jù)量胡岔,或者數(shù)據(jù)對象較大的時候,如果數(shù)據(jù)操作頻繁枷餐,那鏈表法更是不二之選靶瘸。

總結(jié)

散列表由數(shù)組擴(kuò)展而來,使用散列函數(shù)將鍵計算為散列值毛肋,散列值對應(yīng)數(shù)據(jù)存儲的數(shù)組下標(biāo)怨咪。雖然散列表的執(zhí)行效率較高,但會有散列沖突的問題润匙,可以通過開放尋址法和鏈表法解決此問題诗眨。

開放尋址存儲利用效率較低,適用數(shù)據(jù)量較小并且增刪不頻繁的情況孕讳,如果數(shù)據(jù)量較大匠楚,增刪頻繁的情況更加適用鏈表法巍膘,相對之下鏈表法更加普適。


文章中如有問題歡迎留言指正
數(shù)據(jù)結(jié)構(gòu)與算法之美筆記系列將會做為我對王爭老師此專欄的學(xué)習(xí)筆記芋簿,如想了解更多王爭老師專欄的詳情請到極客時間自行搜索峡懈。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市与斤,隨后出現(xiàn)的幾起案子肪康,更是在濱河造成了極大的恐慌,老刑警劉巖撩穿,帶你破解...
    沈念sama閱讀 222,464評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件梅鹦,死亡現(xiàn)場離奇詭異,居然都是意外死亡冗锁,警方通過查閱死者的電腦和手機齐唆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來冻河,“玉大人箍邮,你說我怎么就攤上這事∵缎穑” “怎么了锭弊?”我有些...
    開封第一講書人閱讀 169,078評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長擂错。 經(jīng)常有香客問我味滞,道長,這世上最難降的妖魔是什么钮呀? 我笑而不...
    開封第一講書人閱讀 59,979評論 1 299
  • 正文 為了忘掉前任剑鞍,我火速辦了婚禮,結(jié)果婚禮上爽醋,老公的妹妹穿的比我還像新娘蚁署。我一直安慰自己,他們只是感情好蚂四,可當(dāng)我...
    茶點故事閱讀 69,001評論 6 398
  • 文/花漫 我一把揭開白布光戈。 她就那樣靜靜地躺著,像睡著了一般遂赠。 火紅的嫁衣襯著肌膚如雪久妆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,584評論 1 312
  • 那天跷睦,我揣著相機與錄音筷弦,去河邊找鬼。 笑死送讲,一個胖子當(dāng)著我的面吹牛奸笤,可吹牛的內(nèi)容都是我干的惋啃。 我是一名探鬼主播,決...
    沈念sama閱讀 41,085評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼监右,長吁一口氣:“原來是場噩夢啊……” “哼边灭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起健盒,我...
    開封第一講書人閱讀 40,023評論 0 277
  • 序言:老撾萬榮一對情侶失蹤绒瘦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扣癣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惰帽,經(jīng)...
    沈念sama閱讀 46,555評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,626評論 3 342
  • 正文 我和宋清朗相戀三年父虑,在試婚紗的時候發(fā)現(xiàn)自己被綠了该酗。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,769評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡士嚎,死狀恐怖呜魄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情莱衩,我是刑警寧澤爵嗅,帶...
    沈念sama閱讀 36,439評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站笨蚁,受9級特大地震影響睹晒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜括细,卻給世界環(huán)境...
    茶點故事閱讀 42,115評論 3 335
  • 文/蒙蒙 一伪很、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧勒极,春花似錦是掰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽炫彩。三九已至匾七,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間江兢,已是汗流浹背昨忆。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留杉允,地道東北人邑贴。 一個月前我還...
    沈念sama閱讀 49,191評論 3 378
  • 正文 我出身青樓席里,卻偏偏與公主長得像,于是被迫代替她去往敵國和親拢驾。 傳聞我的和親對象是個殘疾皇子奖磁,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,781評論 2 361