HashMap分析之红黑树树化过程

你真的知道HashMap扩容条件和红黑树升级退化的条件吗? HashMap扩容条件 红黑树升级条件 红黑树退化的条件 缘由  写这篇文章真的不是为了水博客,作者本人正在写java数据结构相关的文章,也在积极找工作和准备面试,看到好多面试视频里对于 HashMap 扩容和红黑树升级退化的条件说的含糊其辞,于是做了一个汇总。Talk is cheap, I will show you the code. HashMap 扩容条件 当 HashMap 中键值对 key-value 个数大于阀值的时候(注意不是什么桶或数组的占用情况) 升级成红黑树时,数组长度小.. 阅读详情

HashMap分析之红黑树树化过程

概述

HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型。随着JDK(Java Developmet Kit)版本的更新,JDK1.8对HashMap底层的实现进行了优化,例如引入红黑树的数据结构和扩容的优化等。本文主要分析一下HashMap中红黑树树化的过程。

红黑树(red black tree)

  • 一个节点标记为红色或者黑色。
  • 根是黑色的。
  • 如果一个节点是红色的,那么它的子节点必须是黑色的(这就是为什么叫红黑树)。
  • 一个节点到到一个null引用的每一条路径必须包含相同数目的黑色节点(所以红色节点不影响)。

其实RB Tree和著名的AVL Tree有很多相同的地方,困难的地方都在于将一个新项插入到树中。了解AVL Tree的朋友应该都知道为了维持树的高度必须在插入一个新的项后必须在树的结构上进行改变,这里主要是通过旋转,当然在RB Tree中原理也是如此。

两种旋转和一种典型的变换

:旋转的方向。

:变换过程。

:互相关联。

:单向关联。

:代表红色的节点。

:代表黑色的节点。

和:代表一个不会破坏红黑树结构的部分,可能是节点,或者是一个子树,总之不会破环当前树的结构。这个部分会由于旋转而连接到其他的节点后面,我们可以理解成由于重力原因它掉到了下面的节点上。

  • 单旋转变换。
  • 双旋转变换(需要两次反方向的单旋转)。
  • 当遇到两个子几点都为红色的话执行颜色变换。

上面的图中描述了红黑树中三种典型的变换,其实前两种变换这正是AVL Tree中的两种典型的变换。

几个问题

为什么要进行旋转?

由于P和X节点都为红色节点这破环了红节点下面的节点必须为黑色节点的规则。

 

新加入的节点总是红色的,这是为什么呢?

因为被插入前的树结构是构建好的,一但我们进行添加黑色的节点,无论添加在哪里都会破坏原有路径上的黑色节点的数量平等关系,所以插入红色节点是正确的选择。

 

为什么要进行颜色变换?

正如第一种旋转新加入的节点X破坏了红黑树的结构不得不进行旋转,后面的就是旋转后的结果,旋转后形成新的结构,此时我们发现两个子节点都是红色的所以执行第三个变换特性,颜色变换,因为如果子节点是红色的那么我们在添加的时候只能添加黑色的节点,然而添加任何黑色叶子节点都会破坏树的第四条性质,所以要对其进行变换。当进行变换后叶子节点是红色的而且我们默认添加的叶子节点是红色的,所以添加到黑色节点后并不会破坏树的第四条结构,所以这种变换很有用。

 

第二种双变换中在树的内部怎么出现的红色的节点?

正是由于上面的颜色变换导致新颜色变换后的节点与他的父节点产生了颜色冲突。

 

与AVL树相比?

比AVL树相比优点是不用在节点类中保存一个节点高度这个变量,节省了内存。

而且红黑树一般不是以递归方式实现的而是以循环的形式实现。

一般的操作在最坏情形下花费O(logN)时间。

 

好了有了这些基本的概念让我们去看一下HashMap中的代码的实现


这里我们不仔细研究HashMap的其他机制如果对其他细节感兴趣或者不懂的话可以参考这篇文章:

http://yikun.github.io/2015/04/01/Java-HashMap工作原理及实现/

 

所有添加的操作最终都由这个方法完成。

