HashMap源码学习:红黑树原理详解

简介: HashMap源码学习:红黑树原理详解

文章导航

HashMap源码学习:红黑树原理详解

HashMap源码学习:JDK1.8版本源码解析

目录

文章导航

前言

概述

红黑树的特性

变色平衡

右旋平衡

左旋平衡

正文

红黑树平衡方法:balanceInsertion

红黑树左旋方法:rotateLeft

红黑树右旋方法:rotateRight

红黑树添加方法:putTreeVal

红黑树查询方法:find

红黑树删除方法:removeTreeNode

总结

前言

JDK1.8后的HashMap引入了红黑树,在学习HashMap源码之前,了解了红黑树原理,及其如何通过代码进行实现后,在整体的看HashMap的源码就会简单很多。

概述

红黑树的特性

bb71615f73e5abba215e0c3d37e36936_0eadb4ec1df54bf098a351da1bc62396.png

  • 根节点必须是黑色节点。
  • 节点是红色或黑色。
  • 所有叶子都是黑色。(叶子是Null节点)
  • 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  • 从任一结点到其每个叶子的所有路径都包含相同数目的黑色节点。

展示叶子节点

2ab151894c97008040315592a185c56b_2397b112c3d44efdaa2be7f00f120a0c.png

在HashMap源码中红黑树也是需要满足以上条件的,当我们在插入时可能会不满足以上特性,这时候就需要进行位置的调整了,如变色、左旋、右旋等操作来保持红黑树的特性。HashMap中红黑树每次插入节点都是红色节点,如果是插入节点是黑色的,则不满足特性。

如:不满足特性5,不同路径的黑色节点数量不一致。

6f6d89fc9f4efc0372fcbf7af5998d1f_db50407a7c8e4d0c8635acb0a92872a3.png

变色平衡

4a4c47ad8ddcddb789b28fbaff1e264b_c3465178143c4ba7b77f584a12b3cc2d.png

以上结构不符合红黑树特性,插入节点为红色,且父亲节点也为红色,这时需要调整树形结构。我们可以知道在插入红色节点35之前,红黑树肯定是平衡的(不平衡则会进行平衡调节),由于插入节点的父亲是红色,可以判断其爷爷节点50肯定是黑色的,这时候我们判断叔叔节点70是红色的,如果父亲节点跟叔叔节点都是红色的,此时对父亲节点和叔叔节点进行变色调整,由黑色调整为红色,爷爷节点需要从黑色变为空色,但由于爷爷节点是根节点,必须为黑色,所以在变红色后,需要判断是否根节点,如果是根节点则变为黑色,此时红黑S树就平衡了。如果爷爷节点不是根节点,此时就还是红色,有可能爷爷的父亲节点存在红色的情况,所以我们需要将爷爷节点作为插入节点,重复进行平衡操作,直接平衡。

2cea5f9119f3815d7747561b93603a50_3efeee8c72ae454c804c4c1fd93ba0f0.png

右旋平衡

32aa1dfeb9bc9c63bf7796503665f40b_64da1cb44fa64e14951b2da9f088769a.png

插入红色节点34时,由于其父亲也是红色,不满足红黑树特性。这时候由于其叔叔节点为空,非红色节点,所以不能直接通过变色解决平衡问题。由于插入节点34在父节点35的左边,父亲节点35在爷爷节点40左右,这时候我们可以以爷爷节点40作为旋转点进行右旋。

b2e8019e007ea5bac99eadfe08cbb824_6aea1697ae064a41a2140e66cbc8e6eb.png

1.将父节点35的父亲设置为太爷爷节点50

2.判断爷爷节点40是太爷爷节点50的左边还是右边,如果是左边,则将太爷爷50的左边设置为父亲节点35,右边则设置右边为父亲节点35。

3.将父亲节点35的右边设置为爷爷节点

4.将爷爷节点40的父亲设置为父亲夜店35

5.将爷爷节点40的左边设置为父亲节点35的右边,图中由于父亲节点35的右边是一个NULL节点,所以爷爷节点40的左边也是一个NULL节点。

通过以上步骤,我们发现树形结构还是不平衡,根节点到每个叶子节点黑色数量不是一致的。所以我们还需要调整父亲节点35的颜色为黑色,爷爷节点40颜色为红色,这样达到平衡效果。

3ec97cf00e8200757ae23c43e2478450_05d2bac78d4947f2aebf967de2c2b4d0.png

