关于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);
        }
    }
}
posted @ 2020-10-06 14:39  张还行  阅读(698)  评论(0编辑  收藏  举报