复制代码

   final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
    {
        Node<K, V>[] tab;
        Node<K, V> p;
        int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else
        {
            Node<K, V> e;
            K k;
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                // 如果当前的bucket里面已经是红黑树的话,执行红黑树的添加操作
                e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
            else
            {
                for (int binCount = 0;; ++binCount)
                {
                    if ((e = p.next) == null)
                    {
                        p.next = newNode(hash, key, value, null);
                        // TREEIFY_THRESHOLD = 8,判断如果当前bucket的位置链表长度大于8的话就将此链表变成红黑树。
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null)
            { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

复制代码

上面的方法通过hash计算插入的项的槽位,如果有是一样的key则根据设置的参数是否执行覆盖,如果相应槽位空的话直接插入,如果对应的槽位有项则判断是红黑树结构还是链表结构的槽位,链表的话则顺着链表寻找如果找到一样的key则根据参数选择覆盖,没有找到则链接在链表最后面,链表项的数目大于8则对其进行树化,如果是红黑树结构则按照树的添加方式添加项。

 

让我们看一下treeifyBin这个方法。

复制代码

   final void treeifyBin(Node<K, V>[] tab, int hash)
    {
        int n, index;
        Node<K, V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            // resize()方法这里不过多介绍,感兴趣的可以去看上面的链接。
            resize();
        // 通过hash求出bucket的位置。
        else if ((e = tab[index = (n - 1) & hash]) != null)
        {
            TreeNode<K, V> hd = null, tl = null;
            do
            {
                // 将每个节点包装成TreeNode。
                TreeNode<K, V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else
                {
                    // 将所有TreeNode连接在一起此时只是链表结构。
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                // 对TreeNode链表进行树化。
                hd.treeify(tab);
        }
    }

复制代码

找个方法所做的事情就是将刚才九个项以链表的方式连接在一起,然后通过它构建红黑树。

 

看代码之前我们先了解一下TreeNode

复制代码

   static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V>
    {
        TreeNode<K, V> parent; // red-black tree links
        TreeNode<K, V> left;
        TreeNode<K, V> right;
        TreeNode<K, V> prev; // needed to unlink next upon deletion
        boolean red;

        TreeNode(int hash, K key, V val, Node<K, V> next)
        {
            super(hash, key, val, next);
        }
        
        final void treeify(Node<K,V>[] tab)
        {
            // ......
        }
        
        static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x)
        {
            // ......
        }
        
        static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p)
        {
            // ......
        }
        
        static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p)
        {
            // ......
        }
        
        // ......其余方法省略
    }

复制代码

可以看出出真正的维护红黑树结构的方法并没有在HashMap中,全部都在TreeNode类内部。

 

我们看一下treeify代码

复制代码

   final void treeify(Node<K, V>[] tab)
    {
        TreeNode<K, V> root = null;
        // 以for循环的方式遍历刚才我们创建的链表。
        for (TreeNode<K, V> x = this, next; x != null; x = next)
        {
            // next向前推进。
            next = (TreeNode<K, V>) x.next;
            x.left = x.right = null;
            // 为树根节点赋值。
            if (root == null)
            {
                x.parent = null;
                x.red = false;
                root = x;
            } else
            {
                // x即为当前访问链表中的项。
                K k = x.key;
                int h = x.hash;
                Class<?> kc = null;
                // 此时红黑树已经有了根节点,上面获取了当前加入红黑树的项的key和hash值进入核心循环。
                // 这里从root开始,是以一个自顶向下的方式遍历添加。
                // for循环没有控制条件,由代码内break跳出循环。
                for (TreeNode<K, V> p = root;;)
                {
                    // dir:directory,比较添加项与当前树中访问节点的hash值判断加入项的路径,-1为左子树,+1为右子树。
                    // ph:parent hash。
                    int dir, ph;
                    K pk = p.key;
                    if ((ph = p.hash) > h)
                        dir = -1;
                    else if (ph < h)
                        dir = 1;
                    else if ((kc == null && (kc = comparableClassFor(k)) == null)
                            || (dir = compareComparables(kc, k, pk)) == 0)
                        dir = tieBreakOrder(k, pk);

                    // xp:x parent。
                    TreeNode<K, V> xp = p;
                    // 找到符合x添加条件的节点。
                    if ((p = (dir <= 0) ? p.left : p.right) == null)
                    {
                        x.parent = xp;
                        // 如果xp的hash值大于x的hash值,将x添加在xp的左边。
                        if (dir <= 0)
                            xp.left = x;
                        // 反之添加在xp的右边。
                        else
                            xp.right = x;
                        // 维护添加后红黑树的红黑结构。
                        root = balanceInsertion(root, x);
                        
                        // 跳出循环当前链表中的项成功的添加到了红黑树中。
                        break;
                    }
                }
            }
        }
        // Ensures that the given root is the first node of its bin,自己翻译一下。
        moveRootToFront(tab, root);
    }

复制代码

第一次循环会将链表中的首节点作为红黑树的根,而后的循环会将链表中的的项通过比较hash值然后连接到相应树节点的左边或者右边,插入可能会破坏树的结构所以接着执行balanceInsertion。

 

我们看balanceInsertion

复制代码

   static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root, TreeNode<K, V> x)
    {
        // 正如开头所说,新加入树节点默认都是红色的,不会破坏树的结构。
        x.red = true;
        // 这些变量名不是作者随便定义的都是有意义的。
        // xp:x parent,代表x的父节点。
        // xpp:x parent parent,代表x的祖父节点
        // xppl:x parent parent left,代表x的祖父的左节点。
        // xppr:x parent parent right,代表x的祖父的右节点。
        for (TreeNode<K, V> xp, xpp, xppl, xppr;;)
        {
            // 如果x的父节点为null说明只有一个节点,该节点为根节点,根节点为黑色,red = false。
            if ((xp = x.parent) == null)
            {
                x.red = false;
                return x;
            } 
            // 进入else说明不是根节点。
            // 如果父节点是黑色,那么大吉大利(今晚吃鸡),红色的x节点可以直接添加到黑色节点后面,返回根就行了不需要任何多余的操作。
            // 如果父节点是红色的,但祖父节点为空的话也可以直接返回根此时父节点就是根节点,因为根必须是黑色的,添加在后面没有任何问题。
            else if (!xp.red || (xpp = xp.parent) == null)
                return root;
            
            // 一旦我们进入到这里就说明了两件是情
            // 1.x的父节点xp是红色的,这样就遇到两个红色节点相连的问题,所以必须经过旋转变换。
            // 2.x的祖父节点xpp不为空。
            
            // 判断如果父节点是否是祖父节点的左节点
            if (xp == (xppl = xpp.left))
            {
                // 父节点xp是祖父的左节点xppr
                // 判断祖父节点的右节点不为空并且是否是红色的
                // 此时xpp的左右节点都是红的,所以直接进行上面所说的第三种变换,将两个子节点变成黑色,将xpp变成红色,然后将红色节点x顺利的添加到了xp的后面。
                // 这里大家有疑问为什么将x = xpp?
                // 这是由于将xpp变成红色以后可能与xpp的父节点发生两个相连红色节点的冲突,这就又构成了第二种旋转变换,所以必须从底向上的进行变换,直到根。
                // 所以令x = xpp,然后进行下下一层循环,接着往上走。
                if ((xppr = xpp.right) != null && xppr.red)
                {
                    xppr.red = false;
                    xp.red = false;
                    xpp.red = true;
                    x = xpp;
                }
                // 进入到这个else里面说明。
                // 父节点xp是祖父的左节点xppr。
                // 祖父节点xpp的右节点xppr是黑色节点或者为空,默认规定空节点也是黑色的。
                // 下面要判断x是xp的左节点还是右节点。
                else
                {
                    // x是xp的右节点,此时的结构是:xpp左->xp右->x。这明显是第二中变换需要进行两次旋转,这里先进行一次旋转。
                    // 下面是第一次旋转。
                    if (x == xp.right)
                    {
                        root = rotateLeft(root, x = xp);
                        xpp = (xp = x.parent) == null ? null : xp.parent;
                    }
                    // 针对本身就是xpp左->xp左->x的结构或者由于上面的旋转造成的这种结构进行一次旋转。
                    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);
                        }
                    }
                }
            }
        }
    }