左旋平衡

c1082f3800459d77d39eab41fd764f2b_11dd3d802b5b49d78240124490800656.png

上图中存在两个连续红色的子父节点,不满足红黑色特性,需要通过平衡处理。

  1. 1.判断其叔叔节点是否存在且为红色,如果不满足条件,则无法直接通过变色解决平衡问题。
  2. 2.判断子节点34在父节点35的左边还是右边,如果在右边,则以父亲节点35作为旋转节点进行左旋。


a9c1f34078a28f3532fa0fd25c8cea58_2ac532428b504b9bbd8d978b21e6fd7a.png

1.将插入节点38的父亲指向爷爷节点40。

2.将父亲节点35的父亲指向插入节点38。

3.将父亲节点35的右边指向插入节点38的左边节点,这里由于插入节点的左边节点为NULL,所以父亲节点35的右边节点也会为NULL。

4.判断父亲节点35位于爷爷节点的左边还是右边,并将对应的指向插入节点38。

由以上步骤,可以发现此时的树形结构跟我们右旋时所遇到的树形情况一样,这时候再将父亲节点35作为插入节点进行平衡,这里参考本文章的右旋平衡章节。

正文

通过红黑树的特性,我们了解其原理,现在看看HashMap中的平衡是如何进行代码实现的。

