算法之路,带你轻松学废算法系列之哈希表
文章目錄
- 前言
 - 引入
 - 1、哈希函數(shù)的引入
 - 2、沖突
 - 3、開(kāi)放地址法
 - ①、線性探測(cè)
 - ②、裝填因子
 - ③、二次探測(cè)
 - ④、再哈希法
 
- 4、鏈地址法
 - 5、桶
 - 6、總結(jié)
 - 結(jié)尾
 
前言
大家好,我是魏果果哦,算法呢,是我們程序員一生無(wú)法避免的墊腳石,但是好多人面對(duì)晦澀難懂的算法知識(shí)望卻生畏,為了讓大家看明白算法,從今天開(kāi)始,我會(huì)和大家一起啃算法,博客的核心思想是原作者知乎張曉康所寫,我進(jìn)行了代碼的部分補(bǔ)充與完善測(cè)試,我會(huì)盡量讓晦澀難懂的算法更加輕松理解,讓大家感受到算法的精妙與唯美,讓大家學(xué)的更加輕松。好啦,廢話不多說(shuō),滴滴滴,大家開(kāi)始上車嘍!🚂
 原文鏈接為:https://zhuanlan.zhihu.com/p/37473142
引入
Hash表也稱散列表,也有直接譯作哈希表,Hash表是一種根據(jù)關(guān)鍵字值(key - value)而直接進(jìn)行訪問(wèn)的數(shù)據(jù)結(jié)構(gòu)。它基于數(shù)組,通過(guò)把關(guān)鍵字映射到數(shù)組的某個(gè)下標(biāo)來(lái)加快查找速度,但是又和數(shù)組、鏈表、樹(shù)等數(shù)據(jù)結(jié)構(gòu)不同,在這些數(shù)據(jù)結(jié)構(gòu)中查找某個(gè)關(guān)鍵字,通常要遍歷整個(gè)數(shù)據(jù)結(jié)構(gòu),也就是O(N)的時(shí)間級(jí),但是對(duì)于哈希表來(lái)說(shuō),只是O(1)的時(shí)間級(jí)。
注意,這里有個(gè)重要的問(wèn)題就是如何把關(guān)鍵字轉(zhuǎn)換為數(shù)組的下標(biāo),這個(gè)轉(zhuǎn)換的函數(shù)稱為哈希函數(shù)(也稱散列函數(shù)),轉(zhuǎn)換的過(guò)程稱為哈希化。
正文:
1、哈希函數(shù)的引入
大家都用過(guò)字典,字典的優(yōu)點(diǎn)是我們可以通過(guò)前面的目錄快速定位到所要查找的單詞。如果我們想把一本英文字典的每個(gè)單詞,從 a 到 zyzzyva(這是牛津字典的最后一個(gè)單詞),都寫入計(jì)算機(jī)內(nèi)存,以便快速讀寫,那么哈希表是個(gè)不錯(cuò)的選擇。
這里我們將范圍縮小點(diǎn),比如想在內(nèi)存中存儲(chǔ)5000個(gè)英文單詞。我們可能想到每個(gè)單詞會(huì)占用一個(gè)數(shù)組單元,那么數(shù)組的大小是5000,同時(shí)可以用數(shù)組下標(biāo)存取單詞,這樣設(shè)想很完美,但是數(shù)組下標(biāo)和單詞怎么建立聯(lián)系呢?
首先我們要建立單詞和數(shù)字(數(shù)組下標(biāo))的關(guān)系:
我們知道 ASCII 是一種編碼,其中 a 表示97,b表示98,以此類推,一直到122表示z,而每個(gè)單詞都是由這26個(gè)字母組成,我們可以不用 ASCII 編碼那么大的數(shù)字,自己設(shè)計(jì)一套類似 ASCII的編碼,比如a表示1,b表示2,依次類推,z表示26,那么表示方法我們就知道了。
接下來(lái)如何把單個(gè)字母的數(shù)字組合成代表整個(gè)單詞的數(shù)字呢?
①、把數(shù)字相加
首先第一種簡(jiǎn)單的方法就是把單詞的每個(gè)字母表示的數(shù)字相加,得到的和便是數(shù)組的下標(biāo)。
比如單詞 cats 轉(zhuǎn)換成數(shù)字:
cats = 3 + 1 + 20 + 19 = 43
那么單詞 cats 存儲(chǔ)在數(shù)組中的下標(biāo)為43,所有的英文單詞都可以用這個(gè)辦法轉(zhuǎn)換成數(shù)組下標(biāo)。但是這個(gè)辦法真的可行嗎?
假設(shè)我們約定一個(gè)單詞最多有 10 個(gè)字母,那么字典的最后一個(gè)單詞為 zzzzzzzzzz ,其轉(zhuǎn)換為數(shù)字:
zzzzzzzzzz = 26*10 = 260
那么我們可以得到單詞編碼的范圍是從1-260。很顯然,這個(gè)范圍是不夠存儲(chǔ)5000個(gè)單詞的,那么肯定有一個(gè)位置存儲(chǔ)了多個(gè)單詞,每個(gè)數(shù)組的數(shù)據(jù)項(xiàng)平均要存儲(chǔ)192個(gè)單詞(5000除以260)。
對(duì)于上面的問(wèn)題,我們?nèi)绾谓鉀Q呢?
第一種方法:考慮每個(gè)數(shù)組項(xiàng)包含一個(gè)子數(shù)組或者一個(gè)子鏈表,這個(gè)辦法存數(shù)據(jù)項(xiàng)確實(shí)很快,但是如果我們想要從192個(gè)單詞中查找到其中一個(gè),那么還是很慢。
第二種方法:為啥要讓那么多單詞占據(jù)同一個(gè)數(shù)據(jù)項(xiàng)呢?也就是說(shuō)我們沒(méi)有把單詞分的足夠開(kāi),數(shù)組能表示的元素太少,我們需要擴(kuò)展數(shù)組的下標(biāo),使其每個(gè)位置都只存放一個(gè)單詞。
對(duì)于上面的第二種方法,問(wèn)題產(chǎn)生了,我們?nèi)绾螖U(kuò)展數(shù)組的下標(biāo)呢?
②、冪的連乘
我們將單詞表示的數(shù)拆成數(shù)列,用適當(dāng)?shù)?27 的冪乘以這些位數(shù)(因?yàn)橛?6個(gè)可能的字符,以及空格,一共27個(gè)),然后把乘積相加,這樣就得出了每個(gè)單詞獨(dú)一無(wú)二的數(shù)字。
比如把單詞cats 轉(zhuǎn)換為數(shù)字:
cats = 3273 + 1272 + 20271 + 19270 = 59049 + 729 + 540 + 19 = 60337
這個(gè)過(guò)程會(huì)為每個(gè)單詞創(chuàng)建一個(gè)獨(dú)一無(wú)二的數(shù),但是注意的是我們這里只是計(jì)算了 4 個(gè)字母組成的單詞,如果單詞很長(zhǎng),比如最長(zhǎng)的10個(gè)字母的單詞 zzzzzzzzzz,僅僅是279 結(jié)果就超出了7000000000000,這個(gè)結(jié)果是很巨大的,在實(shí)際內(nèi)存中,根本不可能為一個(gè)數(shù)組分配這么大的空間。
所以這個(gè)方案的問(wèn)題就是雖然為每個(gè)單詞都分配了獨(dú)一無(wú)二的下標(biāo),但是只有一小部分存放了單詞,很大一部分都是空著的。那么現(xiàn)在就需要一種方法,把數(shù)位冪的連乘系統(tǒng)中得到的巨大的整數(shù)范圍壓縮到可接受的數(shù)組范圍中。
對(duì)于英語(yǔ)字典,假設(shè)只有5000個(gè)單詞,這里我們選定容量為10000 的數(shù)組空間來(lái)存放(后面會(huì)介紹為啥需要多出一倍的空間)。那么我們就需要將從 0 到超過(guò) 7000000000000 的范圍,壓縮到從0到10000的范圍。
第一種方法:取余,得到一個(gè)數(shù)被另一個(gè)整數(shù)除后的余數(shù)。首先我們假設(shè)要把從0-199的數(shù)字(用largeNumber表示),壓縮為從0-9的數(shù)字(用smallNumber表示),后者有10個(gè)數(shù),所以變量smallRange 的值為10,這個(gè)轉(zhuǎn)換的表達(dá)式為:
smallNumber = largeNumber % smallRange
當(dāng)一個(gè)數(shù)被 10 整除時(shí),余數(shù)一定在0-9之間,這樣,我們就把從0-199的數(shù)壓縮為從0-9的數(shù),壓縮率為 20 :1。
我們也可以用類似的方法把表示單詞唯一的數(shù)壓縮成數(shù)組的下標(biāo):
arrayIndex = largerNumber % smallRange
這也就是哈希函數(shù)。它把一個(gè)大范圍的數(shù)字哈希(轉(zhuǎn)化)成一個(gè)小范圍的數(shù)字,這個(gè)小范圍的數(shù)對(duì)應(yīng)著數(shù)組的下標(biāo)。使用哈希函數(shù)向數(shù)組插入數(shù)據(jù)后,這個(gè)數(shù)組就是哈希表。
2、沖突
把巨大的數(shù)字范圍壓縮到較小的數(shù)字范圍,那么肯定會(huì)有幾個(gè)不同的單詞哈希化到同一個(gè)數(shù)組下標(biāo),即產(chǎn)生了沖突。
沖突可能會(huì)導(dǎo)致哈希化方案無(wú)法實(shí)施,前面我們說(shuō)指定的數(shù)組范圍大小是實(shí)際存儲(chǔ)數(shù)據(jù)的兩倍,因此可能有一半的空間是空著的,所以,當(dāng)沖突產(chǎn)生時(shí),一個(gè)方法是通過(guò)系統(tǒng)的方法找到數(shù)組的一個(gè)空位,并把這個(gè)單詞填入,而不再用哈希函數(shù)得到數(shù)組的下標(biāo),這種方法稱為開(kāi)放地址法。比如加入單詞 cats 哈希化的結(jié)果為5421,但是它的位置已經(jīng)被單詞parsnip占用了,那么我們會(huì)考慮將單詞 cats 存放在parsnip后面的一個(gè)位置 5422 上。
另一種方法,前面我們也提到過(guò),就是數(shù)組的每個(gè)數(shù)據(jù)項(xiàng)都創(chuàng)建一個(gè)子鏈表或子數(shù)組,那么數(shù)組內(nèi)不直接存放單詞,當(dāng)產(chǎn)生沖突時(shí),新的數(shù)據(jù)項(xiàng)直接存放到這個(gè)數(shù)組下標(biāo)表示的鏈表中,這種方法稱為鏈地址法。
3、開(kāi)放地址法
開(kāi)發(fā)地址法中,若數(shù)據(jù)項(xiàng)不能直接存放在由哈希函數(shù)所計(jì)算出來(lái)的數(shù)組下標(biāo)時(shí),就要尋找其他的位置。分別有三種方法:線性探測(cè)、二次探測(cè)以及再哈希法。
①、線性探測(cè)
在線性探測(cè)中,它會(huì)線性的查找空白單元。比如如果 5421 是要插入數(shù)據(jù)的位置,但是它已經(jīng)被占用了,那么就使用5422,如果5422也被占用了,那么使用5423,以此類推,數(shù)組下標(biāo)依次遞增,直到找到空白的位置。這就叫做線性探測(cè),因?yàn)樗刂鴶?shù)組下標(biāo)一步一步順序的查找空白單元。
完整代碼:
package hash;public class MyHashTable1 {private DataItem[] hashArray; // 存儲(chǔ)DataItem類型的哈希數(shù)組private int arraySize; // 數(shù)組初始大小private int ItemNum; // 數(shù)組實(shí)際存了多少數(shù)據(jù)private DataItem nonItem;// 用于刪除數(shù)據(jù)項(xiàng)public static void main(String[] args) {MyHashTable1 table = new MyHashTable1(12);for (int i = 0; i < 10; i++) {table.insert(new DataItem((int)(Math.random()*100)));}table.display();System.out.println();System.out.println(table.find(new DataItem(91)));}public MyHashTable1(int size) {this.arraySize = size;hashArray = new DataItem[size];nonItem = null;}// 插入public void insert(DataItem item) {if (isFull()) {System.out.println("哈希表擴(kuò)容成功");extendHashTable();}int index = hashFunction(item.getKey());while(hashArray[index]!=null ) {index++;index%=arraySize;}hashArray[index] = item;ItemNum++;}// 刪除public DataItem delete(DataItem item) {if(isEmpty()) {System.out.println("hashtable 為空");return null;}int index = hashFunction(item.getKey());while(hashArray[index]!=null) {if(hashArray[index].getKey() == item.getKey()) {DataItem temp = hashArray[index];hashArray[index] = nonItem;ItemNum--;return temp;}index++;index%=arraySize;}return null;}// 查找public int find(DataItem item) {if(isEmpty()) {System.out.println("hashtable 為空");return -1;}int index = hashFunction(item.getKey());int num = 1; //記錄查找次數(shù),避免查找不存在的數(shù)據(jù)陷入死循環(huán)while(hashArray[index]!=null) {if(hashArray[index].getKey() == item.getKey()) {return index;}if(num == arraySize) {break;}num++;index++;index%=arraySize;}return -1;}// 獲取下標(biāo)public int hashFunction(int key) {return key % arraySize;}// 擴(kuò)容 /*** 數(shù)組有固定的大小,而且不能擴(kuò)展,所以擴(kuò)展哈希表只能另外創(chuàng)建一個(gè)更大的數(shù)組,然后把舊數(shù)組中的數(shù)據(jù)插到新的數(shù)組中。* 但是哈希表是根據(jù)數(shù)組大小計(jì)算給定數(shù)據(jù)的位置的,所以這些數(shù)據(jù)項(xiàng)不能再放在新數(shù)組中和老數(shù)組相同的位置上。* 因此不能直接拷貝,需要按順序遍歷老數(shù)組,并使用insert方法向新數(shù)組中插入每個(gè)數(shù)據(jù)項(xiàng)。* 這個(gè)過(guò)程叫做重新哈希化。這是一個(gè)耗時(shí)的過(guò)程,但如果數(shù)組要進(jìn)行擴(kuò)展,這個(gè)過(guò)程是必須的。*/public void extendHashTable() {DataItem[] temp = hashArray;hashArray = new DataItem[arraySize<<1];arraySize<<=1;for (int i = 0; i < temp.length; i++) {insert(temp[i]);}}//打印public void display() {for (int i = 0; i < hashArray.length; i++) {System.out.print(hashArray[i]+" ");}}private boolean isFull() {return ItemNum == arraySize;}private boolean isEmpty() {return ItemNum == 0;}static class DataItem {private int data;public DataItem(int data) {this.data = data;}public int getKey() {return data;}@Overridepublic String toString() {return ""+data;}} }需要注意的是,當(dāng)哈希表變得太滿時(shí),我們需要擴(kuò)展數(shù)組,但是需要注意的是,數(shù)據(jù)項(xiàng)不能放到新數(shù)組中和老數(shù)組相同的位置,而是要根據(jù)數(shù)組大小重新計(jì)算插入位置。這是一個(gè)比較耗時(shí)的過(guò)程,所以一般我們要確定數(shù)據(jù)的范圍,給定好數(shù)組的大小,而不再擴(kuò)容。
另外,當(dāng)哈希表變得比較滿時(shí),我們每插入一個(gè)新的數(shù)據(jù),都要頻繁的探測(cè)插入位置,因?yàn)榭赡芎芏辔恢枚急磺懊娌迦氲臄?shù)據(jù)所占用了,這稱為聚集。數(shù)組填的越滿,聚集越可能發(fā)生。
這就像人群,當(dāng)某個(gè)人在商場(chǎng)暈倒時(shí),人群就會(huì)慢慢聚集。最初的人群聚過(guò)來(lái)是因?yàn)榭吹搅四莻€(gè)倒下的人,而后面聚過(guò)來(lái)的人是因?yàn)樗鼈兿胫肋@些人聚在一起看什么。人群聚集的越大,吸引的人就會(huì)越多。
②、裝填因子
已填入哈希表的數(shù)據(jù)項(xiàng)和表長(zhǎng)的比率叫做裝填因子,比如有10000個(gè)單元的哈希表填入了6667 個(gè)數(shù)據(jù)后,其裝填因子為 2/3。當(dāng)裝填因子不太大時(shí),聚集分布的比較連貫,而裝填因子比較大時(shí),則聚集發(fā)生的很大了。
我們知道線性探測(cè)是一步一步的往后面探測(cè),當(dāng)裝填因子比較大時(shí),會(huì)頻繁的產(chǎn)生聚集,那么如果我們探測(cè)比較大的單元,而不是一步一步的探測(cè)呢,這就是下面要講的二次探測(cè)。
③、二次探測(cè)
二測(cè)探測(cè)是防止聚集產(chǎn)生的一種方式,思想是探測(cè)相距較遠(yuǎn)的單元,而不是和原始位置相鄰的單元。
線性探測(cè)中,如果哈希函數(shù)計(jì)算的原始下標(biāo)是x, 線性探測(cè)就是x+1, x+2, x+3, 以此類推;而在二次探測(cè)中,探測(cè)的過(guò)程是x+1, x+4, x+9, x+16,以此類推,到原始位置的距離是步數(shù)的平方。二次探測(cè)雖然消除了原始的聚集問(wèn)題,但是產(chǎn)生了另一種更細(xì)的聚集問(wèn)題,叫二次聚集:比如講184,302,420和544依次插入表中,它們的映射都是7,那么302需要以1為步長(zhǎng)探測(cè),420需要以4為步長(zhǎng)探測(cè), 544需要以9為步長(zhǎng)探測(cè)。只要有一項(xiàng)其關(guān)鍵字映射到7,就需要更長(zhǎng)步長(zhǎng)的探測(cè),這個(gè)現(xiàn)象叫做二次聚集。二次聚集不是一個(gè)嚴(yán)重的問(wèn)題,但是二次探測(cè)不會(huì)經(jīng)常使用,因?yàn)檫€有好的解決方法,比如再哈希法。
④、再哈希法
為了消除原始聚集和二次聚集,我們使用另外一種方法:再哈希法。
我們知道二次聚集的原因是,二測(cè)探測(cè)的算法產(chǎn)生的探測(cè)序列步長(zhǎng)總是固定的:1,4,9,16以此類推。那么我們想到的是需要產(chǎn)生一種依賴關(guān)鍵字的探測(cè)序列,而不是每個(gè)關(guān)鍵字都一樣,那么,不同的關(guān)鍵字即使映射到相同的數(shù)組下標(biāo),也可以使用不同的探測(cè)序列。
方法是把關(guān)鍵字用不同的哈希函數(shù)再做一遍哈希化,用這個(gè)結(jié)果作為步長(zhǎng)。對(duì)于指定的關(guān)鍵字,步長(zhǎng)在整個(gè)探測(cè)中是不變的,不過(guò)不同的關(guān)鍵字使用不同的步長(zhǎng)。
第二個(gè)哈希函數(shù)必須具備如下特點(diǎn):
一、和第一個(gè)哈希函數(shù)不同
二、不能輸出0(否則,將沒(méi)有步長(zhǎng),每次探測(cè)都是原地踏步,算法將陷入死循環(huán))。
專家們已經(jīng)發(fā)現(xiàn)下面形式的哈希函數(shù)工作的非常好:stepSize = constant - key % constant; 其中constant是質(zhì)數(shù),且小于數(shù)組容量。
   再哈希法要求表的容量是一個(gè)質(zhì)數(shù),假如表長(zhǎng)度為15(0-14),非質(zhì)數(shù),有一個(gè)特定關(guān)鍵字映射到0,步長(zhǎng)為5,則探測(cè)序列是0,5,10,0,5,10,以此類推一直循環(huán)下去。算法只嘗試這三個(gè)單元,所以不可能找到某些空白單元,最終算法導(dǎo)致崩潰。如果數(shù)組容量為13, 質(zhì)數(shù),探測(cè)序列最終會(huì)訪問(wèn)所有單元。即0,5,10,2,7,12,4,9,1,6,11,3,一直下去,只要表中有一個(gè)空位,就可以探測(cè)到它。