复制代码

如果您的联想能力很强的话估计到这里应该已经理解这集中变换的主要的关系。

下面简述一下前面的两种种幸运的情况

  1. x本身为根节点返回x。
  2. x的父节点为黑色或者x的父节点是根节点直接返回不需要变换。

如若上述两个条件不满足的话,就要进行变换了,允许我再贴一点代码......没有代码分析起来很困难。

颜色变换

复制代码

            if (xp == (xppl = xpp.left))
            {
                if ((xppr = xpp.right) != null && xppr.red)
                {
                    xppr.red = false;
                    xp.red = false;
                    xpp.red = true;
                    x = xpp;
                }
          // ......
        }

复制代码

这里是一个典型的一个黑色节点的两个子节点都是红色的所以要进行颜色变换。

变换后x可以直接插入在xp的后面。

两个核心旋转(左旋转和右旋转)

复制代码

       // 一旦我们进入到这里就说明了两件是情
            // 1.x的父节点xp是红色的,这样就遇到两个红色节点相连的问题,所以必须经过旋转变换。
            // 2.x的祖父节点xpp不为空。
            
            // 判断如果父节点是否是祖父节点的左节点
            if (xp == (xppl = xpp.left))
            {
                if ((xppr = xpp.right) != null && xppr.red)
                {
            // ......
                }

                // 进入到这个else里面说明。
                // 父节点xp是祖父的左节点xppr。
                // 祖父节点xpp的右节点xppr是黑色节点或者为空,默认规定空节点也是黑色的。
                // 下面要判断x是xp的左节点还是右节点。
                else
                {
                    // x是xp的右节点,此时的结构是:xpp左->xp右->x。这明显是第二中变换需要进行两次旋转,这里先进行一次旋转。
                    // 下面是第一次旋转。
                    if (x == xp.right)
                    {
                        root = rotateLeft(root, x = xp);
                        xpp = (xp = x.parent) == null ? null : xp.parent;
                    }
                    // 针对本身就是xpp左->xp左->x的结构或者由于上面的旋转造成的这种结构进行一次旋转。
                    if (xp != null)
                    {
                        xp.red = false;
                        if (xpp != null)
                        {
                            xpp.red = true;
                            root = rotateRight(root, xpp);
                        }
                    }
                }
            } 