红黑树平衡方法:balanceInsertion

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                    TreeNode<K,V> x) {
            x.red = true;
            //X为插入节点,将其颜色设置为红色
            //xp为插入节点的父亲
            //xpp为插入节点的爷爷
            //xppl、xxpl为其叔叔节点
            for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
                //如果插入节点的父亲为null,证明是其跟节点,此时设置为黑色即可
                if ((xp = x.parent) == null) {
                    x.red = false;
                    return x;
                }
                //如果父亲节点为黑色或者爷爷节点为空,此时证明,父亲节点为根节点。根节点又是黑色,所以插入一个红色节点不会破坏平衡
                else if (!xp.red || (xpp = xp.parent) == null)
                    return root;
                if (xp == (xppl = xpp.left)) {
                  //如果父亲节点是爷爷节点的左边时
          //获取位于右边的叔叔节点,如果叔叔节点不为空且是红色节点。
                    if ((xppr = xpp.right) != null && xppr.red) {
                      //此时进行变色,将父亲跟叔叔节点变为黑色,爷爷节点变为黑色
                        xppr.red = false;
                        xp.red = false;
                        xpp.red = true;
                        //将爷爷节点设置为插入节点,因为爷爷节点变成了红色,可能会破坏平衡,所以需要重新走一边平衡流程
                        x = xpp;
                    }
                    else {
                      //进入这边条件,则证明其右边的叔叔节点为空
                      //判断插入节点是否是父节点的右边
                        if (x == xp.right) {
                          //这种情况跟本文“左旋平衡”中图示情况一致
                          //以父亲作为旋转节点进行左旋,左旋之后此时的树还不是平衡状态,此时还是存在两个连续的红色节点
                            root = rotateLeft(root, x = xp);
                           //重新定义父亲节点,并给爷爷节点赋值
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        if (xp != null) {
                          //将父亲节点设置为黑色
                            xp.red = false;
                            if (xpp != null) {
                                //将爷爷节点设置为红色,并进行右旋,此时红黑树就平衡了。
                                xpp.red = true;
                                root = rotateRight(root, xpp);
                            }
                        }
                    }
                }
                else {
                  //进入这里,证明父亲节点是爷爷节点的右边。这里的操作跟上面条件里面是操作是反过来的。
          //判断是否存在红色的叔叔节点
                    if (xppl != null && xppl.red) {
                      //将父亲节点、叔叔节点设置为黑色,将爷爷节点设置为红色,此时有可能会破坏平衡,需要将爷爷节点作为插入节点,继续走平衡流程
                        xppl.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    else {
                      //如果插入节点为父亲节点的左边
                        if (x == xp.left) {
                          //先进行右旋
                            root = rotateRight(root, x = xp);
                            //重新定义父亲节点,并给爷爷节点赋值
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        if (xp != null) {
                          //将父亲节点设置为黑色
                            xp.red = false;
                            if (xpp != null) {
                            //将爷爷节点设置为红色,以爷爷节点作为旋转节点进行左旋
                                xpp.red = true;
                                root = rotateLeft(root, xpp);
                            }
                        }
                    }
                }
            }
        }

rotateLeft方法,左旋见方法2详解

rotateRight方法,右旋见方法3详解

红黑树左旋方法:rotateLeft

a2b969dbd238f48714741faa5f0f7873_781ea393342146e18069ffaf059137c1.png

上图中的旋转节点为:节点35

        static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                              TreeNode<K,V> p) {
            TreeNode<K,V> r, pp, rl;
            //r为旋转节点的右边节点  简称:旋转右节点
            //pp为旋转节点的父亲节点  简称:旋转父节点
            //rl旋转右节点的左边节点  简称:旋右左节点
            //判断旋转节点和旋转右节点不为null,并对旋转右节点进行赋值
            if (p != null && (r = p.right) != null) {
              //判断旋右左节点不为空,并将旋转节点的右边设置为旋右的左节点,并对旋右左节点rl进行赋值
                if ((rl = p.right = r.left) != null)
                  //将旋右左节点的父亲设置为旋转节点
                    rl.parent = p;
                //旋转右节点的父亲设置为其爷爷节点,并判断其爷爷节点是否为空
                if ((pp = r.parent = p.parent) == null)
                  //为空则证明所处位置为根节点,将其设置为黑色
                    (root = r).red = false;
                //判断旋转节点是在其父亲节点左边还是右边
                else if (pp.left == p)
                  //将旋转节点父亲的左边设置为旋转右节点
                    pp.left = r;
                else
                 //将旋转节点父亲的右边设置为旋转右节点
                    pp.right = r;
                //将旋转右节点的左边设置为旋转节点
                r.left = p;
                //将旋转节点的父亲设置为旋转右节点
                p.parent = r;
            }
            return root;
        }

红黑树右旋方法:rotateRight

右旋跟左旋是对应反着来的。

9966c8a14a2d2d3bbb592b7743518b31_88e66de61eec4ae888e7580e0d4a7f8b.png

上图中的旋转节点为:节点40

static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                               TreeNode<K,V> p) {
            TreeNode<K,V> l, pp, lr;
            //l为旋转节点的右边节点  简称:旋转左节点
            //pp为旋转节点的父亲节点  简称:旋转父节点
            //lr旋转右节点的右边节点  简称:旋左右节点
        //判断旋转节点和旋转左节点不为null,并对旋转左节点进行赋值
            if (p != null && (l = p.left) != null) {
                //判断旋左左节点不为空,并将旋转节点的左边设置为旋左的右节点,并对旋左右节点lr进行赋值
                if ((lr = p.left = l.right) != null)
                  //将旋左右节点的父亲设置为旋转节点
                    lr.parent = p;
                //旋转左节点的父亲设置为其爷爷节点,并判断其爷爷节点是否为空
                if ((pp = l.parent = p.parent) == null)
                  //为空则证明所处位置为根节点,将其设置为黑色
                    (root = l).red = false;
                else if (pp.right == p)
                  //将旋转节点父亲的右边设置为旋转左节点
                    pp.right = l;
                else
                  //将旋转节点父亲的右边设置为旋转右节点
                    pp.left = l;
                //将旋转左节点的右边设置为旋转节点
                l.right = p;
                //将旋转节点的父亲设置为旋转左节点
                p.parent = l;
            }
            return root;
        }

红黑树添加方法:putTreeVal

上面讲解了红黑树的平衡操作,现在进入红黑树的添加节点方法中。

        final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                       int h, K k, V v) {
            Class<?> kc = null;
            boolean searched = false;
            //获取其根节点
            TreeNode<K,V> root = (parent != null) ? root() : this;
            for (TreeNode<K,V> p = root;;) {
              //p:根节点
              //dir:-1代表左边节点方向 1代表右边节点方向
              //hash:插入节点的key hash值
              //key:插入节点的key值
              //pk:当前所在节点的key值
                int dir, ph; K pk;
                //判断插入节点key的hash值与当前节点比较大小,确定走向
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                //判断当前节点的key是否相等,相等则查到了位置
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                //有可能存入的key实现了比较器,所以这里会尝试获取比较器来重新计算下一节点树的方向
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        //标记为已经查找过了
                        searched = true;
                        //尝试从根节点的左节点或右节点进行查询,查询到该节点时,则返回该节点
                        if (((ch = p.left) != null &&
                             (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.find(h, k, kc)) != null))
                            return q;
                    }
                    dir = tieBreakOrder(k, pk);
                }
                TreeNode<K,V> xp = p;
                //判断此时的方向是往哪边查找
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                  //获取当前节点的下个节点值
                    Node<K,V> xpn = xp.next;
                    //如果不存在该相同的key信息,则创建新的节点,
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                    //判断新插入节点放在左边还是右边
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    //当前节点的链表下个节点指向插入节点
                    xp.next = x;
                    //将插入节点的红黑树父节点,及其链表的上个节点指向当前节点。
                    x.parent = x.prev = xp;
                    //如果当前节点原先的链表下个节点不为空,则将其链表的上个节点指向新插入的节点。
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                    //将ROOT节点的位置放到数组的第一个位置中
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
        }

