乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      面經(jīng)手冊 · 第3篇《HashMap核心知識,擾動函數(shù)、負載因子、擴容鏈表拆分,深度學習》

       小傅哥 2021-12-13


      作者:小傅哥
      博客:https://

      沉淀、分享、成長,讓自己和他人都能有所收獲!😄

      一、前言

      得益于Doug Lea老爺子的操刀,讓HashMap成為使用和面試最頻繁的API,沒辦法設計的太優(yōu)秀了!

      HashMap 最早出現(xiàn)在 JDK 1.2中,底層基于散列算法實現(xiàn)。HashMap 允許 null 鍵和 null 值,在計算哈鍵的哈希值時,null 鍵哈希值為 0。HashMap 并不保證鍵值對的順序,這意味著在進行某些操作后,鍵值對的順序可能會發(fā)生變化。另外,需要注意的是,HashMap 是非線程安全類,在多線程環(huán)境下可能會存在問題。

      HashMap 最早在JDK 1.2中就出現(xiàn)了,底層是基于散列算法實現(xiàn),隨著幾代的優(yōu)化更新到目前為止它的源碼部分已經(jīng)比較復雜,涉及的知識點也非常多,在JDK 1.8中包括;1、散列表實現(xiàn)、2、擾動函數(shù)、3、初始化容量4、負載因子、5、擴容元素拆分、6、鏈表樹化7、紅黑樹、8、插入、9、查找、10、刪除11、遍歷、12、分段鎖等等,因涉及的知識點較多所以需要分開講解,本章節(jié)我們會先把目光放在前五項上,也就是關于數(shù)據(jù)結構的使用上。

      數(shù)據(jù)結構相關往往與數(shù)學離不開,學習過程中建議下載相應源碼進行實驗驗證,可能這個過程有點燒腦,但學會后不用死記硬背就可以理解這部分知識。

      二、資源下載

      本章節(jié)涉及的源碼和資源在工程,interview-04中,包括;

      1. 10萬單詞測試數(shù)據(jù),在doc文件夾
      2. 擾動函數(shù)excel展現(xiàn),在dock文件夾
      3. 測試源碼部分在interview-04工程中

      可以通過關注公眾號:bugstack蟲洞棧,回復下載進行獲取{回復下載后打開獲得的鏈接,找到編號ID:19}

      三、源碼分析

      1. 寫一個最簡單的HashMap

      學習HashMap前,最好的方式是先了解這是一種怎么樣的數(shù)據(jù)結構來存放數(shù)據(jù)。而HashMap經(jīng)過多個版本的迭代后,乍一看代碼還是很復雜的。就像你原來只穿個褲衩,現(xiàn)在還有秋褲和風衣。所以我們先來看看最根本的HashMap是什么樣,也就是只穿褲衩是什么效果,之后再去分析它的源碼。

      問題: 假設我們有一組7個字符串,需要存放到數(shù)組中,但要求在獲取每個元素的時候時間復雜度是O(1)。也就是說你不能通過循環(huán)遍歷的方式進行獲取,而是要定位到數(shù)組ID直接獲取相應的元素。

      方案: 如果說我們需要通過ID從數(shù)組中獲取元素,那么就需要把每個字符串都計算出一個在數(shù)組中的位置ID。字符串獲取ID你能想到什么方式? 一個字符串最直接的獲取跟數(shù)字相關的信息就是HashCode,可HashCode的取值范圍太大了[-2147483648, 2147483647],不可能直接使用。那么就需要使用HashCode與數(shù)組長度做與運算,得到一個可以在數(shù)組中出現(xiàn)的位置。如果說有兩個元素得到同樣的ID,那么這個數(shù)組ID下就存放兩個字符串。

      以上呢其實就是我們要把字符串散列到數(shù)組中的一個基本思路,接下來我們就把這個思路用代碼實現(xiàn)出來。

      1.1 代碼實現(xiàn)

      // 初始化一組字符串
      List<String> list = new ArrayList<>();
      list.add("jlkk");
      list.add("lopi");
      list.add("小傅哥");
      list.add("e4we");
      list.add("alpo");
      list.add("yhjk");
      list.add("plop");
      
      // 定義要存放的數(shù)組
      String[] tab = new String[8];
      
      // 循環(huán)存放
      for (String key : list) {
          int idx = key.hashCode() & (tab.length - 1);  // 計算索引位置
          System.out.println(String.format("key值=%s Idx=%d", key, idx));
          if (null == tab[idx]) {
              tab[idx] = key;
              continue;
          }
          tab[idx] = tab[idx] + "->" + key;
      }
      // 輸出測試結果
      System.out.println(JSON.toJSONString(tab));
      

      這段代碼整體看起來也是非常簡單,并沒有什么復雜度,主要包括以下內容;

      1. 初始化一組字符串集合,這里初始化了7個。
      2. 定義一個數(shù)組用于存放字符串,注意這里的長度是8,也就是2的倍數(shù)。這樣的數(shù)組長度才會出現(xiàn)一個 0111 除高位以外都是1的特征,也是為了散列。
      3. 接下來就是循環(huán)存放數(shù)據(jù),計算出每個字符串在數(shù)組中的位置。key.hashCode() & (tab.length - 1)。
      4. 在字符串存放到數(shù)組的過程,如果遇到相同的元素,進行連接操作模擬鏈表的過程。
      5. 最后輸出存放結果。

      測試結果

      key值=jlkk Idx=2
      key值=lopi Idx=4
      key值=小傅哥 Idx=7
      key值=e4we Idx=5
      key值=alpo Idx=2
      key值=yhjk Idx=0
      key值=plop Idx=5
      測試結果:["yhjk",null,"jlkk->alpo",null,"lopi","e4we->plop",null,"小傅哥"]
      
      • 在測試結果首先是計算出每個元素在數(shù)組的Idx,也有出現(xiàn)重復的位置。
      • 最后是測試結果的輸出,1、3、6,位置是空的,2、5,位置有兩個元素被鏈接起來e4we->plop。
      • 這就達到了我們一個最基本的要求,將串元素散列存放到數(shù)組中,最后通過字符串元素的索引ID進行獲取對應字符串。這樣是HashMap的一個最基本原理,有了這個基礎后面就會更容易理解HashMap的源碼實現(xiàn)。

      1.2 Hash散列示意圖

      如果上面的測試結果不能在你的頭腦中很好的建立出一個數(shù)據(jù)結構,那么可以看以下這張散列示意圖,方便理解;

       Hash散列示意圖

      • 這張圖就是上面代碼實現(xiàn)的全過程,將每一個字符串元素通過Hash計算索引位置,存放到數(shù)組中。
      • 黃色的索引ID是沒有元素存放、綠色的索引ID存放了一個元素、紅色的索引ID存放了兩個元素。

      1.3 這個簡單的HashMap有哪些問題

      以上我們實現(xiàn)了一個簡單的HashMap,或者說還算不上HashMap,只能算做一個散列數(shù)據(jù)存放的雛形。但這樣的一個數(shù)據(jù)結構放在實際使用中,會有哪些問題呢?

      1. 這里所有的元素存放都需要獲取一個索引位置,而如果元素的位置不夠散列碰撞嚴重,那么就失去了散列表存放的意義,沒有達到預期的性能。
      2. 在獲取索引ID的計算公式中,需要數(shù)組長度是2的倍數(shù),那么怎么進行初始化這個數(shù)組大小。
      3. 數(shù)組越小碰撞的越大,數(shù)組越大碰撞的越小,時間與空間如何取舍。
      4. 目前存放7個元素,已經(jīng)有兩個位置都存放了2個字符串,那么鏈表越來越長怎么優(yōu)化。
      5. 隨著元素的不斷添加,數(shù)組長度不足擴容時,怎么把原有的元素,拆分到新的位置上去。

      以上這些問題可以歸納為;擾動函數(shù)、初始化容量、負載因子擴容方法以及鏈表和紅黑樹轉換的使用等。接下來我們會逐個問題進行分析。

      2. 擾動函數(shù)

      在HashMap存放元素時候有這樣一段代碼來處理哈希值,這是java 8的散列值擾動函數(shù),用于優(yōu)化散列效果;

      static final int hash(Object key) {
          int h;
          return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
      }
      

      2.1 為什么使用擾動函數(shù)

      理論上來說字符串的hashCode是一個int類型值,那可以直接作為數(shù)組下標了,且不會出現(xiàn)碰撞。但是這個hashCode的取值范圍是[-2147483648, 2147483647],有將近40億的長度,誰也不能把數(shù)組初始化的這么大,內存也是放不下的。

      我們默認初始化的Map大小是16個長度 DEFAULT_INITIAL_CAPACITY = 1 << 4,所以獲取的Hash值并不能直接作為下標使用,需要與數(shù)組長度進行取模運算得到一個下標值,也就是我們上面做的散列列子。

      那么,hashMap源碼這里不只是直接獲取哈希值,還進行了一次擾動計算,(h = key.hashCode()) ^ (h >>> 16)。把哈希值右移16位,也就正好是自己長度的一半,之后與原哈希值做異或運算,這樣就混合了原哈希值中的高位和低位,增大了隨機性。計算方式如下圖;

       擾動函數(shù)

      • 說白了,使用擾動函數(shù)就是為了增加隨機性,讓數(shù)據(jù)元素更加均衡的散列,減少碰撞。

      2.2 實驗驗證擾動函數(shù)

      從上面的分析可以看出,擾動函數(shù)使用了哈希值的高半?yún)^(qū)和低半?yún)^(qū)做異或,混合原始哈希碼的高位和低位,以此來加大低位區(qū)的隨機性。

      但看不到實驗數(shù)據(jù)的話,這終究是一段理論,具體這段哈希值真的被增加了隨機性沒有,并不知道。所以這里我們要做一個實驗,這個實驗是這樣做;

      1. 選取10萬個單詞詞庫
      2. 定義128位長度的數(shù)組格子
      3. 分別計算在擾動和不擾動下,10萬單詞的下標分配到128個格子的數(shù)量
      4. 統(tǒng)計各個格子數(shù)量,生成波動曲線。如果擾動函數(shù)下的波動曲線相對更平穩(wěn),那么證明擾動函數(shù)有效果。
      2.2.1 擾動代碼測試

      擾動函數(shù)對比方法

      public class Disturb {
      
          public static int disturbHashIdx(String key, int size) {
              return (size - 1) & (key.hashCode() ^ (key.hashCode() >>> 16));
          }
      
          public static int hashIdx(String key, int size) {
              return (size - 1) & key.hashCode();
          }
      
      }
      
      • disturbHashIdx 擾動函數(shù)下,下標值計算
      • hashIdx 非擾動函數(shù)下,下標值計算

      單元測試

      // 10萬單詞已經(jīng)初始化到words中
      @Test
      public void test_disturb() {
          Map<Integer, Integer> map = new HashMap<>(16);
          for (String word : words) {
              // 使用擾動函數(shù)
              int idx = Disturb.disturbHashIdx(word, 128);
              // 不使用擾動函數(shù)
              // int idx = Disturb.hashIdx(word, 128);
              if (map.containsKey(idx)) {
                  Integer integer = map.get(idx);
                  map.put(idx, ++integer);
              } else {
                  map.put(idx, 1);
              }
          }
          System.out.println(map.values());
      }
      

      以上分別統(tǒng)計兩種函數(shù)下的下標值分配,最終將統(tǒng)計結果放到excel中生成圖表。

      2.2.2 擾動函數(shù)散列圖表

      以上的兩張圖,分別是沒有使用擾動函數(shù)和使用擾動函數(shù)的,下標分配。實驗數(shù)據(jù);

      1. 10萬個不重復的單詞
      2. 128個格子,相當于128長度的數(shù)組

      未使用擾動函數(shù)

       未使用擾動函數(shù)

      使用擾動函數(shù)

       使用擾動函數(shù)

      • 從這兩種的對比圖可以看出來,在使用了擾動函數(shù)后,數(shù)據(jù)分配的更加均勻了。
      • 數(shù)據(jù)分配均勻,也就是散列的效果更好,減少了hash的碰撞,讓數(shù)據(jù)存放和獲取的效率更佳。

      3. 初始化容量和負載因子

      接下來我們討論下一個問題,從我們模仿HashMap的例子中以及HashMap默認的初始化大小里,都可以知道,散列數(shù)組需要一個2的倍數(shù)的長度,因為只有2的倍數(shù)在減1的時候,才會出現(xiàn)01111這樣的值。

      那么這里就有一個問題,我們在初始化HashMap的時候,如果傳一個17個的值new HashMap<>(17);,它會怎么處理呢?

      3.1 尋找2的倍數(shù)最小值

      在HashMap的初始化中,有這樣一段方法;

      public HashMap(int initialCapacity, float loadFactor) {
          ...
          this.loadFactor = loadFactor;
          this.threshold = tableSizeFor(initialCapacity);
      }
      
      • 閥值threshold,通過方法tableSizeFor進行計算,是根據(jù)初始化來計算的。
      • 這個方法也就是要尋找比初始值大的,最小的那個2進制數(shù)值。比如傳了17,我應該找到的是32。

      計算閥值大小的方法;

      static final int tableSizeFor(int cap) {
          int n = cap - 1;
          n |= n >>> 1;
          n |= n >>> 2;
          n |= n >>> 4;
          n |= n >>> 8;
          n |= n >>> 16;
          return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
      }
      
      • MAXIMUM_CAPACITY = 1 << 30,這個是臨界范圍,也就是最大的Map集合。
      • 乍一看可能有點暈😵怎么都在向右移位1、2、4、8、16,這主要是為了把二進制的各個位置都填上1,當二進制的各個位置都是1以后,就是一個標準的2的倍數(shù)減1了,最后把結果加1再返回即可。

      那這里我們把17這樣一個初始化計算閥值的過程,用圖展示出來,方便理解;

       計算閥值

      3.2 負載因子

      static final float DEFAULT_LOAD_FACTOR = 0.75f;
      

      負載因子是做什么的?

      負載因子,可以理解成一輛車可承重重量超過某個閥值時,把貨放到新的車上。

      那么在HashMap中,負載因子決定了數(shù)據(jù)量多少了以后進行擴容。這里要提到上面做的HashMap例子,我們準備了7個元素,但是最后還有3個位置空余,2個位置存放了2個元素。 所以可能即使你數(shù)據(jù)比數(shù)組容量大時也是不一定能正正好好的把數(shù)組占滿的,而是在某些小標位置出現(xiàn)了大量的碰撞,只能在同一個位置用鏈表存放,那么這樣就失去了Map數(shù)組的性能。

      所以,要選擇一個合理的大小下進行擴容,默認值0.75就是說當閥值容量占了3/4s時趕緊擴容,減少Hash碰撞。

      同時0.75是一個默認構造值,在創(chuàng)建HashMap也可以調整,比如你希望用更多的空間換取時間,可以把負載因子調的更小一些,減少碰撞。

      4. 擴容元素拆分

      為什么擴容,因為數(shù)組長度不足了。那擴容最直接的問題,就是需要把元素拆分到新的數(shù)組中。拆分元素的過程中,原jdk1.7中會需要重新計算哈希值,但是到jdk1.8中已經(jīng)進行優(yōu)化,不在需要重新計算,提升了拆分的性能,設計的還是非常巧妙的。

      4.1 測試數(shù)據(jù)

      @Test
      public void test_hashMap() {
          List<String> list = new ArrayList<>();
          list.add("jlkk");
          list.add("lopi");
          list.add("jmdw");
          list.add("e4we");
          list.add("io98");
          list.add("nmhg");
          list.add("vfg6");
          list.add("gfrt");
          list.add("alpo");
          list.add("vfbh");
          list.add("bnhj");
          list.add("zuio");
          list.add("iu8e");
          list.add("yhjk");
          list.add("plop");
          list.add("dd0p");
          for (String key : list) {
              int hash = key.hashCode() ^ (key.hashCode() >>> 16);
              System.out.println("字符串:" + key + " \tIdx(16):" + ((16 - 1) & hash) + " \tBit值:" + Integer.toBinaryString(hash) + " - " + Integer.toBinaryString(hash & 16) + " \t\tIdx(32):" + ((
              System.out.println(Integer.toBinaryString(key.hashCode()) +" "+ Integer.toBinaryString(hash) + " " + Integer.toBinaryString((32 - 1) & hash));
          }
      }
      

      測試結果

      字符串:jlkk Idx(16)3 Bit值:1100011101001000010011 - 10000 Idx(32)19
      1100011101001000100010 1100011101001000010011 10011
      字符串:lopi Idx(16)14 Bit值:1100101100011010001110 - 0 Idx(32)14
      1100101100011010111100 1100101100011010001110 1110
      字符串:jmdw Idx(16)7 Bit值:1100011101010100100111 - 0 Idx(32)7
      1100011101010100010110 1100011101010100100111 111
      字符串:e4we Idx(16)3 Bit值:1011101011101101010011 - 10000 Idx(32)19
      1011101011101101111101 1011101011101101010011 10011
      字符串:io98 Idx(16)4 Bit值:1100010110001011110100 - 10000 Idx(32)20
      1100010110001011000101 1100010110001011110100 10100
      字符串:nmhg Idx(16)13 Bit值:1100111010011011001101 - 0 Idx(32)13
      1100111010011011111110 1100111010011011001101 1101
      字符串:vfg6 Idx(16)8 Bit值:1101110010111101101000 - 0 Idx(32)8
      1101110010111101011111 1101110010111101101000 1000
      字符串:gfrt Idx(16)1 Bit值:1100000101111101010001 - 10000 Idx(32)17
      1100000101111101100001 1100000101111101010001 10001
      字符串:alpo Idx(16)7 Bit值:1011011011101101000111 - 0 Idx(32)7
      1011011011101101101010 1011011011101101000111 111
      字符串:vfbh Idx(16)1 Bit值:1101110010111011000001 - 0 Idx(32)1
      1101110010111011110110 1101110010111011000001 1
      字符串:bnhj Idx(16)0 Bit值:1011100011011001100000 - 0 Idx(32)0
      1011100011011001001110 1011100011011001100000 0
      字符串:zuio Idx(16)8 Bit值:1110010011100110011000 - 10000 Idx(32)24
      1110010011100110100001 1110010011100110011000 11000
      字符串:iu8e Idx(16)8 Bit值:1100010111100101101000 - 0 Idx(32)8
      1100010111100101011001 1100010111100101101000 1000
      字符串:yhjk Idx(16)8 Bit值:1110001001010010101000 - 0 Idx(32)8
      1110001001010010010000 1110001001010010101000 1000
      字符串:plop Idx(16)9 Bit值:1101001000110011101001 - 0 Idx(32)9
      1101001000110011011101 1101001000110011101001 1001
      字符串:dd0p Idx(16)14 Bit值:1011101111001011101110 - 0 Idx(32)14
      1011101111001011000000 1011101111001011101110 1110
      
      • 這里我們隨機使用一些字符串計算他們分別在16位長度和32位長度數(shù)組下的索引分配情況,看哪些數(shù)據(jù)被重新路由到了新的地址。
      • 同時,這里還可以觀察🕵出一個非常重要的信息,原哈希值與擴容新增出來的長度16,進行&運算,如果值等于0,則下標位置不變。如果不為0,那么新的位置則是原來位置上加16。{這個地方需要好好理解下,并看實驗數(shù)據(jù)}
      • 這樣一來,就不需要在重新計算每一個數(shù)組中元素的哈希值了。

      4.2 數(shù)據(jù)遷移

       數(shù)據(jù)遷移

      • 這張圖就是原16位長度數(shù)組元素,像32位數(shù)組長度中轉移的過程。
      • 其中黃色區(qū)域元素zuio因計算結果 hash & oldCap 為1,則被遷移到下標位置24。
      • 同時還是用重新計算哈希值的方式驗證了,確實分配到24的位置,因為這是在二進制計算中補1的過程,所以可以通過上面簡化的方式確定哈希值的位置。

      四、總結

      • 如果你能堅持看完這部分內容,并按照文中的例子進行相應的實驗驗證,那么一定可以學會本章節(jié)涉及這五項知識點;1、散列表實現(xiàn)、2、擾動函數(shù)、3、初始化容量、4、負載因子5、擴容元素拆分。
      • 對我個人來說以前也知道這部分知識,但是沒有驗證過,只知道概念如此,正好借著寫面試手冊專欄,加深學習,用數(shù)據(jù)驗證理論,讓知識點可以更加深入的理解。
      • 這一章節(jié)完事,下一章節(jié)繼續(xù)進行HashMap的其他知識點挖掘,讓懂了就是真的懂了。好了,寫到這里了,感謝大家的閱讀。如果某處沒有描述清楚,或者有不理解的點,歡迎與我討論交流。

      五、推薦閱讀

      • HashCode為什么使用31作為乘數(shù)?
      • 面經(jīng)手冊 · 開篇《面試官都問我啥》
      • 工作兩年簡歷寫成這樣,誰要你呀!
      • 講道理,只要你是一個愛折騰的程序員,畢業(yè)找工作真的不需要再花錢培訓!
      • 大學四年到畢業(yè)工作5年的學習路線資源匯總
      • 源碼分析 | Mybatis接口沒有實現(xiàn)類為什么可以執(zhí)行增刪改查

        轉藏 分享 獻花(0

        0條評論

        發(fā)表

        請遵守用戶 評論公約

        類似文章 更多