复制代码

颜色变换完成后进入下面的else块

我们已知xp是xpp的左节点,首先判断了x是xp的左节点还是右节点,如果是右节点的话构成了下面的结构。

这中结构需要进行双旋转,首先先进行一次向左旋转。

左旋转

复制代码

 1     static <K, V> TreeNode<K, V> rotateLeft(TreeNode<K, V> root, TreeNode<K, V> p)
 2     {
 3         // r:right,右节点。
 4         // pp:parent parent,父节点的父节点。
 5         // rl:right left,右节点的左节点。
 6         TreeNode<K, V> r, pp, rl;
 7         if (p != null && (r = p.right) != null)
 8         {
 9             if ((rl = p.right = r.left) != null)
10                 rl.parent = p;
11             if ((pp = r.parent = p.parent) == null)
12                 (root = r).red = false;
13             else if (pp.left == p)
14                 pp.left = r;
15             else
16                 pp.right = r;
17             r.left = p;
18             p.parent = r;
19         }
20         return root;
21     }

复制代码

1.刚进入方法时,状态如下图。(RL可能是空的)

2.进入了if块后执行到第10行后。

 9             if ((rl = p.right = r.left) != null)
10                 rl.parent = p;

此时如果9行的条件符合的话执行10行RL指向P,如果RL为null的话,P的右节点指向null。

3.接着看11和12行代码。

11             if ((pp = r.parent = p.parent) == null)
12                 (root = r).red = false;

首先我们看11行if里面的赋值语句所造成的影响。

在if里面的表达式不管符不符合条件()内的内容都会执行。

如果符合条件的话会执行12行的代码,变成了下面的结果。

由于PP为空所以只剩下这三个。

4.如果11行的条件为假的话,执行完11行()内的表达式后执行13行

13             else if (pp.left == p)
14                 pp.left = r;

 

满足条件的话R和PP互相关联。

5.由于进入了13和14行所以不进入15和16行的else语句。

15             else
16                 pp.right = r;

6.看17和18行。

17             r.left = p;
18             p.parent = r;

最终执行完了一个旋转变成了我们开始说的第一种旋转的形式,这个结构还需要向右旋转一次。

                    if (x == xp.right)
                    {
                        root = rotateLeft(root, x = xp);
                        xpp = (xp = x.parent) == null ? null : xp.parent;
                    }