到最后这块的代码涉及了链表的赋值,有读者可能有疑问,明明是红黑树怎么又对链表进行操作了?

HashMap1.8引入了红黑树,当链表节点个人>=8个时,会转为红黑树。当节点个数<=6个时,会转为链表。所以我们在操作红黑树的插入操作时,需要记录节点的上个节点,及其下个节点的指向,以便后续转为链表。

40b3e37422efcd17809f69e759716159_02d556556ff745cd919a0e760c07434b.png

我们可以看到红黑树节点都有prev属性,而TreeNode继承了Node,所以也有了next属性。

红黑树查询方法:find

 final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
      //将红黑树所在的当前节点赋值给p
            TreeNode<K,V> p = this;
            do {
                int ph, dir; K pk;
                TreeNode<K,V> pl = p.left, pr = p.right, q;
                //比较当前节点的hash值大小,决定查找方向
                if ((ph = p.hash) > h)
                    p = pl;
                else if (ph < h)
                    p = pr;
                //能走到这里证明hash值是一样的,如果key值一样,则是查找到了
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                //能走到这里证明hash值是一样的(hash冲突情况),如果左边节点为null,则赋值为右边,从右边进行查询,否则从左边查询
                else if (pl == null)
                    p = pr;
                else if (pr == null)
                    p = pl;
                //走到这里,证明还没办法确定走向,所以尝试获取key是否实现Comparable,并重新计算出其方向
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&
                         (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                //递归,从右边节点进行查找,如果查不到则从左边进行查找
                else if ((q = pr.find(h, k, kc)) != null)
                    return q;
                else
                    p = pl;
            } while (p != null);
            //找不到返回null
            return null;
        }

红黑树删除方法:removeTreeNode

