關於Map集合的負載因子、初始大小、紅黑樹、尾插法的初步探究


負載因子,數組長度在2的次方,當鏈表長度>=8時擴容成紅黑樹?

  • 負載因子

    當我們將負載因子不定為0.75的時候(兩種情況):

    1、 假如負載因子定為1(最大值),那么只有當元素填滿組長度的時候才會選擇去擴容,雖然負載因子定為1可以最大程度的提高空間的利用率,但是會增加hash碰撞,以此可能會增加鏈表長度,因此查詢效率會變得低下(因為鏈表查詢比較慢)。hash表默認數組長度為16,好的情況下就是16個空間剛好一個坑一個,但是大多情況下是沒有這么好的情況。

    結論:所以當加載因子比較大的時候:節省空間資源,耗費時間資源

    2、加入負載因子定為0.5(一個比較小的值),也就是說,直到到達數組空間的一半的時候就會去擴容。雖然說負載因子比較小可以最大可能的降低hash沖突,鏈表的長度也會越少,但是空間浪費會比較大。

    結論:所以當加載因子比較小的時候:節省時間資源,耗費空間資源

    但是我們設計程序的時候肯定是會在空間以及時間上做平衡,那么我們能就需要在時間復雜度和空間復雜度上做折中,選擇最合適的負載因子以保證最優化。所以就選擇了0.75這個值,Jdk那幫工程師一定是做了大量的測試,得出的這個值吧~

  • hash表的數組長度總在2的次方

    1:

    // WeakHashMap.java 源碼:
    /**
    * Returns index for hash code h.
    */
    private static int indexFor(int h, int length) {
        return h & (length-1);
    }
    

    擴容也是以2的次方進行擴容,是因為2的次方的數的二進制是10..0,在二的次方數進行減1操作之后,二進制都是11...1,那么和hashcode進行與操作時,數組中的每一個空間都可能被使用到。

    如果不是2的次方,比如數組長度為17,那么17的二進制是10001,在indexFor方法中,進行減1操作為16,16的二進制是10000,隨着進行與操作,很明顯,地址二進制數末尾為1的空間,不會得到使用,比如地址為10001,10011,11011這些地址空間永遠不會得到使用。因此就會造成大量的空間浪費。

    所以必須得是2的次方,可以合理使用數組空間。

    2:

    擴容臨界值 = 負載因子 * 數組長度
    

    負載因子是0.75即3/4,又因為數組長度為2的次方,那么相乘得到的擴容臨界值必定是整數,這樣更加方便獲得一個方便操作的擴容臨界值。

  • 當鏈表長度>=8時構建成紅黑樹

    利用泊松分布計算出當鏈表長度大於等於8時,幾率很小很小

    當put進來一個元素,通過hash算法,然后最后定位到同一個桶(鏈表)的概率會隨着鏈表的長度的增加而減少,當這個鏈表長度為8的時候,這個概率幾乎接近於0,所以我們才會將鏈表轉紅黑樹的臨界值定為8。

    tips:了解紅黑樹,請移步至Java數據結構與算法:紅黑樹 AVL樹.md


為什么jdk8,hashmap底層會用紅黑樹,而不使用AVL樹?

首先需要了解什么是紅黑樹,什么是AVL樹。請移步至Java數據結構與算法:紅黑樹 AVL樹.md

紅黑樹和AVL樹增刪改查的時間復雜度平均和最壞情況都是在O(lgN),包括但不超過。

紅黑樹性質:

  1. 節點不是黑色就是紅色
  2. 根節點必須為黑色
  3. 不能有兩個連續紅色節點
  4. 葉子節點是黑色
  5. 從根節點到葉子節點經過的黑節點數量相同

特點:最長路徑不會超過最短路徑的2倍。

AVL性質:

  1. 任何節點的兩個子樹的高度最大差別為1

