zoukankan      html  css  js  c++  java
  • 关于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);
            }
        }
    }
    
  • 相关阅读:
    MongoDB的查询
    商品订购及货物采购信息系统(代码分析)
    Java连接数据库(mysql,sqlserver)
    开通博客第一天
    Ubuntu下java环境的搭建
    商品订购及货物采购信息系统(需求分析)
    GitHub客户端发布托管代码
    property中copy和strong修饰符的使用指北
    iOS界面间传值
    GPUImage的滤镜功能一览表
  • 原文地址:https://www.cnblogs.com/turbo30/p/13773381.html
Copyright © 2011-2022 走看看