完整再哈希法代碼:
package hash;public class HashDouble1 {private DataItem[] hashArray; // 存儲(chǔ)DataItem類型的哈希數(shù)組private int arraySize; // 數(shù)組初始大小private int ItemNum; // 數(shù)組實(shí)際存了多少數(shù)據(jù)private DataItem nonItem;// 用于刪除數(shù)據(jù)項(xiàng)public static void main(String[] args) {HashDouble1 table = new HashDouble1();for (int i = 0; i < 10; i++) {table.insert(new DataItem((int) (Math.random()*100)));}table.display();System.out.println();System.out.println(table.arraySize);System.out.println(table.find(new DataItem(91)));}public HashDouble1() {this.arraySize = 17;hashArray = new DataItem[arraySize];nonItem = null;}// 插入public void insert(DataItem item) {if (isFull()) {System.out.println("哈希表擴(kuò)容成功");extendHashTable();}int index = hashFunction1(item.getKey());int step = hashFunction2(item.getKey());while (hashArray[index] != null) {index += step;index %= arraySize;}hashArray[index] = item;ItemNum++;}// 刪除public DataItem delete(DataItem item) {if (isEmpty()) {System.out.println("hashtable 為空");return null;}int index = hashFunction1(item.getKey());int step = hashFunction2(item.getKey());while (hashArray[index] != null) {if (hashArray[index].getKey() == item.getKey()) {DataItem temp = hashArray[index];hashArray[index] = nonItem;ItemNum--;System.out.println("被刪除的位置為:");return temp;}index += step;index %= arraySize;}return null;}// 查找public int find(DataItem item) {if (isEmpty()) {System.out.println("hashtable 為空");return -1;}int index = hashFunction1(item.getKey());int step = hashFunction2(item.getKey());int num = 1; // 記錄查找次數(shù),避免查找不存在的數(shù)據(jù)陷入死循環(huán)while (hashArray[index] != null) {if (hashArray[index].getKey() == item.getKey()) {return index;}if (num == arraySize) {break;}num += step;index++;index %= arraySize;}return -1;}// 獲取下標(biāo)public int hashFunction1(int key) {return key % arraySize;}public int hashFunction2(int key) {/*** 專家們已經(jīng)發(fā)現(xiàn)下面形式的哈希函數(shù)工作的非常好: stepSize = constant - key % constant;* 其中constant是質(zhì)數(shù),且小于數(shù)組容量。*/return 5 - key % 5;}// 擴(kuò)容public void extendHashTable() {DataItem[] temp = hashArray;hashArray = new DataItem[arraySize << 1];arraySize <<= 1;for (int i = 0; i < temp.length; i++) {insert(temp[i]);}}// 打印public void display() {for (int i = 0; i < hashArray.length; i++) {System.out.print(hashArray[i] + " ");}}private boolean isFull() {return ItemNum == arraySize;}private boolean isEmpty() {return ItemNum == 0;}static class DataItem {private int data;public DataItem(int data) {this.data = data;}public int getKey() {return data;}@Overridepublic String toString() {return "" + data;}} }4、鏈地址法
在開(kāi)放地址法中,通過(guò)再哈希法尋找一個(gè)空位解決沖突問(wèn)題,另一個(gè)方法是在哈希表每個(gè)單元中設(shè)置鏈表(即鏈地址法),某個(gè)數(shù)據(jù)項(xiàng)的關(guān)鍵字值還是像通常一樣映射到哈希表的單元,而數(shù)據(jù)項(xiàng)本身插入到這個(gè)單元的鏈表中。其他同樣映射到這個(gè)位置的數(shù)據(jù)項(xiàng)只需要加到鏈表中,不需要在原始的數(shù)組中尋找空位。
有序鏈表:
public class SortLink {public static void main(String[] args) {SortLink link = new SortLink();int[] a = new int[] {100,2,5,9,3,1};for (int i = 0; i < a.length; i++) {link.insert(a[i]);}link.delete(1);link.print();}private Node head;public SortLink() {head = new Node(-1, null);}//插入public void insert(int value) {Node p = head;Node q = head.next;while(q!=null && q.data<value) {p = q;q = q.next;}p.next = new Node(value, q);}//刪除public void delete(int value) {Node p = head;Node q = head.next;while(q!=null && q.data<value) {p = q;q = q.next;}if(q.data == value) {p.next = q.next;}}public void print() {Node p = head;Node q = head.next;while(q!=null) {System.out.print(q+" ");q = q.next;}}static class Node {int data;Node next;public Node(int data, Node next) {this.data = data;this.next = next;}public Node(Node next) {this.next = next;}@Overridepublic String toString() {// TODO Auto-generated method stubreturn ""+data;}} }鏈地址法:
package hash;public class HashChain {private SortLink[] hashArray;// 數(shù)組中存放鏈表private int arraySize;public static void main(String[] args) {HashChain chain = new HashChain(3);int[] a = new int[] {100,2,5,9,3,1};for (int i = 0; i < a.length; i++) {chain.insert(new Node(a[i], null));}chain.delete(100);chain.displayTable();}public HashChain(int size) {arraySize = size;hashArray = new SortLink[arraySize];// new 出每個(gè)空鏈表初始化數(shù)組for (int i = 0; i < arraySize; i++) {hashArray[i] = new SortLink();}}public void displayTable() {for (int i = 0; i < arraySize; i++) {System.out.println("hashArray" + i);hashArray[i].print();System.out.println();}}public int hashFunction(int key) {return key % arraySize;}public void insert(Node node) {int key = node.data;int hashVal = hashFunction(key);hashArray[hashVal].insert(node.data);// 直接往鏈表中添加即可}public Node delete(int key) {int hashVal = hashFunction(key);Node temp = find(key);hashArray[hashVal].delete(key);// 從鏈表中找到要?jiǎng)h除的數(shù)據(jù)項(xiàng),直接刪除return temp;}public Node find(int key) {int hashVal = hashFunction(key);Node node = new Node(hashArray[hashVal].find(key), null);return node;}static class Node {int data;Node next;public Node(int data, Node next) {this.data = data;this.next = next;}public Node(Node next) {this.next = next;}@Overridepublic String toString() {// TODO Auto-generated method stubreturn "" + data;}} }鏈地址法中,裝填因子(數(shù)據(jù)項(xiàng)數(shù)和哈希表容量的比值)與開(kāi)放地址法不同,在鏈地址法中,需要有N個(gè)單元的數(shù)組中轉(zhuǎn)入N個(gè)或更多的數(shù)據(jù)項(xiàng),因此裝填因子一般為1,或比1大(有可能某些位置包含的鏈表中包含兩個(gè)或兩個(gè)以上的數(shù)據(jù)項(xiàng))。
找到初始單元需要O(1)的時(shí)間級(jí)別,而搜索鏈表的時(shí)間與M成正比,M為鏈表包含的平均項(xiàng)數(shù),即O(M)的時(shí)間級(jí)別。
5、桶
另外一種方法類似于鏈地址法,它是在每個(gè)數(shù)據(jù)項(xiàng)中使用子數(shù)組,而不是鏈表。這樣的數(shù)組稱為桶。
這個(gè)方法顯然不如鏈表有效,因?yàn)橥暗娜萘坎缓眠x擇,如果容量太小,可能會(huì)溢出,如果太大,又造成性能浪費(fèi),而鏈表是動(dòng)態(tài)分配的,不存在此問(wèn)題。所以一般不使用桶。
6、總結(jié)
哈希表基于數(shù)組,類似于key-value的存儲(chǔ)形式,關(guān)鍵字值通過(guò)哈希函數(shù)映射為數(shù)組的下標(biāo),如果一個(gè)關(guān)鍵字哈希化到已占用的數(shù)組單元,這種情況稱為沖突。用來(lái)解決沖突的有兩種方法:開(kāi)放地址法和鏈地址法。在開(kāi)發(fā)地址法中,把沖突的數(shù)據(jù)項(xiàng)放在數(shù)組的其它位置;在鏈地址法中,每個(gè)單元都包含一個(gè)鏈表,把所有映射到同一數(shù)組下標(biāo)的數(shù)據(jù)項(xiàng)都插入到這個(gè)鏈表中。
結(jié)尾
我們的哈希表就講到這里嘍
大家學(xué)廢了嗎,學(xué)廢了的同學(xué)請(qǐng)?jiān)谠u(píng)論區(qū)扣666或者打卡都行。。
全篇有點(diǎn)長(zhǎng),能堅(jiān)持下去看到最后的都是棒棒的~~
暫時(shí)定為一周一更,更新速度還是看你們學(xué)沒(méi)學(xué)廢了,有疑問(wèn)的請(qǐng)?jiān)谠u(píng)論區(qū)留言,經(jīng)過(guò)討論的往往記憶更加深刻↖(ω)↗
總結(jié)
以上是生活随笔為你收集整理的算法之路,带你轻松学废算法系列之哈希表的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
                            
                        - 上一篇: Greenplum实战--standby
 - 下一篇: win7 开启梦幻桌面