CMU 15445 5. hash 表

https://15445.courses.cs.cmu.edu/fall2018/slides/06-hashtables.pdf

DBMS對系統(tǒng)內(nèi)部的許多不同部分使用各種數(shù)據(jù)結(jié)構(gòu):
?內(nèi)部元數(shù)據(jù):跟蹤有關(guān)數(shù)據(jù)庫和系統(tǒng)狀態(tài)的信息。
?核心數(shù)據(jù)存儲:可用作數(shù)據(jù)庫中元組的基本存儲。
?臨時數(shù)據(jù)結(jié)構(gòu):DBMS可以在處理查詢時動態(tài)構(gòu)建數(shù)據(jù)結(jié)構(gòu)以加速執(zhí)行(例如,用于連接的哈希表)。
?表索引:輔助數(shù)據(jù)結(jié)構(gòu),以便更容易找到特定元組。
設(shè)計決策:
1.數(shù)據(jù)組織:我們?nèi)绾尾季謨?nèi)存以及在數(shù)據(jù)結(jié)構(gòu)中存儲哪些信息。
2.并發(fā):如何在不引起問題的情況下啟用多個線程來訪問數(shù)據(jù)結(jié)構(gòu)请毛。

Hash 表 設(shè)計理念

哈希表實(shí)現(xiàn)了將鍵映射到值的關(guān)聯(lián)數(shù)組抽象數(shù)據(jù)類型。
哈希表實(shí)現(xiàn)由兩部分組成:

?散列函數(shù):如何將大密鑰空間映射到較小的空間里瞭亮。我們需要考慮如何在快速與沖突率之間的權(quán)衡方仿。

?散列方案:如何在散列后處理關(guān)鍵沖突。 需要考慮需要分配大型哈希表以減少沖突與執(zhí)行其他指令以查找/插入密鑰之間的權(quán)衡统翩。

考慮一個簡單的靜態(tài)哈希表實(shí)現(xiàn):
?為每個元素分配一個帶有一個插槽的巨型陣列仙蚜。 通過元素數(shù)修改鍵以查找數(shù)組中的偏移量。
?有問題的假設(shè):
你提前了解了元素的數(shù)量
2.每個密鑰都是唯一的
3.完美散列函數(shù)(如果key1厂汗!= key2則hash(key1)鳍征!= hash(key2))

經(jīng)典的hash算法比較

首先又一些hash函數(shù),我們會想到面徽,如md5,crc,sha艳丛。但是我們不需要這些加密哈希函數(shù),因?yàn)槲覀儾恍枰獜墓V蝎@取密鑰趟紊。 我們只關(guān)心速度和碰撞率氮双。
下面是幾種hash函數(shù)的吞吐率。


image.png

靜態(tài)hash結(jié)構(gòu)

1 開放尋址法

哈希沖突解決策略:開放尋址法(Open Addressing)

通常采用的沖突解決策略為開放尋址法(Open Addressing)霎匈,將所有的元素都存放在哈希表內(nèi)的數(shù)組中戴差,不使用額外的數(shù)據(jù)結(jié)構(gòu)。

開放尋址法的最簡單的一種實(shí)現(xiàn)就是線性探查(Linear Probing)铛嘱,步驟如下:

  1. 當(dāng)插入新的元素時暖释,使用哈希函數(shù)在哈希表中定位元素位置;
  2. 檢查哈希表中該位置是否已經(jīng)存在元素墨吓。如果該位置內(nèi)容為空球匕,則插入并返回,否則轉(zhuǎn)向步驟 3帖烘。
  3. 如果該位置為 i亮曹,則檢查 i+1 是否為空,如果已被占用,則檢查 i+2照卦,依此類推式矫,直到找到一個內(nèi)容為空的位置。

線性探查(Linear Probing)方式雖然簡單役耕,但并不是解決沖突的最好的策略采转,因?yàn)樗鼤?dǎo)致同類哈希的聚集(Primary Clustering)。這導(dǎo)致搜索哈希表時瞬痘,沖突依然存在故慈。如果我們要訪問 Edward 的信息,因?yàn)?Edward 的社保號 111-00-1235 哈希為 1235图云,然而我們在 1235 位置找到的是 Bob惯悠,所以再搜索 1236邻邮,找到的卻是 Danny竣况,以此類推直到找到 Edward。

2. 羅賓漢哈希

羅賓漢哈贤惭希可以顯著降低探查長度的方差丹泉。我們來看一下它是怎么做的:

對每個哈希表中的元素,記錄插入時的探查長度
當(dāng)插入一個新元素時鸭蛙,對于探查過程中的元素摹恨,如果它的探查長度小于當(dāng)前插入元素的探查長度,那么交換這兩個元素 (以及探測長度)娶视,然后繼續(xù)探查
也就是說晒哄,事實(shí)上大家的探查長度更加平均了,所以期望最長探查長度也會顯著的下降肪获。這也是羅賓漢 (英國傳說中的俠盜) 哈希名字的來源寝凌,劫富濟(jì)貧。雖然大部分元素的探查長度都更趨近于平均值孝赫,不是一次就能查到较木,但是由于這部分開銷較 CPU 加載 cache line 開銷可以忽略不計,所以整體上仍有顯著的提高青柄。

如果沒有沖突伐债,數(shù)值是0,如果沖突1次數(shù)值為1.


image.png
image.png

當(dāng)沖突的次數(shù)超過當(dāng)前的次數(shù)了致开,就先把這個坑占了峰锁,如圖e 占了 d。
就要把當(dāng)前的d再計1次沖突双戳。

image.png
image.png

3. cuckoo hash

使用具有不同散列函數(shù)的多個散列表祖今。
→在插入時,檢查每個表并選擇任何有空閑插槽的表。
→如果沒有表有空閑插槽千诬,則從其中一個中刪除該元素耍目,然后重新散列它以找到新位置。
查找和刪除始終為O(1)徐绑,因?yàn)槊總€哈希表只檢查一個位置邪驮。

移動鍵時,確保我們不會陷入無限循環(huán)傲茄。
如果我們找到一個循環(huán)毅访,那么我們可以用新的散列函數(shù)重建整個散列表。
→使用兩個哈希函數(shù)盘榨,我們(可能)不需要重建表喻粹,直到它滿50%。
→使用三個哈希函數(shù)草巡,我們(可能)不需要重建表守呜,直到它滿約90%。


image.png

image.png

動態(tài)hash結(jié)構(gòu)

1. Chained Hash

?為哈希表中的每個槽維護(hù)一個桶的鏈表山憨。
?通過將具有相同散列鍵的元素放入同一個存儲桶中來解決沖突查乒。
?如果存儲桶已滿,請?zhí)砑恿硪粋€存儲桶列表郁竟。 哈希表可以無限增長玛迄,因?yàn)槟粩嗵砑有峦啊?br> ?要處理并發(fā)性,您只需要在每個存儲桶上設(shè)置一個latch


image.png

?非唯一鍵的方法

1.單獨(dú)的鏈表:將值存儲在單獨(dú)的存儲區(qū)域中
2.存儲在存儲桶中:將重復(fù)的密鑰存儲在相同的存儲桶中(把value 和key 存在一起)


image.png

2. extensible hash

http://www.cosc.brocku.ca/~efoxwell/2P03/slides/Week12Slides.pdf
實(shí)現(xiàn)見project 1

3. linear hash

線性哈希是一種動態(tài)擴(kuò)展哈希表的方法棚亩。

線性哈希的數(shù)學(xué)原理:

假定key = 5 蓖议、 9 、13

key % 4 = 1

現(xiàn)在我們對8求余

5 % 8 = 5

9 % 8=1

13 % 8 = 5

由上面的規(guī)律可以得出

(任意key) % n = M

(任意key) %2n = M或 (任意key) %2n = M + n

線性哈希的具體實(shí)現(xiàn):

我們假設(shè)初始化的哈希表如下:

image.png

為了方便敘述讥蟆,我們作出以下假定:

1:為了使哈希表能進(jìn)行動態(tài)的分裂勒虾,我們從桶0開始設(shè)定一個分裂點(diǎn)。

2:一個桶的容量為listSize = 5攻询,當(dāng)桶的容量超出后就從分裂點(diǎn)開始進(jìn)行分裂从撼。

3:hash函數(shù)為 h0 = key %4 h1 = key % 8,h1會在分裂時使用钧栖。

4:整個表初始化包含了4個桶低零,桶號為0-3,并已提前插入了部分的數(shù)據(jù)拯杠。

分裂過程如下:

現(xiàn)在插入key = 27

1:進(jìn)行哈希運(yùn)算掏婶,h0 = 27 % 4 = 3

2:將key = 27插入桶3,但發(fā)現(xiàn)桶3已經(jīng)達(dá)到了桶的容量潭陪,所以觸發(fā)哈希分裂

3:由于現(xiàn)在分裂點(diǎn)處于0桶雄妥,所以我們對0桶進(jìn)行分割最蕾。這里需要注意雖然這里是3桶滿了,但我們并不會直接從3桶進(jìn)行分割老厌,而是從分割點(diǎn)進(jìn)行分割瘟则。這里為什么這么做,在下面會進(jìn)一步介紹枝秤。

