目录

一、红黑树定义

二、节点新增原理:

三、红黑树的生成

2.1 一个节点

2.2 两个节点

2.3 三个节点

2.3.1 第二个节点作为root右子树情况下

2.3.2 第二个节点作为root左子树情况下

四、左旋和右旋

4.1 左旋

4.2 右旋

五、四种情况分析

5.1 情况一变红黑树

5.2 情况二变红黑树

5.3 情况三变红黑树

5.4 情况四变红黑树

5.5 总结

六、源码分析

6.1 链表转换为半成品树

6.2 半成品树转换为红黑树

6.3 二叉搜索树变成红黑树

6.4 旋转

6.4.1 左旋

6.4.2 右旋

6.5 插入新节点


一、红黑树定义

红黑树是一种结点带有颜色属性的二叉查找树,除此还有以下5大性质:
  1. 节点是红色或黑色。
  2. 根是黑色。
  3. 所有叶子都是黑色(叶子是NIL节点,这类节点不可以忽视,否则代码会看不懂)。
  4. 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
  5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点(黑色平衡)。

NIL节点:为了红黑树平衡而添加的空节点

二、节点新增原理:

新插入节点默认是红色,如果是黑色的话那么当前分支上就会多出一个黑色节点出来,从而破坏了黑色平衡。

  1. 如果插入的是第一个节点(根节点),红色变黑色。
  2. 如果父节点为黑色,则直接插入,不需要变色。
  3. 如果父节点是红色,没有叔叔节点或者叔叔节点是黑色,则以爷爷节点为支点旋转,旋转之后原来的爷爷节点变红色,原来的父节点变黑色。
  4. 如果父节点为红色,叔叔节点也是红色(此种情況爷爷节点一定是黑色),则父节点和叔叔节点变黑色,爷爷节点变红色(如果爷爷节点是根节点,则再变成黑色),爷爷节点此时需要递归(把爷爷节点当做新插入的节点再次进行比较)。

三、红黑树的生成

2.1 一个节点

当插入一个元素为5的节点时,由于是新插入的节点,所以应该是红色。但是该树只有一个节点,也就是root根节点,根据红黑树定义2可得,该节点变为黑色。

2.2 两个节点

当已经有一个根节点插入第二个节点元素为x时,分为两种情况。当x>5时,该节点为右节点。当x<5时,该节点为左节点。

2.3 三个节点

在已存在的两个节点产生的这两种情况来看,再添加一个元素,会有以下6种情况

2.3.1 第二个节点作为root右子树情况下

2.3.2 第二个节点作为root左子树情况下

上面存在的六种情况,由于其中两种已经是平衡的红黑树所以不需要旋转。其余的四种情况我们要进一步分析,如何旋转才能让他成为红黑树。

四、左旋和右旋

4.1 左旋

左旋:以某个节点作为旋转点,其右子节点变为旋转节点的父节点,右子节点的左子节点变为旋转节点的右子节点,左子节点保持不变。

4.2 右旋

右旋:以某个节点作为旋转点,其左子节点变为旋转节点的父节点,左子节点的右子节点变为旋转节点的左子节点,右子节点保持不变。

五、四种情况分析

5.1 情况一变红黑树

由图可知,明显该树左边太重了,所有的节点都是左子树,那我们应该向右旋转。以元素为10的节点为旋转点,左子节点5变成他的父节点。左子节点5的右子节点变为旋转节点的左子节点,由于是NIL节点所以在此不再画出。然后进行变色。

5.2 情况二变红黑树

由图可知,情况二的树右边太重了,所有的节点都是右子树,那我们应该向左旋转。以元素为5的节点为旋转点,右子节点10变成他的父节点。右子节点10的左子节点变为旋转节点的右子节点,由于是NIL节点所以在此不再画出。然后进行变色。

5.3 情况三变红黑树

如图所示,情况三刚开始我们无法判定是向左旋还是向右旋。那我们就看他的部分子树,元素10节点和元素x节点如果向右旋转生成的树结构那是不是就和情况二一样了。此时节点为5的右子树为x节点,x节点右子树是元素为10的节点。这就与情况二一样了,再通过左旋并变色处理变成红黑树。

5.4 情况四变红黑树

如图所示,元素5的节点和元素x节点先进行左旋,然后整个树结构与情况一一样,再进行右旋,并进行变色处理,就成为了一个红黑树。

5.5 总结

  • 以上情况都是在节点新增原理的前三条基本原理基础上进行分析的。
  • 无论一个红黑树的节点多少,深度多大,当它新增节点的时候,发生颜色冲突,如果符合节点新增原理的第四条那就无需旋转,只要变色就可以成为新的红黑树。其它需要旋转才能解决的场景都是以上四种情况的变形。
  • 红黑树的形成有两个阶段:成为二叉搜索树和旋转变色。

六、源码分析

6.1 链表转换为半成品树

当满足散列表上的一条链表节点数大于等于8时会进入treeifyBin(tab, hash)方法。将Node节点转换为TreeNode节点,但是TreeNode节点之间通过前后指针相连,并不是左右子树相连。所以我称它为半成品树。

final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;// 散列表为空或者长度小于64时if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)// 散列表进行扩容操作resize();// 否则将链表转换为半成品树(这些树节点由前指针相连)else if ((e = tab[index = (n - 1) & hash]) != null) {TreeNode<K,V> hd = null, tl = null;do {// 根据Node节点创建新的TreeNode节点TreeNode<K,V> p = replacementTreeNode(e, null);// 尾指针为null时,说明树还未创建if (tl == null)// 头指针赋值给第一个树节点hd = p;else {// 新插入的树节点的前指针指向上一个尾节点p.prev = tl;// 尾节点指向新插入的树节点tl.next = p;}// 尾指针指向最新插入的树节点tl = p;} while ((e = e.next) != null); // 遍历下一个节点// 半成品树的头指针赋值给散列表对应位置if ((tab[index] = hd) != null)// 转换为红黑树hd.treeify(tab);}}

由上可知,节点转换为红黑树的两个条件:

  1. 链表节点数大于等于8
  2. 散列表长度大于等于64

6.2 半成品树转换为红黑树

