手撕HashMap红黑树

红黑树 可以看作二叉搜索树平衡二叉树AVL 树)的一个折中。


二叉搜索树 、平衡二叉树


二叉搜索树:一棵空树,或者是具有下列性质的 二叉树

  1. 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

  2. 若右子树不空,则右子树上所有结点的值均大于它的根结点的值;

  3. 左、右子树也分别为二叉排序树;

  4. 没有键值相等的结点。

AVL 树本质上还是一棵二叉搜索树,它的特点是:

  1. 本身首先是一棵二叉搜索树。

  2. 带有平衡条件:每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。也就是说,AVL树,本质上是带了平衡功能的二叉查找树(二叉排序树,二叉搜索树)


红黑树


红黑树是一种半平衡的二叉搜索树,它放弃了二叉搜索树的绝对平衡,换来了较为简单的可维护性,使得二叉搜索树插入新数据,以及搜索数据时,都具有不错的搜索性能。

之所以说红黑树是一种半平衡的二叉搜索树,是因为红黑树中所有叶子节点的深度相差不会超过一倍。

红黑树的特性:

  1. 每个结点是黑色或者红色。
  2. 根结点是黑色。
  3. 每个叶子结点( NIL 或 NULL )是黑色。 [注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点!]
  4. 如果一个结点是红色的,则它的子结点必须是黑色的。
  5. 每个结点到叶子结点 NIL 或 NULL 所经过的黑色结点的个数一样的

只要二叉搜索树符合以上 5 条性质,它就是红黑树。事实上,提出这 5 条性质的目的就是为了获得红黑树的“所有叶子节点的深度相差不会超过一倍”这个特性。之所以这么费尽心思的维护一个红黑树,是因为实践证明红黑树的这些规则遵循起来是相对简单的。

关于红黑树的具体插入和删除分析,可以参考文末的几篇博客,后续不再具体分析。


插入


关于红黑树的插入情景如下图(摘自 30张图带你彻底理解红黑树):

红黑树插入情景图

那么结合 HashMap 源码分析,HashMap 中关于红黑树的操作,都放在了 TreeNode 类中。

在上文分析 HashMap 中,留下了关于红黑树的一部分,其中 put 方法的调用中涉及到的 TreeNodeputTreeVal(插入) 和 treeify(调整链表为红黑树) 方法。

先分析 treeify ,在分析具体源码之前,我们可以确认现在箱中的节点已经为树节点(但仍然为链表结构),且调用 treeify 方法的对象为链表头节点。

        final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            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 {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = root;;) {
                        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);

                        TreeNode<K,V> xp = p;
                        // 找到树上的可插入点
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            // 平衡插入
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);
        }

上面执行的目的是将链表转换为红黑树。第一个循环是为了遍历链表,第二个循环是为了找到已构建的红黑树中当前节点的插入位置,并且需要在 balanceInsertion 方法中处理平衡红黑树操作(涉及到上图中的几种情形):

        static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                    TreeNode<K,V> x) {
            // 插入节点为红色节点
            x.red = true;
            /*
            * xp :x 的父节点
            * xpp :x 的祖父节点
            * xppl : x 的祖父节点的左子节点
            * xppr:x 的祖父节点的右子节点
            */
            for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
                // 情景1:空树
                if ((xp = x.parent) == null) {
                    x.red = false;
                    return x;
                }
                // 情景3:插入节点的父节点为黑节点
                else if (!xp.red || (xpp = xp.parent) == null)
                    return root;
                // 情景4:插入节点的父节点为红色节点
                // 且父亲节点是祖父节点的左子节点
                if (xp == (xppl = xpp.left)) {
                    // 情景4.1:叔叔节点存在,且为红色,做平衡调整
                    // 父节点和叔叔节点设置为黑色,祖父节点设置为红色,并继续做插入操作自平衡处理,直到					   // 平衡为止(自下向上)
                    if ((xppr = xpp.right) != null && xppr.red) {
                        xppr.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    // 情景4.2:叔叔节点不存在,或者为黑节点
                    else {
                        // 情景4.2.2:插入节点是其父节点的右子节点
                        if (x == xp.right) {
                            // 左旋,重新调整
                            root = rotateLeft(root, x = xp);
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        // 情景4.2.3:插入节点是其父节点的左子节点
                        if (xp != null) {
                            xp.red = false;
                            if (xpp != null) {
                                xpp.red = true;
                                root = rotateRight(root, xpp);
                            }
                        }
                    }
                }
                // 父亲节点是祖父节点的右子节点
                else {
                    // 情景4.1:叔叔节点存在并且为红节点
                    if (xppl != null && xppl.red) {
                        xppl.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    else {
                        // 情景4.3:叔叔节点不存在,或者为黑节点
                        // 情景4.3.2:插入节点是其父节点的左子节点
                        if (x == xp.left) {
                            root = rotateRight(root, x = xp);
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        // 情景4.3.1:插入节点是其父节点的右子节点
                        if (xp != null) {
                            xp.red = false;
                            if (xpp != null) {
                                xpp.red = true;
                                root = rotateLeft(root, xpp);
                            }
                        }
                    }
                }
            }
        }

结合红黑树的插入情景来分析源码就会发现容易多了。

关于构建完红黑树后调用的 moveRootToFront(tab, root) 方法,它的作用正如它声明的那样确保 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;;) {
                int dir, ph; K pk;
                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) {
                    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;
                    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;
                    // 做插入平衡
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
        }

整体的流程走完了,但其中还涉及比较多的细节,可自行品味。


删除


关于红黑树的删除情景如下图(摘自 30张图带你彻底理解红黑树):
红黑树删除情景图
这一块的分析,就没有记录下来;删除操作更加复杂,仅仅是看懂就花了很多时间。可以参考文末的 30张图带你彻底理解红黑树 这篇博客,如果这篇博客理解了,那么整个底层也就明白了。

另附一个很好的在线绘制红黑树的网站红黑树可视化


参考博文


红黑树原理和算法介绍

自平衡二叉树和红黑树

30张图带你彻底理解红黑树

数据结构可视化


推荐博文

手撕Java类HashMap
手撕HashMap迭代器

我与风来


认认真真学习,做思想的产出者,而不是文字的搬运工
错误之处,还望指出

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值