final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                                  boolean movable) {
            int n;
            if (tab == null || (n = tab.length) == 0)
                return;
            //计算出所在的数组下标
            int index = (n - 1) & hash;
            //获取当前下标中的第一个值,也就是跟节点
            TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
            //获取当前移除节点的下个节点,及其上个节点的指向值
            TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
            //移除节点上个节点为空,则证明其排在第一位,需要将数组下标中的第一个节点重新赋值
            if (pred == null)
                tab[index] = first = succ;
            else
              //将上个节点的next指向移除节点的下个节点
                pred.next = succ;
            //下个节点不为空,则将下个节点的prev 指向移除节点的prev节点。
            if (succ != null)
                succ.prev = pred;
            if (first == null)
                return;
            //如果第一个节点的父节点有值,则获取当前红黑树的根节点
            if (root.parent != null)
                root = root.root();
            //如果根节点不存在 ,节点树为0
            //movable默认为true&&根节点的右边为空,节点数小于3
            //根节点的左节点为空,节点数小于3
            //左节点的左节点为空时,节点数小于等于6
            if (root == null
                || (movable
                    && (root.right == null
                        || (rl = root.left) == null
                        || rl.left == null))) {
                //链表化,因为前面对链表节点完成了删除操作,故在这里完成之后直接返回,即可完成节点的删除
                tab[index] = first.untreeify(map);  // too small
                return;
            }
      //p为删除节点
      //pl为删除节点的左节点
      //pr为删除节点的右节点
      //replacement 为删除节点的替换节点
            TreeNode<K,V> p = this, pl = left, pr = right, replacement;
            if (pl != null && pr != null) {
                //这里的目的是获取删除节点的右边中最小值来替换当前被删除节点,这样才能保证树形特性。也可以查询删除节点的左边中的最大值节点来替换
                TreeNode<K,V> s = pr, sl;
                while ((sl = s.left) != null) // find successor
                    s = sl;
                //获取待替换节点的颜色C,将待替换节点颜色与删除节点保持一致,将删除节点颜色替换为待替换节点颜色。这里就是将待替换节点与删除节点进行替换工作
                boolean c = s.red; s.red = p.red; p.red = c; // swap colors
                //获取待替换节点的右节点
                TreeNode<K,V> sr = s.right;
                //获取删除节点的父节点
                TreeNode<K,V> pp = p.parent;
                //如果待替换节点的父节点为删除节点的右边
                if (s == pr) { // p was s's direct parent
                  //交换两个节点的位置,父节点变子节点,子节点变父节点
                    p.parent = s;
                    s.right = p;
                }
                else {
                  //这里整块逻辑就是将删除节点与待替换节点进行互换,但是删除节点的左边节点,其父亲节点还未改变
                  //获取待替换节点的父节点
                    TreeNode<K,V> sp = s.parent;
                    //将删除节点的父节点指向待替换节点的父节点,这里将删除节点的位置替换到待替换节点中
                    if ((p.parent = sp) != null) {
                        if (s == sp.left)
                            sp.left = p;
                        else
                            sp.right = p;
                    }
                    //将删除节点的右边节点,其父节点替换为待替换节点
                    if ((s.right = pr) != null)
                        pr.parent = s;
                }
                //将删除节点的左节点设置为空
                p.left = null;
                //将删除节点右边设置为待替换节点的右节点,并将替换节点的右节点,其父亲替换为删除节点
                if ((p.right = sr) != null)
                    sr.parent = p;
                //待替换节点的左节点设置为删除节点的左节点,并且将该左节点的父节点设置为待替换节点
                if ((s.left = pl) != null)
                    pl.parent = s;
                //待替换节点的父节点设置为删除节点的父节点,如果为空则设置待替换节点为根节点,否则将删除节点的父节点,其对应的左节点或右节点进行替换。
                if ((s.parent = pp) == null)
                    root = s;
                else if (p == pp.left)
                    pp.left = s;
                else
                    pp.right = s;
                //设置待移除的节点,这里主要判断删除节点替换位置后,是否是最后一个节点,如果是则后面将其删除,不是则移除节点设置为其右边的节点,后面再把移除的右边节点赋值给替换后的删除节点,达到删除的效果
                if (sr != null)
                    replacement = sr;
                else
                    replacement = p;
            }
            //走到这里证明删除节点的右边为Null,此时其左边节点作为替换节点,否则为将其右边节点作为替换节点
            else if (pl != null)
                replacement = pl;
            else if (pr != null)
                replacement = pr;
            else
                //到这里,证明删除节点没有子节点,所以替换节点设置为本身
                replacement = p;
            //由于替换节点是根删除节点相邻,所以将替换节点顶替其删除节点,这样删除节点就从树中被移除了,再将其属性都置为null
            if (replacement != p) {
                TreeNode<K,V> pp = replacement.parent = p.parent;
                if (pp == null)
                    root = replacement;
                else if (p == pp.left)
                    pp.left = replacement;
                else
                    pp.right = replacement;
                p.left = p.right = p.parent = null;
            }
         //如果删除节点是红色的,替换者肯定是黑色的,所以不需要进行平衡操作,否则需要进行平衡
            TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
        //到这里证明删除节点处于最后一个节点时
            if (replacement == p) {  // detach
              //将删除节点从树中移除
                TreeNode<K,V> pp = p.parent;
                p.parent = null;
                if (pp != null) {
                    if (p == pp.left)
                        pp.left = null;
                    else if (p == pp.right)
                        pp.right = null;
                }
            }
            //保持根节点处于 所在数组index位置中的第一个节点
            if (movable)
                moveRootToFront(tab, r);
        }

总结

为了保持红黑树的特性,在插入或者删除时,可能破坏其平衡结构,所以通过变色、左旋、右旋等方式来保持红黑树的平衡。


红黑树特性:


根节点必须是黑色节点。

结点是红色或黑色。

所有叶子都是黑色。(叶子是Null节点)

每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。