4:對分割點(diǎn)所指向的桶(桶0)所包含的key采用新的hash函數(shù)(h1)進(jìn)行分割醋拧。

對所有key進(jìn)行新哈希函數(shù)運(yùn)算后,將產(chǎn)生如下的哈希表

image.png

5:雖然進(jìn)行了分裂淀弹,但桶3并不是分裂點(diǎn)丹壕,所以桶3會將多出的key,放于溢出頁.,一直等到桶3進(jìn)行分裂薇溃。

6:進(jìn)行分裂后菌赖,將分裂點(diǎn)向后移動一位。

一次完整的分裂結(jié)束沐序。

key的讀取:

采用h0對key進(jìn)行計算琉用。

如果算出的桶號小于了分裂點(diǎn),表示桶已經(jīng)進(jìn)行的分裂薄啥,我們采用h1進(jìn)行hash運(yùn)算辕羽,算出key所對應(yīng)的真正的桶號逛尚。再從真正的桶里取出value

如果算出的桶號大于了分裂點(diǎn)垄惧,那么表示此桶還沒進(jìn)行分裂,直接從當(dāng)前桶進(jìn)行讀取value绰寞。

說明:

1:如果下一次key插入0到逊、1、2滤钱、4桶觉壶,是不會觸發(fā)分裂。(沒有超出桶的容量)如果是插入桶3件缸,用戶在實(shí)現(xiàn)時可以自己設(shè)定铜靶,可以一旦插入就觸發(fā),也可以等溢出頁達(dá)到listSize再觸發(fā)新的分裂他炊。

2:現(xiàn)在0桶被分裂了争剿,新數(shù)據(jù)的插入怎么才能保證沒分裂的桶能正常工作,已經(jīng)分裂的桶能將部分插入到新分裂的桶呢?

只要分裂點(diǎn)小于桶的總數(shù)痊末,我們依然采用h0函數(shù)進(jìn)行哈希計算蚕苇。

如果哈希結(jié)果小于分裂號,那么表示這個key所插入的桶已經(jīng)進(jìn)行了分割凿叠,那么我就采用h1再次進(jìn)行哈希涩笤,而h1的哈希結(jié)果就這個key所該插入的桶號嚼吞。

如果哈希結(jié)果大于分裂號,那么表示這個key所插入的桶還沒有進(jìn)行分裂蹬碧。直接插入舱禽。

這也是為什么雖然是桶3的容量不足,但分裂的桶是分裂點(diǎn)所指向的桶恩沽。如果直接在桶3進(jìn)行分裂呢蔫,那么當(dāng)新的key插入的時候就不能正常的判斷哪些桶已經(jīng)進(jìn)行了分裂。

3:如果使用分割點(diǎn)飒筑,就具備了無限擴(kuò)展的能力片吊。當(dāng)分割點(diǎn)移動到最后一個桶(桶3)。再出現(xiàn)分裂协屡。那么分割點(diǎn)就會回到桶0俏脊,到這個時候,h0作廢肤晓,h1替代h0, h2(key % 12)替代h1爷贫。那么又可以開始動態(tài)分割。那個整個初始化狀態(tài)就發(fā)生了變化补憾。就好像沒有發(fā)生過分裂漫萄。那么上面的規(guī)則就可以循環(huán)使用。

3:線性哈希的論文中是按上面的規(guī)則來進(jìn)行分裂的盈匾。其實(shí)我們可以安裝自己的實(shí)際情況來進(jìn)行改動腾务。

假如我們現(xiàn)在希望去掉分割點(diǎn),一旦哪個桶滿了削饵,馬上對這個桶進(jìn)行分割岩瘦。

可以考慮了以下方案:

1:為所有桶增加一個標(biāo)志位。初始化的時候?qū)λ型暗臉?biāo)志位清空窿撬。