在jdk8中hashmap的hash表桶中的鏈表長度大於8時,會將鏈表轉為紅黑樹。雖然紅黑樹與AVL樹的時間復雜度都為O(lgN),但是在調整樹上面花費的時間相差很大。因為AVL樹是平衡二叉樹,要求嚴苛,任何節點的兩個子樹的高度最大差別為1,因此每次插入一個數或者刪除一個數,最壞情況下,會使得AVL樹進行很多次調整,為了保證符合AVL樹的規則,調整時間花費較多。而紅黑樹,在時間復雜度上與AVL樹相持平,但是在調整樹上沒有AVL樹嚴苛,它允許局部很少的不完全平衡,但最長路徑不會超過最短路徑的2倍,這樣以來,最多只需要旋轉3次就可以使其達到平衡,調整時間花費較少。

最重要的一點,在JUC中有一個CurrentHashMap類,該類為線程同步的hashmap類,當高並發時,需要在意的是時間,由於AVL樹在調整樹上花費的時間相對較多,因此在調整樹的過程中,其他線程需要等待的時間就會增長,這樣導致效率降低,所以會選擇紅黑樹。

總結:在增加、刪除的時間復雜度相同的情況下,調整時間相對花費較少的是紅黑樹,因此選擇紅黑樹。


既然紅黑樹那么好,為什么不一來就使用紅黑樹?

因為經過泊松定律知道,一個在負載因子為0.75時,出現的hash沖突,在一個桶中的鏈表長度大於8的幾率是很少很少幾乎為0,如果一來就使用紅黑樹,由於增刪頻繁,從而會調整樹的結構,反而增加了負擔,浪費時間,而直接使用鏈表增刪反而比紅黑樹快很多,因此為了增加效率,而只是在長度大於8時使用紅黑樹。


hashmap在get和put的時候為什么使用尾插法,而摒棄了頭插法?

這是因為多線程並發操作下,可能形成環化。

比如線程T1將要添加一個B元素進來,此時線程T2正在resize,達到了擴容臨界值,所以需要重計算,在重計算中,線程T1的B元素插在了A元素的頭上:

由於線程T2重計算數組長度后,擴容之后,在舊數組中同一條Entry鏈上的元素,通過重新計算索引位置后,有可能被放到了新數組的不同位置上。所以,元素A可能會插到元素B頭上,形成了環狀,死循環:

為了解決這個問題,在jdk8之后就使用了尾插法!最終不會形成環化。

雖然尾插法解決了這個問題,為什么在高並發下還是不能使用hashmap呢?

因為在hashmap中,沒有鎖的化,高並發下一個線程put的值,另一個線程可能不能及時get到最新put的值。所以要使用currentHashMap,用的鎖+尾插法


練習:計算一個字符串每個字符出現的次數

public class Test{
    public static void mian(String[] args){
        forTest01();
    }
    
    public static void forTest01(){
        Scanner sc = new Scanner(System.in);
        String str = sc.nextLine();
        
        char[] charArray = str.toCharArray();
        HashMap<Character,Integer> hashMap = new HashMap<>();
        for(int i = 0; i < charArray.length(); i++){
            char c = charArray.get(i);
            if( hashMap.containsKey(c) ){
                Integer in = hashMap.get(c);
                ++in;
                hashMap.put(c,in);
            }else{
                hashMap.put(c,1);
            }
        }
        
        Set<Map.Entry<Character,Integer>> set = hashMap.entrySet();
        for(Map.Entry<Character,Integer> entry : set){
            System.out.println(entry.getKey()+"---"+entry.getValue());
        }
    }
    
    public static void forTest02(){
        Scanner sc = new Scanner(System.in);
        String str = sc.nextLine();
        HashMap<Character,Integer> hashMap = new HashMap<>();
        for(char c : str.toCharArray()){
            if( hashMap.containsKey(c) ){
                Integer in = hashMap.get(c);
                ++in;
                hashMap.put(c,in);
            }else{
                hashMap.put(c,1);
            }
        }
        
        for(Character key : hashMap.keySet()){
            Integer value = hashMap.get(key);
            System.out.println(key+"---"+value);
        }
    }
}


免責聲明!

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



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