HashMap面試題


HashMap原理:

  • “HashMap基於hashing原理,我們通過put()和get()方法儲存和獲取對象。當我們將鍵值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,讓后找到bucket位置來儲存值對象。當獲取對象時,通過鍵對象的equals()方法找到正確的鍵值對,然后返回值對象。HashMap使用鏈表來解決碰撞問題,當發生碰撞了,對象將會儲存在鏈表的下一個節點中。 HashMap在每個鏈表節點中儲存鍵值對對象。”

1.“你用過HashMap嗎?”

  • 答:“HashMap可以接受null鍵值和值,而Hashtable則不能;HashMap是非synchronized;HashMap很快;以及HashMap儲存的是鍵值對等等。這顯示出你已經用過HashMap,而且對它相當的熟悉。”

2.“你知道HashMap的工作原理嗎?” “你知道HashMap的get()方法的工作原理嗎?”

  • 答:HashMap是基於hashing的原理,我們使用put(key, value)存儲對象到HashMap中,使用get(key)從HashMap中獲取對象。當我們給put()方法傳遞鍵和值時,我們先對鍵調用hashCode()方法,返回的hashCode用於找到bucket位置來儲存Entry對象(Node 對象)。”

3.當兩個對象的hashcode相同會發生什么?”

答:因為hashcode相同,所以它們的bucket位置相同,‘碰撞’會發生。因為HashMap使用鏈表存儲對象,這個Entry(包含有鍵值對的Map.Entry對象)會存儲在鏈表中。”

  • 它們會儲存在同一個bucket位置的鏈表中。鍵對象的equals()方法用來找到鍵值對。

4.如果兩個鍵的hashcode相同,你如何獲取值對象?”

  • 答:"當我們調用get()方法,HashMap會使用鍵對象的hashcode找到bucket位置,找到bucket位置之后,會調用keys.equals()方法去找到鏈表中正確的節點,最終找到要找的值對象。" 
  • HashMap在鏈表中存儲的是鍵值對。

5.“如果HashMap的大小超過了負載因子(load factor)定義的容量,怎么辦?”

  • 答:“默認的負載因子大小為0.75,也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)一樣,將會創建原來HashMap大小的兩倍的bucket數組,來重新調整map的大小,並將原來的對象放入新的bucket數組中。這個過程叫作rehashing,因為它調用hash方法找到新的bucket位置。” 

6.“你了解重新調整HashMap大小存在什么問題嗎?”

  • 答:“當重新調整HashMap大小的時候,確實存在條件競爭,因為如果兩個線程都發現HashMap需要重新調整大小了,它們會同時試着調整大小。在調整大小的過程中,存儲在鏈表中的元素的次序會反過來,因為移動到新的bucket位置的時候,HashMap並不會將元素放在鏈表的尾部,而是放在頭部,這是為了避免尾部遍歷(tail traversing)。如果條件競爭發生了,那么就死循環了。這個時候,你可以質問面試官,為什么這么奇怪,要在多線程的環境下使用HashMap呢?”

 7.HashMap中put方法的過程?

  • 答:“調用哈希函數獲取Key對應的hash值,再計算其數組下標;
  • 如果沒有出現哈希沖突,則直接放入數組;如果出現哈希沖突,則以鏈表的方式放在鏈表后面;
  • 如果鏈表長度超過閥值( TREEIFY THRESHOLD==8),就把鏈表轉成紅黑樹,鏈表長度低於6,就把紅黑樹轉回鏈表;
  • 如果結點的key已經存在,則替換其value即可;
  • 如果集合中的鍵值對大於12,調用resize方法進行數組擴容。”

8.哈希函數怎么實現的?

  • 調用Key的hashCode方法獲取hashCode值,並將該值的高16位和低16位進行異或運算。

9.哈希沖突怎么解決?

  • 將新結點添加在鏈表后面

10.數組擴容的過程?

  • 創建一個新的數組,其容量為舊數組的兩倍,並重新計算舊數組中結點的存儲位置。結點在新數組中的位置只有兩種,原下標位置或原下標+舊數組的大小。

11.拉鏈法導致的鏈表過深問題為什么不用二叉查找樹代替,而選擇紅黑樹?為什么不一直使用紅黑樹?

  • 答:“之所以選擇紅黑樹是為了解決二叉查找樹的缺陷,二叉查找樹在特殊情況下會變成一條線性結構(這就跟原來使用鏈表結構一樣了,造成很深的問題),遍歷查找會非常慢。而紅黑樹在插入新數據后可能需要通過左旋,右旋、變色這些操作來保持平衡,引入紅黑樹就是為了查找數據快,解決鏈表查詢深度的問題,我們知道紅黑樹屬於平衡二叉樹,但是為了保持“平衡”是需要付出代價的,但是該代價所損耗的資源要比遍歷線性鏈表要少,所以當長度大於8的時候,會使用紅黑樹,如果鏈表長度很短的話,根本不需要引入紅黑樹,引入反而會慢。”

12.說說你對紅黑樹的見解?

  • 1、每個節點非紅即黑
  • 2、根節點總是黑色的
  • 3、如果節點是紅色的,則它的子節點必須是黑色的(反之不一定)
  • 4、每個葉子節點都是黑色的空節點(NIL節點)
  • 5、從根節點到葉節點或空子節點的每條路徑,必須包含相同數目的黑色節點(即相同的黑色高度)