2:一旦某個桶滿了启昧,直接對這個桶進(jìn)行分割,然后將設(shè)置標(biāo)志位劈伴。當(dāng)新的數(shù)據(jù)插入的時候密末,經(jīng)過哈希計算(h0)發(fā)現(xiàn)這個桶已經(jīng)分裂了,那么就采用新的哈希函數(shù)(h1)來計算分裂之后的桶號跛璧。在讀取數(shù)據(jù)的時候處理類似严里。

   Linehash 實(shí)現(xiàn)代碼如下:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class LineHash {
    
    public int pageSize;  //桶的容量

    public int overPoint = 0; //分裂點(diǎn)

    public int listSize = 4; //哈希表的初始大小

    public int initlistSize = 4; //哈希大小的記錄值
     
    public int workRound = 1;  //分裂輪數(shù)
    
    public List> hash = null; //模擬哈希表

    public LineHash(int pageSIze) {
        this.pageSize = pageSIze;
        hash = new ArrayList>(4);
        for (int i = 0; i < listSize; i++) {
            hash.add(new HashMap()); //向哈希表中初始化桶
        }
    }
    //查詢函數(shù)
    public String getKeyValue(int key){
        int index = hashFun(key, workRound); //根據(jù)分裂輪數(shù)調(diào)用不同的哈希函數(shù)
        if(index < overPoint){              //當(dāng)前桶產(chǎn)生了分裂
            index = hashFun(key, workRound + 1); //采用新的哈希函數(shù)進(jìn)行計算
        }
        return hash.get(index).get(key);
    }
    //添加函數(shù)
    public void addKeyValue(int key, String value) { 
        int index = hashFun(key, workRound);   
        if(index < overPoint){
            index = hashFun(key, workRound + 1);
        }
        Map map = hash.get(index);
        if (map.size() < pageSize) {   //判斷當(dāng)前桶是否滿了
            map.put(key, value);
        } else {
            map.put(key, value);  
            splitHash();              //滿了就進(jìn)行分裂
        }
    }

    public int hashFun(int key, int f1) {
        return key % (4 * f1);
    }

    public void splitHash() {
        Map OldMap = hash.get(overPoint);   //舊桶
        Map NewMap = new HashMap(); //分裂產(chǎn)生的新桶

        Integer[] keyList = OldMap.keySet().toArray(new Integer[0]);
        for (int i = 0; i < keyList.length; i++) {  //準(zhǔn)備移動一半的數(shù)據(jù)到新桶
            int key = keyList[i].intValue();
            int index = hashFun(key, workRound + 1);
            if (index >= listSize) {
                String value = OldMap.get(key);
                OldMap.remove(key);
                NewMap.put(key, value);
            }
        }
        hash.add(NewMap);  //將新桶放入哈希表
        listSize++;  //哈希表長度增加
        overPoint++;  //分裂點(diǎn)移動
        if(overPoint >= initlistSize){  //分裂點(diǎn)移動了一輪就更換新的哈希函數(shù)
            workRound++;
            initlistSize = initlistSize * 2;
            overPoint = 0;
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市赡模,隨后出現(xiàn)的幾起案子田炭,更是在濱河造成了極大的恐慌,老刑警劉巖漓柑,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件教硫,死亡現(xiàn)場離奇詭異叨吮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)瞬矩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進(jìn)店門粥航,熙熙樓的掌柜王于貴愁眉苦臉地迎上來危队,“玉大人,你說我怎么就攤上這事≌忠” “怎么了葵礼?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵粗梭,是天一觀的道長艇纺。 經(jīng)常有香客問我,道長媚污,這世上最難降的妖魔是什么舀瓢? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮耗美,結(jié)果婚禮上京髓,老公的妹妹穿的比我還像新娘。我一直安慰自己商架,他們只是感情好堰怨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蛇摸,像睡著了一般备图。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上皇型,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天诬烹,我揣著相機(jī)與錄音砸烦,去河邊找鬼弃鸦。 笑死,一個胖子當(dāng)著我的面吹牛幢痘,可吹牛的內(nèi)容都是我干的唬格。 我是一名探鬼主播,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼颜说,長吁一口氣:“原來是場噩夢啊……” “哼购岗!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起门粪,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤喊积,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后玄妈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乾吻,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡髓梅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了绎签。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片枯饿。...
    茶點(diǎn)故事閱讀 39,745評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖诡必,靈堂內(nèi)的尸體忽然破棺而出奢方,到底是詐尸還是另有隱情,我是刑警寧澤爸舒,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布蟋字,位于F島的核電站,受9級特大地震影響扭勉,放射性物質(zhì)發(fā)生泄漏愉老。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一剖效、第九天 我趴在偏房一處隱蔽的房頂上張望嫉入。 院中可真熱鬧,春花似錦璧尸、人聲如沸咒林。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽垫竞。三九已至,卻和暖如春蛀序,著一層夾襖步出監(jiān)牢的瞬間欢瞪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工徐裸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留遣鼓,地道東北人。 一個月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓重贺,卻偏偏與公主長得像骑祟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子气笙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,652評論 2 354

推薦閱讀更多精彩內(nèi)容