treeify(Node<K,V>[] tab)方法就可以分为先成为一个二叉搜索树,再调用balanceInsertion(root, x)方法通过旋转变色成为红黑树。

    final void treeify(Node<K,V>[] tab) {TreeNode<K,V> root = null;// 遍历循环半成品树节点for (TreeNode<K,V> x = this, next; x != null; x = 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;// 所遍历的树节点hash值大于要插入的节点hash值,向左子树继续遍历if ((ph = p.hash) > h)dir = -1;// 所遍历的树节点hash值小于要插入的节点hash值,向右子树继续遍历else if (ph < h)dir = 1;// 如果要插入的节点hash值等于遍历所在节点hash,hash相等时,通过内存地址进行比较else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0)//说明红黑树中没有与之相等的  那就必须进行插入操作。// 分出插入节点是左节点还是右节点dir = tieBreakOrder(k, pk);TreeNode<K,V> xp = p;// 根据dir区分要继续遍历左节点还是右节点// 当下一个节点为null的时候说明已经找到要插入的树节点所在的位置if ((p = (dir <= 0) ? p.left : p.right) == null) {// 要插入的树节点父指针 指向 调整成树后遍历所得树节点x.parent = xp;// 根据dir区分出插入节点放入左节点还是右节点if (dir <= 0)xp.left = x;elsexp.right = x;// 插入完成后是一个二叉搜索树,需要变色或旋转成为红黑树root = balanceInsertion(root, x);break;}}}}// 检验root节点是不是第一个节点moveRootToFront(tab, root);}

6.3 二叉搜索树变成红黑树

这里是从叶节点遍历到root根节点,从部分到整体一步步满足红黑树的条件。新插入的节点根据是父节点的左子树还是右子树,以及父节点、爷爷节点和叔叔节点的颜色可以分为不同的情况,根据不同的情况分别进行左旋和右旋。

rotateLeft(TreeNode<K,V> root,TreeNode<K,V> p)是进行左旋转。

rotateRight(TreeNode<K,V> root,TreeNode<K,V> p)是进行右旋转。

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,TreeNode<K,V> x) {// 此处就是节点新增原理提到的新插入节点默认为红色x.red = true;// 遍历树x节点一直到root节点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)) {// 如果叔叔节点不为空并且是红色//      xpp//     /   \//   xp(R) Redif ((xppr = xpp.right) != null && xppr.red) {xppr.red = false;xp.red = false;xpp.red = true;x = xpp;}// 如果叔叔节点为空或者不为空是黑色else {// 如果该节点是右节点//      xpp             xpp//     /   \           ///   xp(R) black     xp(R)//     \              \//      x(R)           x(R)if (x == xp.right) {// 左旋root = rotateLeft(root, x = xp);xpp = (xp = x.parent) == null ? null : xp.parent;}// 如果该节点是左节点//      xpp             xpp//     /   \            ///   xp(R) black     xp(R)//   /              ///  x(R)           x(R)if (xp != null) {xp.red = false;if (xpp != null) {xpp.red = true;//      xpp(R)          xpp(R)//     /   \            ///    xp(B) black   xp(B)//   /              ///  x(R)           x(R)// 右旋将的到的新树赋给root,再次遍历root = rotateRight(root, xpp);}}}}// 如果父节点是爷爷节点的右子树else {// 如果叔叔节点不为空并且是红色//             xpp//           /   \//         Red  xp(R)if (xppl != null && xppl.red) {xppl.red = false;xp.red = false;xpp.red = true;x = xpp;}// 如果叔叔节点为空或者不为空是黑色else {// 如果该节点是左节点//      xpp             xpp//         \           /   \//        xp(R)     black xp(R)//          /              ///        x(R)           x(R)if (x == xp.left) {// 右旋root = rotateRight(root, x = xp);xpp = (xp = x.parent) == null ? null : xp.parent;}// 如果该节点是右节点//      xpp             xpp//         \           /   \//        xp(R)     black xp(R)//          \               \//          x(R)            x(R)if (xp != null) {xp.red = false;if (xpp != null) {xpp.red = true;//      xpp(R)          xpp(R)//         \           /   \//        xp(B)     black xp(B)//          \               \//          x(R)            x(R)// 左旋root = rotateLeft(root, xpp);}}}}}}

6.4 旋转

我根据源码将不同的情况下的左旋或右旋结果,用注释表示了出来。大家可以与第五节那四种情况结合分析。

6.4.1 左旋

static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,TreeNode<K,V> p) {TreeNode<K,V> r, pp, rl;if (p != null && (r = p.right) != null) {// p的右节点指向r的左孩子(即rl),如果rl不为空,其父节点指向p;//            p//             \//              r//             ///            rlif ((rl = p.right = r.left) != null)rl.parent = p;//         r//       ///      p//------------------------------------// p节点为根节点,直接root指向r,同时颜色置为黑色(根节点颜色都为黑色)if ((pp = r.parent = p.parent) == null)(root = r).red = false;// 如果该节点是右节点//       pp             pp//     /   \           ///    p(R) black     p(R)//     \              \//      r(R)           r(R)else if (pp.left == p)pp.left = r;// 走完该方法图形后的//        pp             pp//      /   \           ///    r(R) black     r(R)//    /              ///  p(R)            p(R)//---------------------------------//      pp              pp//       \               \//      p(R)            p(R)//         \           /   \//         r(B)     black  r(B)//          \               \//          x(R) t           x(R)elsepp.right = r;// 走完该方法后的图形//      pp              pp//       \               \//       r(B)           r(B)//      /  \           /   \//   p(R)  x(R)     p(R)  x(R)//                   ///                blackr.left = p;p.parent = r;}return root;}

6.4.2 右旋

static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,TreeNode<K,V> p) {TreeNode<K,V> l, pp, lr;if (p != null && (l = p.left) != null) {// p的左节点指向l的右孩子(即lr),如果lr不为空,其父节点指向p;//            p//          ///         l//          \//          lrif ((lr = p.left = l.right) != null)lr.parent = p;//         l//          \//           p//------------------------------------// 如果pp为null,说明p节点为根节点,直接root指向l,同时颜色置为黑色(根节点颜色都为黑色)if ((pp = l.parent = p.parent) == null)(root = l).red = false;//        pp             pp//         \           /   \//        p(R)     black  p(R)//          /              ///        l(R)           l(R)else if (pp.right == p)pp.right = l;// 走完该方法后的图形//        pp             pp//         \           /   \//        l(R)     black  l(R)//          \               \//          p(R)            p(R)// --------------------------------------//        pp(B)            pp(B)//        /               ///      p(R)            p(R)//     /   \            ///    l(B) black     l(B)//   /              ///  x(R)           x(R)elsepp.left = l;// 走完该方法后的图形//        pp(B)           pp(B)//        /               ///      l(B)            l(B)//     /   \            /  \//    x(R) p(R)      x(R) p(R)//           \//          blackl.right = p;p.parent = l;}return root;}

6.5 插入新节点

插入新节点从root节点往下遍历分为4种情况:

  1. 要插入的节点hash值小于遍历所在节点hash,遍历左子树
  2. 要插入的节点hash值大于遍历所在节点hash,遍历右子树
  3. 要插入的节点hash值等于遍历所在节点hash,并且key值相等返回该节点
  4. 要插入的节点hash值等于遍历所在节点hash,但是key不等时,发生hash冲突。此时又分为两种情况。
    1. 遍历该节点的左右子节点是否存在hash相等,并且key也相等的节点,有则返回该节点
    2. 如果没有则调用tieBreakOrder(k, pk)方法,比较key值,确定是遍历左子树还是右子树
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;// 如果要插入的节点hash值小于遍历所在节点hash,遍历左子树if ((ph = p.hash) > h)dir = -1;// 如果要插入的节点hash值大于遍历所在节点hash,遍历右子树else if (ph < h)dir = 1;// 如果要插入的节点hash值等于遍历所在节点hash,并且// key值相等返回该节点else if ((pk = p.key) == k || (k != null && k.equals(pk)))return p;// 如果要插入的节点hash值等于遍历所在节点hash,但是key不等时,此时发生hash冲突else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0) {// 在左右子树递归的寻找 是否有key的hash相同  并且equals相同的节点if (!searched) {TreeNode<K,V> q, ch;searched = true;// (ch = p.left) != null 左子树不为空// (ch = p.right) != null 右子树不为空// (q = ch.find(h, k, kc)) != null) 递归查找hash值相等的并且key也相等// 如果找到hash值相等的则返回该节点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;// 如果dir小于0,那p等于p的左子树节点,不为null则继续遍历// 如果dir大于0,那p等于p的右子树节点,不为null则继续遍历// 当为null时说明是叶子节点则执行下面方法if ((p = (dir <= 0) ? p.left : p.right) == null) {Node<K,V> xpn = xp.next;// 由于TreeNode继承了Node,创建一个新的TreeNode节点将要插入的// hash、key、value存入TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);// dir小于0,新节点为左节点if (dir <= 0)xp.left = x;// dir大于0,新节点为右节点elsexp.right = x;xp.next = x;x.parent = x.prev = xp;if (xpn != null)((TreeNode<K,V>)xpn).prev = x;// balanceInsertion(root, x)方法让一个树成为红黑树,并返回根节点// moveRootToFront,检验root节点是不是第一个节点moveRootToFront(tab, balanceInsertion(root, x));return null;}}}

HashMap红黑树原理及源码分析---图形、注释一应俱全相关推荐

  1. Android技术栈(五)HashMap(包括红黑树)与ArrayMap源码解析

    1 总览 本文会对 Android 中常用HashMap(有红黑树)和ArrayMap进行源码解析,其中 HashMap 源码来自 Android Framework API 28 (JDK=1.8) ...

  2. HashMap红黑树原理解析

    HashMap红黑树原理解析 定义: 简单来说红黑树是一种近视平衡二叉查找树,主要优点是"平衡",即左右子树高度几乎一致,以此来防止树退化为链表,通过这种方式来保障查找的时间复杂度 ...

  3. ConcurrentHashMap实现原理及源码分析

    ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现(若对HashMap的实现原理还不甚了解,可参考我的另一篇文章HashMap实现原理及源码分析),Con ...

  4. concurrenthashmap_ConcurrentHashMap实现原理及源码分析

    ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现(若对HashMap的实现原理还不甚了解,可参考我的另一篇文章HashMap实现原理及源码分析),Con ...

  5. 深入理解Spark 2.1 Core (十一):Shuffle Reduce 端的原理与源码分析

    我们曾经在<深入理解Spark 2.1 Core (一):RDD的原理与源码分析 >讲解过: 为了有效地实现容错,RDD提供了一种高度受限的共享内存,即RDD是只读的,并且只能通过其他RD ...

  6. 深入理解Spark 2.1 Core (七):Standalone模式任务执行的原理与源码分析

    这篇博文,我们就来讲讲Executor启动后,是如何在Executor上执行Task的,以及其后续处理. 执行Task 我们在<深入理解Spark 2.1 Core (三):任务调度器的原理与源 ...

  7. 深入理解Spark 2.1 Core (六):Standalone模式运行的原理与源码分析

    我们讲到了如何启动Master和Worker,还讲到了如何回收资源.但是,我们没有将AppClient是如何启动的,其实它们的启动也涉及到了资源是如何调度的.这篇博文,我们就来讲一下AppClient ...

  8. 【转】HashMap,ArrayMap,SparseArray源码分析及性能对比

    HashMap,ArrayMap,SparseArray源码分析及性能对比 jjlanbupt 关注 2016.06.03 20:19* 字数 2165 阅读 7967评论 13喜欢 43 Array ...

  9. SIFT原理与源码分析:DoG尺度空间构造

    <SIFT原理与源码分析>系列文章索引:http://blog.csdn.net/xiaowei_cqu/article/details/8069548 尺度空间理论 自然界中的物体随着观 ...

最新文章

  1. 用SDM架构Cisco IOS ***图文详解全攻略(一)——easy ***
  2. Android旋转视频工具类,Android开发实现的IntentUtil跳转多功能工具类【包含视频、音频、图片、摄像头等操作功能】...
  3. Socket,SocketImpl与SocketImplFactory的关系
  4. 前端项目里常见的十种报错及其解决办法
  5. 元素(洛谷-P4570)
  6. linux串口环形缓冲区,能不能讲解下串口环形缓冲区的概念?
  7. 中国农业大学计算机研究生专业课,中国农业大学2019计算机考研纯干货分享
  8. 精易论坛多线程培训第二期
  9. 秀米编辑器详细使用教程
  10. 成功解决AttributeError: ‘Series‘ object has no attribute ‘split‘
  11. 【答学员问】虚拟机不能正常启动,提示找不到VMX二进制文件
  12. [词性] 十一、介词 2 地点状语在前,时间状语在后 [ at ]
  13. 户用光伏数字化解决方案
  14. Simulink 界面模型的矢量图复制
  15. 时光机穿梭(管理修改)
  16. 【cx_Oracle】记录一次 python cx_Oracle出现 ORA-00911: 无效字符
  17. 100M和1000M网线做法
  18. C语言 计算存款利息
  19. R pdf大小_全能格式转换工具分享,PDF 转 Word、视频图片格式转换等
  20. [蓝桥杯 2018 国 B] 搭积木 (区间dp + 二维前缀和优化)

热门文章

  1. myeclipse常用快捷键简介
  2. UVM中item_done的作用?
  3. 2022年移动应用程序开发的最佳后端框架
  4. 常用数据类型和bytes数组互转
  5. mysql 哨兵模式_redis配置主从+sentine哨兵模式
  6. 52abp前端angular 字体资源本地化
  7. 微信开发学习:点歌台
  8. 从毕业生成为职业人(转载)
  9. PTAN实战1 || 安装与使用
  10. 域名whois查询函数(万网和新网