13.為什么HashMap集合在儲存數據的時候要使用哈希算法?

  • HaspMap中存儲的數據都是不可重復的並且是無序的,那么我們在存儲一個新的數據的時候就需要判斷這個數據原來是否存在,這個時候就需要通過java中的equals方法來判斷兩個對象的實例是否相等,如果相等,那么就是重復數據。但是,如果每增加一個元素就檢查一次,那么當元素比較多時,添加新元素的時候就需要用equals比較很多次,這個時候存儲的效率就非常低。
  • 例如:如果一個HashMap集合中已經有10000個元素,這個時候添加一個新元素的時候就需要用equals比較10000次,再添加一個元素,就需要用equals比較10001次,存儲的元素越多,效率就會越低。
  • 於是,Java便采用哈希算法來提高效率。當向集合中添加新的元素的時候,先將對象通過哈希算法(hashCode方法)計算得到哈希值(正整數),然后將哈希值和集合(數組)長度進行&運算,得到該對象在該數組存放位置的索引值。如果這個位置上沒有元素,就可以直接存儲在這個位置上,如果這個位置上已經有元素了,就調用它的equals方法與新元素進行比較,相同的話就覆蓋,不相同的話就發生碰撞,形成鏈表。

14.jdk8中對HashMap做了哪些改變?

  • 在java 1.8中,如果鏈表的長度超過了8,那么鏈表將轉換為紅黑樹。(桶的數量必須大於64,小於64的時候只會擴容)
  • 發生hash碰撞時,java 1.7 會在鏈表的頭部插入,而java 1.8會在鏈表的尾部插入
  • 在java 1.8中,Entry被Node替代(換了一個馬甲)。

15.存儲過程 

  • 在jdk7的基礎上,形成了鏈表之后,當我們查詢一個對象的時候,如果這個位置已經形成了鏈表,那么此時查詢的效率就比較低了,因為我們得遍歷這個鏈表,運氣不好的話,可能要查找的元素就是在鏈表的最后一個,那么此時我們就需要把整個鏈表都遍歷一遍,效率比較低。
  • 在jdk1.8之后,在數組+鏈表的基礎上,還多了一個紅黑樹。現在的結構就是數組+鏈表+紅黑樹,當碰撞的次數大於8並且總容量大於64的時候,鏈表就會變為紅黑樹結構,轉為紅黑樹之后,除了添加以外,其他的效率都比鏈表高,因為在添加的時候,鏈表是直接加到鏈表的末尾,而紅黑樹添加的時候,需要比較大小,然后再進行添加。轉為紅黑樹之后,集合進行擴容以后,不用重新對元素進行計算,只需要找每個元素的二倍,然后把元素放入位置就可以了,提高了效率。

 16.負載因子為什么會影響HashMap性能

  • 我們都知道有序數組存儲數據,對數據的索引效率都很高,但是插入和刪除就會有性能瓶頸(回憶ArrayList),鏈表存儲數據,要一次比較元素來檢索出數據,所以索引效率低,但是插入和刪除效率高(回憶LinkedList),兩者取長補短就產生了哈希散列這種存儲方式,也就是HashMap的存儲邏輯.而負載因子表示一個散列表的空間的使用程度,有這樣一個公式:initailCapacity*loadFactor=HashMap的容量。
  • 所以負載因子越大則散列表的裝填程度越高,也就是能容納更多的元素,元素多了,鏈表大了,所以此時索引效率就會降低。反之,負載因子越小則鏈表中的數據量就越稀疏,此時會對空間造成爛費,但是此時索引效率高。

17.為什么initailCapacity要設置成2的n次冪

我們可以看到在hashmap中要找到某個元素,需要根據key的hash值來求得對應數組中的位置。如何計算這個位置就是hash算法。前面說過hashmap的數據結構是數組和鏈表的結合,所以我們當然希望這個hashmap里面的元素位置盡量的分布均勻些,盡量使得每個位置上的元素數量只有一個,那么當我們用hash算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,而不用再去遍歷鏈表。 

  • 所以我們首先想到的就是把hashcode對數組長度取模運算,這樣一來,元素的分布相對來說是比較均勻的。但是,“模”運算的消耗還是比較大的,能不能找一種更快速,消耗更小的方式那?java中時這樣做的:
  • static int indexFor(int h, int length) {  
           return h & (length-1);  
       }  
  • 左邊兩組是數組長度為16(2的4次方),右邊兩組是數組長度為15。兩組的hashcode均為8和9,但是很明顯,當它們和1110“與”的時候,產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,8和9會被放到同一個鏈表上,那么查詢的時候就需要遍歷這個鏈表,得到8或者9,這樣就降低了查詢的效率。同時,我們也可以發現,當數組長度為15的時候,hashcode的值會與14(1110)進行“與”,那么最后一位永遠是0,而0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,數組可以使用的位置比數組長度小了很多,這意味着進一步增加了碰撞的幾率,減慢了查詢的效率!
  • 所以說,當數組長度為2的n次冪的時候,不同的key算得得index相同的幾率較小,那么數據在數組上分布就比較均勻,也就是說碰撞的幾率小,相對的,查詢的時候就不用

    遍歷某個位置上的鏈表,這樣查詢效率也就較高了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM