java 8 HashMap源码分析《一》

红黑树部分(第一部分)

红黑树的性质:

  • 根结点是黑色的
  • 每个节点,不是黑色的就是红色的
  • 每个叶子结点(每个根结点为null或nil节点)都是黑色的
  • 每个红色节点的孩子节点为黑色节点
  • 每个红色节点,到其叶子节点的路径,经过相同的黑色节点

红黑树是一种二叉查找树,

红黑树的查找

  • 树的查找,是树插入、删除的核心;是否能够快速的查找到树,决定了红黑树插入和删除的效率
  • 红黑树的查找,参考二叉平衡树的查找;同时,hashmap中的红黑树按照hash值的大小进行插入


    java 8 HashMap源码分析《一》_第1张图片
    红黑树查找流程图.png
红黑树查找
         /**
         * Finds the node starting at root p with the given hash and key.
         * The kc argument caches comparableClassFor(key) upon first use
         * comparing keys.
         */
       //  h,被查找值的hash值,被查找的key,key的class
        final TreeNode find(int h, Object k, Class kc) {
            TreeNode p = this;
      //  这是一个do-while循环,root=p开始查找
            do {
                int ph, dir; K pk;
                TreeNode pl = p.left, pr = p.right, q;
                if ((ph = p.hash) > h)
//  如果ph的值大于h,则p=pl遍历左子树
                    p = pl;
                else if (ph < h)
//  如果ph的值小于h,则p=pl遍历右子树
                    p = pr;
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
//如果p.key== k|| 或者k.equals(pk)&&k!=null
                    return p;
                else if (pl == null)
//  如果左子树为空,直接遍历右子树
                    p = pr;
                else if (pr == null)
                    p = pl;
//注意代码运行到这里,hash值相同,且key值不相同;
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&
                         (dir = compareComparables(kc, k, pk)) != 0)
//kc comparableClassFor 如果属性 x implements (x),可以进行比较,是可以进行比较的类
                    p = (dir < 0) ? pl : pr;
//如果节点包装类,不能进行比较,则遍历查找右子树;直至遍历到链表尾部;
                else if ((q = pr.find(h, k, kc)) != null)
                    return q;
//否则查找左子树
                else
                    p = pl;
            } while (p != null);
            return null;
        }

上述精妙之处,也是看代码困惑的地方

  • comparableClassFor与compareComparables的使用;第一个方法是判断该entry中K对应的包装类,是否能够实现了class comparable接口(class C implements Comparable);第二个方法: compareComparables 比较两个k h的值,如果h比h小,则遍历左子树,否则右子树
  • (q = pr.find(h, k, kc)) != null 执行这句的前提是,p的key和当前节点key不相同;同时p的key的包装类不能比较大小;则需要将p左右的子树遍历一遍;先将右子树遍历完,如果找到停止,否则遍历左子树

红黑树的插入

红黑树插入的方法,类似于find的方法;

         /**
         * Tree version of putVal.
         */
        final TreeNode putTreeVal(HashMap map, Node[] tab,
                                       int h, K k, V v) {
            Class kc = null;
            boolean searched = false;
//找到treeNode的根结点parent=root();
            TreeNode root = (parent != null) ? root() : this;
            for (TreeNode p = root;;) {
                int dir, ph; K pk;
//dir是子树的遍历方向,h比p(游标)的hash值小,则dir为-1;
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
//找到了该节点,直接返回
                    return p;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
//如果包装类没有实现comparale接口;判断该子树有没有搜索过;如果没有,继续
                    if (!searched) {
                        TreeNode 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;
                    }
//如果实现过comparale 接口;
                    dir = tieBreakOrder(k, pk);
                }
                TreeNode xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
//找到了节点需要插入的节点;当p为空,找到p需要插入的位置
                    Node xpn = xp.next;
                    TreeNode x = map.newTreeNode(h, k, v, xpn);
//新建节点xpn,如果dir小于等于0,则插入p的左子树
                    if (dir <= 0)
                        xp.left = x;
                    else
//否则插入右子树
                        xp.right = x;
//先xp的next为x,x的parent以及prev为xp;
                    xp.next = x;
                    x.parent = x.prev = xp;
                    if (xpn != null)
                        ((TreeNode)xpn).prev = x;
  // 平衡红黑树并保证root是index处首节点
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
        }

红黑树的删除

/**
         * Removes the given node, that must be present before this call.
         * This is messier than typical red-black deletion code because we
         * cannot swap the contents of an interior node with a leaf
         * successor that is pinned by "next" pointers that are accessible
         * independently during traversal. So instead we swap the tree
         * linkages. If the current tree appears to have too few nodes,
         * the bin is converted back to a plain bin. (The test triggers
         * somewhere between 2 and 6 nodes, depending on tree structure).
         */