xpp = (xp = x.parent) == null ? null : xp.parent;

执行完上面的代码,旋转后调整x,xp,和xpp的关系得到下图。

右旋转

复制代码

                    if (xp != null)
                    {
                        xp.red = false;
                        if (xpp != null)
                        {
                            xpp.red = true;
                            root = rotateLeft(root, xpp);
                        }
                    }                            

复制代码

1.首先让XP变成黑色。

 

2.如果XPP不为空的话变成红色。

由于我们在rotateLeft(root, xpp),传进来的是XXP所以下面的的旋转中实际上就是对XP和XXP执行了一次与上面的方向相反其他完全相同的旋转。

接着我们看向右旋转的代码

复制代码

 1     static <K, V> TreeNode<K, V> rotateRight(TreeNode<K, V> root, TreeNode<K, V> p)
 2     {
 3         // l:left,左节点。
 4         // pp:parent parent,父节点的父节点。
 5         // lr:left right,左节点的右节点。
 6         TreeNode<K, V> l, pp, lr;
 7         if (p != null && (l = p.left) != null)
 8         {
 9             if ((lr = p.left = l.right) != null)
10                 lr.parent = p;
11             if ((pp = l.parent = p.parent) == null)
12                 (root = l).red = false;
13             else if (pp.right == p)
14                 pp.right = l;
15             else
16                 pp.left = l;
17             l.right = p;
18             p.parent = l;
19         }
20         return root;
21     }

复制代码

3.刚进来的时候结构是这个样子。

在这里的P就是刚才传进来的XPP。

4.这里我们认为LR是存在的,其实这个不影响主要的旋转,为空就指向null呗,直接执行完9和10行。

 9             if ((lr = p.left = l.right) != null)
10                 lr.parent = p;

5.在这里我们假使PP是存在的,直接执行完11的表达式不再执行12行。(不再分析不存在的情况)。

11             if ((pp = l.parent = p.parent) == null)
12                 (root = l).red = false;

6.由于11行的条件不符合,现在直接执行13行的表达式,不符合执行15行else,执行16行。

15             else
16                 pp.left = l;

7.最后执行层17和18行。

17             l.right = p;
18             p.parent = l;

 

最终完成两次的旋转。


 

疑问?

大家可能觉得和刚才接不上其实是这样的,刚才在右旋转前的时候的图像是这个样的。

因为我们传进来的是XPP,所以结合上一次的向左旋转我们在向右旋转的时候看到全图应该是这个样子的。(注:XPPP可能是XPP的左父节点也可能是右父节点这里不影响,而且可以是任意颜色)

现在知道为什么XPPP可以是任意颜色的了吧,因为旋转过后X是黑色的即便XPPP是红色,此时我们又可以对两个红色的子节点进行颜色变换了,变换后X和XPPP有发生了颜色冲突,接着进行旋转直到根。

复制代码

   static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root, TreeNode<K, V> x)
    {
        x.red = true;
        for (TreeNode<K, V> xp, xpp, xppl, xppr;;)
        {
            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))
            {
               // 插入位置父节点在祖父节点的左边。
            }
            else
            {
         // 插入位置父节点在祖父节点的右边。
            }
        }
    }

复制代码

我们值分析了插入位置父节点在祖父节点的左边的情况,并没有分析另外一面的对称情况,其实是一样的因为调用的都是相同的方法。

 

以上就是在1.8中的HashMap新引进的红黑树树化的过程,与原来的链表相比当同一个bucket上存储很多entry的话树形的查找结构明显要比链表线性的的效率要高。

 

转载请注明出处。

 

好文要顶 关注我 收藏该文  

雪皓梅丹
关注 - 0
粉丝 - 0

+加关注

2

0

« 上一篇:Tomcat与SpringMVC结合分析(一)
» 下一篇:Java 8手动实现一个Collector

posted on 2018-01-09 15:16 雪皓梅丹 阅读(5638) 评论(2) 编辑 收藏


评论:#1楼 2018-08-07 22:07 | 这个名有人取了  

太强了,写得非常详细,学习了

支持(0)反对(0)

  #2楼 2019-03-13 10:25 | 简蛙  