目录
相关文章
|
1月前
|
Java
让星星⭐月亮告诉你,HashMap中保证红黑树根节点一定是对应链表头节点moveRootToFront()方法源码解读
当红黑树的根节点不是其对应链表的头节点时,通过调整指针的方式将其移动至链表头部。具体步骤包括:从链表中移除根节点,更新根节点及其前后节点的指针,确保根节点成为新的头节点,并保持链表结构的完整性。此过程在Java的`HashMap$TreeNode.moveRootToFront()`方法中实现,确保了高效的数据访问与管理。
29 2
|
1月前
|
Java 索引
让星星⭐月亮告诉你,HashMap之往红黑树添加元素-putTreeVal方法源码解读
本文详细解析了Java `HashMap` 中 `putTreeVal` 方法的源码,该方法用于在红黑树中添加元素。当数组索引位置已存在红黑树类型的元素时,会调用此方法。具体步骤包括:从根节点开始遍历红黑树,找到合适位置插入新元素,调整节点指针,保持红黑树平衡,并确保根节点是链表头节点。通过源码解析,帮助读者深入理解 `HashMap` 的内部实现机制。
33 2
|
1月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
52 0
|
1月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
53 5
|
1月前
|
算法 索引
让星星⭐月亮告诉你,HashMap的resize()即扩容方法源码解读(已重新完善,如有不足之处,欢迎指正~)
`HashMap`的`resize()`方法主要用于数组扩容,包括初始化或加倍数组容量。该方法首先计算新的数组容量和扩容阈值,然后创建新数组。接着,旧数组中的数据根据`(e.hash & oldCap)`是否等于0被重新分配到新数组中,分为低位区和高位区两个链表,确保数据迁移时的正确性和高效性。
64 3
|
1月前
|
Java 索引
让星星⭐月亮告诉你,HashMap中红黑树TreeNode的split方法源码解读
本文详细解析了Java中`HashMap`的`TreeNode`类的`split`方法,该方法主要用于在`HashMap`扩容时将红黑树节点从旧数组迁移到新数组,并根据`(e.hash & oldCap)`的结果将节点分为低位和高位两个子树。低位子树如果元素数少于等于6,则进行去树化操作;若多于6且高位子树非空,则进行树化操作,确保数据结构的高效性。文中还介绍了`untreeify`和`replacementNode`方法,分别用于将红黑树节点转换为普通链表节点。
23 2
|
1月前
|
机器学习/深度学习 算法
让星星⭐月亮告诉你,HashMap之tableSizeFor(int cap)方法原理详解(分2的n次幂和非2的n次幂两种情况讨论)
`HashMap` 的 `tableSizeFor(int cap)` 方法用于计算一个大于或等于给定容量 `cap` 的最小的 2 的幂次方值。该方法通过一系列的无符号右移和按位或运算,逐步将二进制数的高位全部置为 1,最后加 1 得到所需的 2 的幂次方值。具体步骤包括: 1. 将 `cap` 减 1,确保已经是 2 的幂次方的值直接返回。 2. 通过多次无符号右移和按位或运算,将最高位 1 后面的所有位都置为 1。 3. 最终加 1,确保返回值为 2 的幂次方。 该方法保证了 `HashMap` 的数组容量始终是 2 的幂次方,从而优化了哈希表的性能。
33 1
|
1月前
|
存储 Java
HashMap之链表转红黑树(树化 )-treefyBin方法源码解读(所有涉及到的方法均有详细解读,欢迎指正)
本文详细解析了Java HashMap中链表转红黑树的机制,包括树化条件(链表长度达8且数组长度≥64)及转换流程,确保高效处理大量数据。
71 1
|
1月前
|
存储 缓存 Java
【用Java学习数据结构系列】HashMap与TreeMap的区别,以及Map与Set的关系
【用Java学习数据结构系列】HashMap与TreeMap的区别,以及Map与Set的关系
34 1
|
2月前
|
设计模式 安全 Java
HashMap底层原理:数据结构+put()流程+2的n次方+死循环+数据覆盖问题
假如有T1、T2两个线程同时对某链表扩容,他们都标记头结点和第二个结点,此时T2阻塞,T1执行完扩容后链表结点顺序反过来,此时T2恢复运行再进行翻转就会产生环形链表,即B.next=A;采用2的指数进行扩容,是为了利用位运算,提高扩容运算的效率。JDK8中,HashMap采用尾插法,扩容时链表节点位置不会翻转,解决了扩容死循环问题,但是性能差了一点,因为要遍历链表再查到尾部。例如15(即2^4-1)的二进制为1111,31的二进制为11111,63的二进制为111111,127的二进制为1111111。
HashMap底层原理:数据结构+put()流程+2的n次方+死循环+数据覆盖问题