HashMap奪命14問,你能堅(jiān)持到第幾問?
這里是碼農(nóng)充電第一站,回復(fù)“666”,獲取一份專屬大禮包 真愛,請?jiān)O(shè)置“星標(biāo)”或點(diǎn)個(gè)“在看”

當(dāng)鏈表超過8且數(shù)組長度(數(shù)據(jù)總量)超過64才會(huì)轉(zhuǎn)為紅黑樹
將鏈表轉(zhuǎn)換成紅黑樹前會(huì)判斷,如果當(dāng)前數(shù)組的長度小于64,那么會(huì)選擇先進(jìn)行數(shù)組擴(kuò)容,而不是轉(zhuǎn)換為紅黑樹,以減少搜索時(shí)間。

hashmap存取是無序的
鍵和值位置都可以是null,但是鍵位置只能是一個(gè)null
鍵位置是唯一的,底層的數(shù)據(jù)結(jié)構(gòu)是控制鍵的
jdk1.8前數(shù)據(jù)結(jié)構(gòu)是:鏈表+數(shù)組jdk1.8之后是:數(shù)組+鏈表+紅黑樹
閾值(邊界值)>8并且數(shù)組長度大于64,才將鏈表轉(zhuǎn)換成紅黑樹,變成紅黑樹的目的是提高搜索速度,高效查詢
開放定址法也稱為再散列法,基本思想就是,如果p=H(key)出現(xiàn)沖突時(shí),則以p為基礎(chǔ),再次hash,p1=H(p),如果p1再次出現(xiàn)沖突,則以p1為基礎(chǔ),以此類推,直到找到一個(gè)不沖突的哈希地址pi。因此開放定址法所需要的hash表的長度要大于等于所需要存放的元素,而且因?yàn)榇嬖谠俅蝖ash,所以只能在刪除的節(jié)點(diǎn)上做標(biāo)記,而不能真正刪除節(jié)點(diǎn)
再哈希法(雙重散列,多重散列),提供多個(gè)不同的hash函數(shù),R1=H1(key1)發(fā)生沖突時(shí),再計(jì)算R2=H2(key1),直到?jīng)]有沖突為止。這樣做雖然不易產(chǎn)生堆集,但增加了計(jì)算的時(shí)間。
鏈地址法(拉鏈法),將哈希值相同的元素構(gòu)成一個(gè)同義詞的單鏈表,并將單鏈表的頭指針存放在哈希表的第i個(gè)單元中,查找、插入和刪除主要在同義詞鏈表中進(jìn)行,鏈表法適用于經(jīng)常進(jìn)行插入和刪除的情況。
建立公共溢出區(qū),將哈希表分為公共表和溢出表,當(dāng)溢出發(fā)生時(shí),將所有溢出數(shù)據(jù)統(tǒng)一放到溢出區(qū)
開放定址法只能使用同一種hash函數(shù)進(jìn)行再次hash,再哈希法可以調(diào)用多種不同的hash函數(shù)進(jìn)行再次hash

平方取中法
取余數(shù)
偽隨機(jī)數(shù)法
首先根據(jù)key的值計(jì)算hash值,找到該元素在數(shù)組中存儲(chǔ)的下標(biāo)
如果數(shù)組是空的,則調(diào)用resize進(jìn)行初始化;
如果沒有哈希沖突直接放在對應(yīng)的數(shù)組下標(biāo)里
如果沖突了,且key已經(jīng)存在,就覆蓋掉value
如果沖突后是鏈表結(jié)構(gòu),就判斷該鏈表是否大于8,如果大于8并且數(shù)組容量小于64,就進(jìn)行擴(kuò)容;如果鏈表節(jié)點(diǎn)數(shù)量大于8并且數(shù)組的容量大于64,則將這個(gè)結(jié)構(gòu)轉(zhuǎn)換成紅黑樹;否則,鏈表插入鍵值對,若key存在,就覆蓋掉value
如果沖突后,發(fā)現(xiàn)該節(jié)點(diǎn)是紅黑樹,就將這個(gè)節(jié)點(diǎn)掛在樹上

final Node[] resize() { //oldTab:引用擴(kuò)容前的哈希表Node[] oldTab = table; //oldCap:表示擴(kuò)容前的table數(shù)組的長度int oldCap = (oldTab == null) ? 0 : oldTab.length;//獲得舊哈希表的擴(kuò)容閾值int oldThr = threshold;//newCap:擴(kuò)容之后table數(shù)組大小//newThr:擴(kuò)容之后下次觸發(fā)擴(kuò)容的條件int newCap, newThr = 0;//條件成立說明hashMap中的散列表已經(jīng)初始化過了,是一次正常擴(kuò)容if (oldCap > 0) {//判斷舊的容量是否大于等于最大容量,如果是,則無法擴(kuò)容,并且設(shè)置擴(kuò)容條件為int最大值,//這種情況屬于非常少數(shù)的情況if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}//設(shè)置newCap新容量為oldCap舊容量的二倍(<<1),并且<最大容量,而且>=16,則新閾值等于舊閾值的兩倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}//如果oldCap=0并且邊界值大于0,說明散列表是null,但此時(shí)oldThr>0//說明此時(shí)hashMap的創(chuàng)建是通過指定的構(gòu)造方法創(chuàng)建的,新容量直接等于閾值//1.new HashMap(intitCap,loadFactor)//2.new HashMap(initCap)//3.new HashMap(map)else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;//這種情況下oldThr=0;oldCap=0,說明沒經(jīng)過初始化,創(chuàng)建hashMap//的時(shí)候是通過new HashMap()的方式創(chuàng)建的else { // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}//newThr為0時(shí),通過newCap和loadFactor計(jì)算出一個(gè)newThrif (newThr == 0) {//容量*0.75float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})//根據(jù)上面計(jì)算出的結(jié)果創(chuàng)建一個(gè)更長更大的數(shù)組Node[] newTab = (Node [])new Node[newCap]; //將table指向新創(chuàng)建的數(shù)組table = newTab;//本次擴(kuò)容之前table不為nullif (oldTab != null) {//對數(shù)組中的元素進(jìn)行遍歷for (int j = 0; j < oldCap; ++j) {//設(shè)置e為當(dāng)前node節(jié)點(diǎn)Nodee; //當(dāng)前桶位數(shù)據(jù)不為空,但不能知道里面是單個(gè)元素,還是鏈表或紅黑樹,//e = oldTab[j],先用e記錄下當(dāng)前元素if ((e = oldTab[j]) != null) {//將老數(shù)組j桶位置為空,方便回收oldTab[j] = null;//如果e節(jié)點(diǎn)不存在下一個(gè)節(jié)點(diǎn),說明e是單個(gè)元素,則直接放置在新數(shù)組的桶位if (e.next == null)newTab[e.hash & (newCap - 1)] = e;//如果e是樹節(jié)點(diǎn),證明該節(jié)點(diǎn)處于紅黑樹中else if (e instanceof TreeNode)((TreeNode)e).split(this, newTab, j, oldCap); //e為鏈表節(jié)點(diǎn),則對鏈表進(jìn)行遍歷else { // preserve order//低位鏈表:存放在擴(kuò)容之后的數(shù)組的下標(biāo)位置,與當(dāng)前數(shù)組下標(biāo)位置一致//loHead:低位鏈表頭節(jié)點(diǎn)//loTail低位鏈表尾節(jié)點(diǎn)NodeloHead = null, loTail = null; //高位鏈表,存放擴(kuò)容之后的數(shù)組的下標(biāo)位置,=原索引+擴(kuò)容之前數(shù)組容量//hiHead:高位鏈表頭節(jié)點(diǎn)//hiTail:高位鏈表尾節(jié)點(diǎn)NodehiHead = null, hiTail = null; Nodenext; do {next = e.next;//oldCap為16:10000,與e.hsah做&運(yùn)算可以得到高位為1還是0//高位為0,放在低位鏈表if ((e.hash & oldCap) == 0) {if (loTail == null)//loHead指向eloHead = e;elseloTail.next = e;loTail = e;}//高位為1,放在高位鏈表else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);//低位鏈表已成,將頭節(jié)點(diǎn)loHead指向在原位if (loTail != null) {loTail.next = null;newTab[j] = loHead;}//高位鏈表已成,將頭節(jié)點(diǎn)指向新索引if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}
保持原位置不動(dòng)(新bit位為0時(shí))
散列原索引+擴(kuò)容大小的位置去(新bit位為1時(shí))


因?yàn)镾tring是不可變的,當(dāng)創(chuàng)建字符串時(shí),它的hashcode被緩存下來,不需要再次計(jì)算,相對于其他對象更快
因?yàn)楂@取對象的時(shí)候要用到equals()和hashCode()方法,那么鍵對象正確的重寫這兩個(gè)方法是非常重要的,這些類很規(guī)范的重寫了hashCode()以及equals()方法
* Because TreeNodes are about twice the size of regular nodes, we* use them only when bins contain enough nodes to warrant use* (see TREEIFY_THRESHOLD). And when they become too small (due to* removal or resizing) they are converted back to plain bins. In* usages with well-distributed user hashCodes, tree bins are* rarely used. Ideally, under random hashCodes, the frequency of* nodes in bins follows a Poisson distribution* (http://en.wikipedia.org/wiki/Poisson_distribution) with a* parameter of about 0.5 on average for the default resizing* threshold of 0.75, although with a large variance because of* resizing granularity. Ignoring variance, the expected* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /* factorial(k)).
因?yàn)闃涔?jié)點(diǎn)的大小大約是普通節(jié)點(diǎn)的兩倍,所以我們只在箱子包含足夠的節(jié)點(diǎn)時(shí)才使用樹節(jié)點(diǎn)(參見TREEIFY_THRESHOLD)。當(dāng)他們邊的太小(由于刪除或調(diào)整大小)時(shí),就會(huì)被轉(zhuǎn)換回普通的桶,在使用分布良好的hashcode時(shí),很少使用樹箱。理想情況下,在隨機(jī)哈希碼下,箱子中節(jié)點(diǎn)的頻率服從泊松分布第一個(gè)值是:* 0: 0.60653066* 1: 0.30326533* 2: 0.07581633* 3: 0.01263606* 4: 0.00157952* 5: 0.00015795* 6: 0.00001316* 7: 0.00000094* 8: 0.00000006* more: less than 1 in ten million
從平均查找長度來看,紅黑樹的平均查找長度是logn,如果長度為8,則logn=3,而鏈表的平均查找長度為n/4,長度為8時(shí),n/2=4,所以閾值8能大大提高搜索速度
當(dāng)長度為6時(shí)紅黑樹退化為鏈表是因?yàn)閘ogn=log6約等于2.6,而n/2=6/2=3,兩者相差不大,而紅黑樹節(jié)點(diǎn)占用更多的內(nèi)存空間,所以此時(shí)轉(zhuǎn)換最為友好
多線程下擴(kuò)容死循環(huán)。JDK1.7中的HashMap使用頭插法插入元素,在多線程的環(huán)境下,擴(kuò)容的時(shí)候有可能導(dǎo)致環(huán)形鏈表的出現(xiàn),形成死循環(huán)。因此JDK1.8使用尾插法插入元素,在擴(kuò)容時(shí)會(huì)保持鏈表元素原本的順序,不會(huì)出現(xiàn)環(huán)形鏈表的問題
多線程的put可能導(dǎo)致元素的丟失。多線程同時(shí)執(zhí)行put操作,如果計(jì)算出來的索引位置是相同的,那會(huì)造成前一個(gè)key被后一個(gè)key覆蓋,從而導(dǎo)致元素的丟失。此問題在JDK1.7和JDK1.8中都存在
put和get并發(fā)時(shí),可能導(dǎo)致get為null。線程1執(zhí)行put時(shí),因?yàn)樵貍€(gè)數(shù)超出threshold而導(dǎo)致rehash,線程2此時(shí)執(zhí)行g(shù)et,有可能導(dǎo)致這個(gè)問題,此問題在JDK1.7和JDK1.8中都存在
我們計(jì)算索引需要將hashCode值與length-1進(jìn)行按位與運(yùn)算,如果數(shù)組長度很小,比如16,這樣的值和hashCode做異或?qū)嶋H上只有hashCode值的后4位在進(jìn)行運(yùn)算,hash值是一個(gè)隨機(jī)值,而如果產(chǎn)生的hashCode值高位變化很大,而低位變化很小,那么有很大概率造成哈希沖突,所以我們?yōu)榱耸乖馗玫纳⒘校瑢ash值的高位也利用起來\


(完)
碼農(nóng)突圍資料鏈接
1、臥槽!字節(jié)跳動(dòng)《算法中文手冊》火了,完整版 PDF 開放下載!
2、計(jì)算機(jī)基礎(chǔ)知識總結(jié)與操作系統(tǒng) PDF 下載
3、艾瑪,終于來了!《LeetCode Java版題解》.PDF
4、Github 10K+,《LeetCode刷題C/C++版答案》出爐.PDF歡迎添加魚哥個(gè)人微信:smartfish2020,進(jìn)粉絲群或圍觀朋友圈。