//大致意思,就是我们在删除红黑树节点的时候,需要考虑linkhashmap的结构(next,prev节点);同时需要考虑如果节点比较少的时候,进行tree与bin之间的转换
        final void removeTreeNode(HashMap map, Node[] tab,
                                  boolean movable) {
            int n;
            if (tab == null || (n = tab.length) == 0)
                return;
            int index = (n - 1) & hash;
//找到删除节点的所在的index;从根结点开始遍历,
            TreeNode first = (TreeNode)tab[index], root = first, rl;
//需要删除节点的 的 后继节点succ,前驱节点pred;
            TreeNode succ = (TreeNode)next, pred = prev;
//处理node作为linkhashmap时候,前驱与后继节点之间的关系;
            if (pred == null)
                tab[index] = first = succ;
            else
//  前驱的后继,等于该节点的后继;
                pred.next = succ;
            if (succ != null)
//  处理后继的前驱,为该节点的前驱;
                succ.prev = pred;
            if (first == null)
                return;
//处理完作为link的后继与前驱的关系;后面处理红黑树的关系;
            if (root.parent != null)
                root = root.root();
            if (root == null || root.right == null ||
                (rl = root.left) == null || rl.left == null) {
                tab[index] = first.untreeify(map);  // too small
//如果该节点节点太少,则转换成bin桶
                return;
            }
//如果节点满足红黑树的结构;继续遍历;replacement 标记需要移除的节点
            TreeNode p = this, pl = left, pr = right, replacement;
//如果左右子树都不为空的情况;p移除之后,为了不破坏红黑树的节点,需要找到替换节点;
//替换节点有两种可能性,即左子树的最大,或者右子树的最小;
            if (pl != null && pr != null) {
                TreeNode s = pr, sl;//p为右子树;
                while ((sl = s.left) != null) //先找到右子树的左子树,find successor
                    s = sl;
                boolean c = s.red; s.red = p.red; p.red = c; // swap colors p与s交换颜色;
                TreeNode sr = s.right;//sr为s的右节点
                TreeNode pp = p.parent;//pp为p的父节点
//注意p为移除节点,s为替换节点,s.value>p.value;
                if (s == pr) { // p was s's direct parent
//如果刚好p的右子树为s,直接将s与p节点进行交换(s为叶子节点,)
                    p.parent = s;
                    s.right = p;
                }
                else {
                    TreeNode sp = s.parent;
                    if ((p.parent = sp) != null) {
//s为sp的左孩子,则s父亲的左孩子改成p
                        if (s == sp.left)
                            sp.left = p;
                        else
//????为什么sp可能
                            sp.right = p;
                    }
//将p的右子树,设置成s的右子树,p右子树的父节点改成s
                    if ((s.right = pr) != null)
                        pr.parent = s;
                }
                p.left = null;
//s的右(左)孩子赋值给p的右孩子,s右孩子的父亲改成p;同理处理s的父亲以及p的父亲‘
                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;
//自此,p与s的节点交换完毕;
                if (sr != null)
                    replacement = sr;
                else
                    replacement = p;
            }
            else if (pl != null)
                replacement = pl;
            else if (pr != null)
                replacement = pr;
            else
                replacement = p;
//注意replacement对应的节点,有两种种情况是p != replacement
            if (replacement != p) {
                TreeNode 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 r = p.red ? root : balanceDeletion(root, replacement);

            if (replacement == p) {  // detach
                TreeNode pp = p.parent;
                p.parent = null;
                if (pp != null) {
                    if (p == pp.left)
                        pp.left = null;
                    else if (p == pp.right)
                        pp.right = null;
                }
            }
            if (movable)
                moveRootToFront(tab, r);
        }
java 8 HashMap源码分析《一》_第2张图片
红黑树节点替换.jpg

红黑树的旋转

旋转的条件

  • 即树的高度 必须满足 h<=log2(n+1) 否则需要旋转
  • 不符合上述红黑树性质的,都需要着色调整,或者旋转调整

左旋

java 8 HashMap源码分析《一》_第3张图片
image.png
 /* ------------------------------------------------------------ */
        // Red-black tree methods, all adapted from CLR
    //左旋和右旋都不会涉及到节点颜色的变化,除非遇到根结点,输入参数是 树的根结点
     //以及旋转的节点 P
        static  TreeNode rotateLeft(TreeNode root,
                                              TreeNode p) {
            TreeNode r, pp, rl;
            //p节点不为空,且将p的右孩子赋值给r ,
            if (p != null && (r = p.right) != null) {
          //特别重要一: 取出原来右孩子的左孩子作为新p的右孩子;并赋值给rl;
                if ((rl = p.right = r.left) != null)
                //将rl的parent设置为p节点,与上一个形成呼应  
                    rl.parent = p;
        // 特点二 : 转换父节点,将p的父节点,赋值给r的父节点;并赋值给pp
        //  如果pp节点为空,即r为root 节点,然后将新的根结点赋值给r,并且着色为黑色
                if ((pp = r.parent = p.parent) == null)
                    (root = r).red = false;
      //特点三: 如果p为父节点的左孩子,则pp的左孩子为新的节点r; 同理右孩子
                else if (pp.left == p)
                    pp.left = r;
                else
                    pp.right = r;
    //特点四: 新的节点r左孩子为p,并且p的父亲节点为新的节点r
                r.left = p;
                p.parent = r;
            }
            return root;
        }

同理 右旋;

红黑树的插入

  • 每个插入的节点,默认为红色的节点;
static  TreeNode balanceInsertion(TreeNode root,
                                                    TreeNode x) {
            //新加入的节点的着色为红色
            x.red = true;
            for (TreeNode xp, xpp, xppl, xppr;;) {
            //第一种情况 如果新加节点的父节点为空,即root为空,刚加入的节点为root,着色为黑色,返回x
                if ((xp = x.parent) == null) {
                    x.red = false;
                    return x;
                }
          // 第二种情况 如果xp(x的父亲节点)的节点为黑色,或者x的祖父节点为空节点,返回该树
                else if (!xp.red || (xpp = xp.parent) == null)
                    return root;
        //第三种情况 如果父亲节点为红色,且祖父节点不为空的情况;且xp在xpp的左子树下;
                if (xp == (xppl = xpp.left)) {
                    //如果x的叔叔为右节点,且节点不为空,且为红色的情况,即和父亲的颜色一致,因为叔叔和父亲节点
                  //都为红色,则直接进行调整着色;叔叔和父亲节点改成黑色,祖父着色改成红色,
                //同时,x赋值给祖父,继续向上遍历
                    if ((xppr = xpp.right) != null && xppr.red) {
                        xppr.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    else {
            //如果且xp在xpp的左子树下;但是x的叔叔节点为空的情况;
                        if (x == xp.right) {
          //x为xp的右子树,先左旋xp(即xp为上图二的4节点,进行左旋,左旋之后变成如图二的结构)
                            root = rotateLeft(root, x = xp);
        //重排xp,xpp,x之间的关系;从叶子节点开始依次为原来为xp-x-xpp改为x-xp-xpp;
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
        //注意此时的着色,x为红,xp为红,xpp为黑色;
                        if (xp != null) {
        //变化着色,xp修改为黑色;xpp为黑色,然后将整个左子树从xpp开始进行右旋,
                            xp.red = false;
                            if (xpp != null) {
                                xpp.red = true;
                                root = rotateRight(root, xpp);
                            }
                        }
                    }
                }
                else {
  //第三种情况 如果父亲节点为红色,且祖父节点不为空的情况;且xp在xpp的右子树的情况;
                    if (xppl != null && xppl.red) {
  //叔叔节点为红色,和父亲一个颜色,则调整颜色,将父亲和叔叔节点改成黑色,祖父节点为红色,
//继续向上遍历节点
                        xppl.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    else {
    //  叔叔节点为空的情况,
                        if (x == xp.left) {
    //x为xp的左节点,先将x进行右旋,同理重构x,xp,xpp之间 的关系
                            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);
                            }
                        }
                    }
                }
            }
        }

特别强调以下几点

  • 新加入的节点,默认着色为红色
  • 如果xp为黑色的话,则不需要做任何操作

hashmap红黑树源码的一些体会;

  • 红黑树是一种二叉查找树的特例,满足二叉查找树的基本性质;
  • swap节点,注意孩子赋予新的父亲,则父亲也拥有了新的左右孩子
  • balanceInsertion以及balanceDeletion,都是对红黑树局部优化(可认为仅涉及其三层结构,即爷爷的左右孩子->叔叔节点)
  • 同一个桶内,是 index=hash & length-1 相同,并不是hash值相同;
  • hashmap的红黑树,其实保留了linkhashmap的特性,即保留了next,prev节点,在删除以及增加红黑树节点的时候,注意linkhashmap,前驱和后继节点的处理;
  • 找到同一个值的要求是;ph = hash&(p.key == key || key !=null key.equals.p,key),这里有一个条件就是允许,key的值null,但是仅有一个null值的key;

参考地址:https://blog.csdn.net/troy_kfrozen/article/details/78906022
https://www.jianshu.com/p/eb79b0ceb62c

你可能感兴趣的:(java 8 HashMap源码分析《一》)