有个问题请教一下,关于第二种情况,要进行左旋操作的时候为什么执行x=xp操作?

HashMap中红黑树退化成链表的条件(源码分析) 条件 扩容 resize( ) 时,红黑树拆分成的 树的结点数小于等于临界值6个,则退化成链表。 移除元素 remove( ) 时,在removeTreeNode( ) 方法会检查红黑树是否满足退化条件,与结点数无关。 扩容 resize( ) 的源码分析 扩容时如果是红黑树结构会执行红黑树的 split( ) 方法 split 方法中会初始化生成 loHead 和 hiHead 两个红黑树的头结点(之后会用 low 和 high 表示) lc 和 hc 分别为 low 和 high 的元素个数,初 阅读详情

相关推荐

HashMap红黑树树化过程,看这一篇就够了

HashMap红黑树树化过程,看这一篇就够了红黑树基础概念红黑树性质红黑树示意图红黑树数据结构JDK1.8 HashMap源码分析putVal()treeifyBin()treeNode()treeify()moveRootToFront()rotateLeft()rotateRight()balanceInsertion()左旋、右旋过程详细解析 上一篇文章大厂面试爱问的HashMap死锁问题,看这一篇就够了,我们分析了JDK1.7中Hashmap的部分源码实现,最后说到在JDK1.8的时候HashMap

wildyuhao的博客 1293

hashmap链表转化成红黑树的过程以及红黑树转化成链表的过程

hashmap链表转化成红黑树的过程

weixin_45660035的博客 3116

一文带你速通HashMap底层核心数据结构红黑树

每个节点或者红色,或者是黑色。根节点为黑色。每一个叶子节点都是黑色。如果一个节点是红色,那么他的孩子节点都是黑色。从任意一个节点,经过的黑色节点是一样的。在《算法4》一书中认为红黑树和2-3树是等价的。

禅与计算机程序设计的艺术 2456

HashSet(HashMap)的扩容机制及红黑树简略梳理

2.在table表的数据量已经等于负载值(当前容量*负载系数),则会将table表扩容至当前容量的2倍,负载系数负载值也变为原负载值的两倍,如:初始容量为16,负载系数为0.75,那么负载值就是16*0.75 = 12,当添加完第12个元素后,就会立马进行扩容,容量扩容为32,负载值也变成24,以此类推;下次添加数据,如果又添加到了这条链表,就会将table表扩容至64,再次添加数据,如果又添加到了这条链表后面,或者其他链表使某条链表达到8个元素,就会将这条链表转成红黑树。

beixi1234的博客 1008

VM与欧姆龙PLC通讯设置:Fins TCP/UDP协议实战避坑指南

本文详细解析了VM视觉平台与欧姆龙PLC通过Fins TCP/UDP协议进行通讯的实战配置与避坑要点。文章对比了Fins TCP与Fins UDP的差异,推荐在多数工业控制场景下优先选用Fins TCP以保证可靠性,并深入讲解了网络设置、寄存器地址映射、轮询优化等核心配置步骤。同时,针对常见的多次触发拍照、数据解析错误等问题提供了具体的解决方案和调试技巧,旨在帮助工程师快速建立稳定高效的通讯连接。

e4f5g6h7的博客 596

1688店铺所有商品API接口(整店商品查询API接口)

1688店铺所有商品API接口、1688店铺列表API接口,1688店铺API接口,1688店铺查询商品API接口,1688店铺商品API接口,1688店铺详情接口,1688商品API接口

tbprice的博客 1569

强化基础,HashMap源码全角度详细解析

HashMap源码深度解析 一、重新认识HashMap 什么是HashMap? HashMap底层基于散列(Hash)算法,采用hash表实现键值对集合,继承了AbstractMap,实现了Map接口。最早出现在jdk1.2,允许null键和null值,null键的哈希值为0。需要注意的是HashMap不保证键值对顺序,同时非线程安全。 长啥样? 散列算法分为散列再探测和拉链式,HashM...

静待花开 5555

用Python手把手教你实现一个多层感知机(MLP)——从理论到代码实战

本文详细介绍了如何使用Python和Theano库从零开始实现多层感知机(MLP),涵盖从核心原理到代码实战的全过程。通过构建隐藏层、输出层和完整MLP模型,结合权重初始化、激活函数选择和训练技巧,帮助开发者深入理解神经网络工作机制,并应用于实际机器学习任务。

web39explorer的博客 970

HashMap 源码分析

基础入门 1、数组的优势和劣势 优势:数组的内存地址是连续的,每个元素内存大小相同,并且有索引,查找的速度非常快 劣势:在数组中间新增或者删除元素,需要移动大量元素,效率低 应用场景:增删操作少,查询多的场景 2、链表的优势和劣势 优势:链表的内存不是连续的,每个元素通过指针相连,增加或者删除的时候只要改变元素中的指针即可,效率高 劣势:链表查询需要从头开始遍历比较慢,时间是0(N),链表越长,查询越慢,除非刚好查的是第一个。 3、散列表有什么特点 散列表,又称hash表,是根据key值而进行直接访

诗和远方 259

告别调试烦恼:用Lattice Diamond手把手教你JTAG烧录FPGA(附SRAM/Flash模式详解)

本文详细介绍了使用Lattice Diamond软件进行JTAG烧录FPGA的全流程,包括SRAM临时调试和Flash固化的核心区别。从环境准备、硬件连接到文件准备、烧录模式选择,再到典型问题排查和高级技巧,手把手教你快速掌握FPGA烧录技术,提升开发效率。

weixin_42569329的博客 496

ABAP Bits and Bytes 完全参考,理解内存、字节、位运算与类型解释

ABAP开发中业务代码与底层字节处理的差异:业务层关注采购订单、会计凭证等语义信息,而接口开发需处理十六进制、位运算等底层操作。文章通过整数变量示例说明同一段字节在不同类型解释下会呈现不同值,并介绍x类型、位运算及类型转换的实际应用。这些知识在文件接口、加密传输等场景中尤为重要,开发者需注意字节序、编码等问题。建议将底层协议处理与业务逻辑分离,以保持代码清晰和可维护性。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 142

HashMap 源码详细分析(JDK1.8)

转载自:HashMap 源码详细分析(JDK1.8) 概述 本篇文章我们来聊聊大家日常开发中常用的一个集合类 - HashMap。HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现。HashMap 允许 null 键和 null 值,在计算哈键的哈希值时,null 键哈希值为 0。HashMap 并不保证键值对的顺序,这意味着在进行某些操作后,键值对的顺序可能会发生变化。另外,需要...

weixin_34060741的博客 108

嵌入式视频应用开发者必读:嵌入式Linux+FFmpeg硬件编解码实战指南

在嵌入式Linux上,FFmpeg支持多种硬件加速API,比如VAAPI、VDPAU、CUDA,以及针对特定平台的专用接口(如NVIDIA的NVENC、Intel的QSV、ARM的Mali)。FFmpeg的硬件解码通过-hwaccel参数启用,咱们以常见的H.264和HEVC(H.265)格式为例,展示如何用VAAPI和V4L2实现高效解码。: FFmpeg硬件加速需要额外的库,比如libva(VAAPI)、libv4l2(V4L2)或libnvcodec(CUDA)。

大模型大数据攻城狮的专栏 2204

遍历删除_面经手册 第4篇HashMap数据插入、查找、删除、遍历,源码分析

持续坚持原创输出,点击蓝字关注我吧作者:小傅哥博客:https://bugstack.cn❝沉淀、分享、成长,让自己和他人都能有所收获!?❞目录一、前言二、HashMap源码分析1. 插入2. 查找3. 删除4. 遍历三、总结一、前言在上一章节我们讲解并用数据验证了,HashMap中的,散列表的实现、扰动函数、负载因子以及扩容拆分等核心知识点以及相应的作用。除了以上这些知识点外,Has...

weixin_39967670的博客 129

华为OD机试 C++【TLV解析】

这里的valueOffset指的是value在整串消息里的起始位置(从0开始数)。这表示tag是15(十六进制的0F),length是4,接着的8个字符是value的内容。你收到了一串由两端设备传递的TLV格式的消息,现在你需要根据这串消息生成一个对应的(tag, length, valueOffset)列表。tag是15,对应的数据长度为4,value从第3个字节开始,所以偏移量为2。第二行:一个数字n,表示我们给出的tag的数量(0 < n < 1000)。第一行:一串消息,长度不超过10000。

wtswts1232的博客 482

OCR技术解析:用Tesseract和PaddleOCR识别文本

OCR技术解析:用Tesseract和PaddleOCR识别文本 ,人工智能,计算机视觉,大模型,AI,光学字符识别(Optical Character Recognition,简称 OCR)是一项将图片、扫描件等文档中的文字信息转换为可编辑文本的技术。在数字化时代,OCR 技术广泛应用于文档处理、车牌识别、身份证识别、古籍数字化等领域,极大地提高了文字信息处理的效率。Tesseract 和 PaddleOCR 是两款极具代表性的 OCR 工具。

不踩坑,不知道坑有多深;不总结,踩过的坑白踩。 3万+

HashMap 源码分析(下)

HashMap 这种散列表的数据结构,最大的性能在于可以 O(1)时间复杂度定位到元素,但因为哈希碰撞不得已在一个下标里存放多组数据,那么 jdk1.8 之前的设计只是采用链表的方式进行存放,如果需要从链表中定位到数据时间复杂度就是O(n),链表越长性能越差。HashMap 是基于数组+链表和红黑树实现的,但用于存放 key 值得的数组桶的长度是固定的,由初始化决定。我们讲解并用数据验证了,HashMap 中的,散列表的实现、扰动函数、负载因子以及扩容拆分等核心知识点以及相应的作用。

优快云_LiMingfly的博客 774

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

HashMap源码详细解析

HashMap源码详细解析1、重新认识HashMap1.1、什么是HashMap?1.2、长啥样?1.3、HashMap特点2、 知其所以然----撸源码2.1、 从类注释开始,能够掌握整个HashMap大致框架2.2、初始化2.1.1、构造方法解析2.2.2、负载因子、阈值深度解析2.3、新增2.3.1、新增源码分析:2.3.2、扩容2.3.3、链表树化2.3.4、红黑树拆分、链化2.4、删除2.5、查找2.6、遍历 1、重新认识HashMap 1.1、什么是HashMap? HashMap底层基于散列(

qq_34896730的博客 650

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

容器-HashMap源码解析

概述 HashMap底层基于散列算法实现。HashMap允许null键和null值,在计算哈希键的哈希值拾,null键的哈希值为0。HashMap并不保证键值对的顺序,说明在进行某些操作后,键值对的顺序可能会发生改变。需要注意的是,HashMap是非线程安全类,在多线程环境下可能存在问题。 HashMap特点: HashMap是基于哈希表的Map接口实现。 HashMap底层采用的是...

努力找工作呀 312

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

13-Java集合框架之HashMap详解

1.HashMap是什么? 概念: 基于哈希表(jdk1.8加入了红黑树)的 Map 接口的实现。HashMap 最早出现在 JDK 1.2中,底层基于散列算法实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希...

VirtiL 553

【Java容器源码】HashMap(二)添加元素及树化源码分析

HashMap 系列: 【Java容器源码】HashMap(一)底层结构分析 【Java容器源码】HashMap(二)添加元素及树化源码分析 【Java容器源码】HashMap(三)扩容源码分析 【Java容器源码】HashMap(四)获取 value 及迭代器源码分析 【Java容器源码】HashMap(五)源码分析&死锁问题(JDK7) 【Java容器源码】关于HashMap的几个问题 1.向 Hash 表中添加元素 put() public V put(K ke...

二狗家有矿 229

HashMap源码分析

摘要:HashMap通过扰动函数优化哈希分布,将key的哈希值高16位与低16位异或,增加随机性以减少碰撞。扩容时采用2的幂次方容量,通过位运算计算新容量。当链表长度超过8且桶容量大于64时转为红黑树,提升查询效率。插入、查找和删除操作均通过哈希定位到桶,再根据节点类型(链表/红黑树)进行相应处理。扩容时元素重新散列,通过哈希值与旧容量按位与运算判断新位置,优化迁移效率。

weiye2012的博客 888
上一篇: ReenTrantLock可重入锁(和synchronized的区别)总结
下一篇: 【Java-集合】HashMap-Hash冲突解决
javaPie
博客等级 码龄16年 1171粉丝 43原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值