红黑树

红黑树顾名思义就是节点是红色或者黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡。对于一棵有效的红黑树二叉树而言我们必须增加如下规则:

1、每个节点都只能是红色或者黑色

2、根节点是黑色

3、每个叶节点(NIL节点,空节点)是黑色的。

4、如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。

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

这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。所以红黑树它是复杂而高效的,其检索效率O(log n)。

红黑树的插入操作

如果插入第一个节点,我们直接用树根记录这个节点,并设置为黑色,否则作递归查找插入。

默认插入的节点颜色都是红色,因为插入黑色节点会破坏根路径上的黑色节点总数,但即使如此,也会出现连续红色节点的情况。因此在一般的插入操作之后,出现红黑树约束条件不满足的情况(称为失去平衡)时,就必须要根据当前的红黑树的情况做相应的调整。和AVL树的平衡调整通过旋转操作的实现类似,红黑树的调整操作一般都是通过旋转结合节点的变色操作来完成的。

  • 叔父节点是黑色(若是空节点则默认为黑色)

这种情况下通过旋转和变色操作可以使红黑树恢复平衡。但是考虑当前节点n和父节点p的位置又分为四种情况:

A、n是p左子节点,p是g的左子节点。

B、n是p右子节点,p是g的右子节点。

C、n是p左子节点,p是g的右子节点。

D、n是p右子节点,p是g的左子节点。

  • 情况A:n是p左子节点,p是g的左子节点。针对该情况可以通过一次右旋转操作,并将p设为黑色,g设为红色完成重新平衡。

右旋操作的步骤是将p挂接在g节点原来的位置(如果g原是根节点,需要考虑边界条件),将p的右子树x挂到g的左子节点,再把g挂在p的右子节点上,完成右旋操作。这里将最终旋转结果的子树的根节点作为旋转轴(p节点),也就是说旋转轴在旋转结束后称为新子树的根节点!

  • 情况B则需要使用左单旋操作来解决平衡问题,方法和情况A类似。

  • 情况C:n是p左子节点,p是g的右子节点。针对该情况通过一次左旋,一次右旋操作(旋转轴都是n,注意不是p),并将n设为黑色,g设为红色完成重新平衡。

需要注意的是,由于此时新插入的节点是n,它的左右子树x,y都是空节点,但即使如此,旋转操作的结果需要将x,y新的位置设置正确(如果不把p和g的对应分支设置为空节点的话,就会破坏树的结构)。在之后的其他操作中,待旋转的节点n的左右子树可能就不是空节点了。

  • 情况D则需要使用一次右单旋,一次左单旋操作来解决平衡问题,方法和情况C类似。

  • 叔父节点是红色

当叔父节点是红色时,则不能直接通过上述方式处理了(把前边的所有情况的u节点看作红色,会发现节点u和g是红色冲突的)。但是我们可以交换g与p,u节点的颜色完成当前冲突的解决。

但是仅仅这样做颜色交换是不够的,因为祖父节点g的父节点(记作gp)如果也是红色的话仍然会有冲突(g和gp是连续的红色,违反规则2)。为了解决这样的冲突,我们需要从当前插入点n向根节点root回溯两次。

第一次回溯时处理所有拥有两个红色节点的节点,并按照上图中的方式交换父节点g与子节点p,u的颜色,并暂时忽略gp和p的颜色冲突。如果根节点的两个子节点也是这种情况,则在颜色交换完毕后重新将根节点设置为黑色。

第二次回溯专门处理连续的红色节点冲突。由于经过第一遍的处理,在新插入点n的路径上一定不存在同为红色的兄弟节点了。而仍出现gp和p的红色冲突时,gp的兄弟节点(gu)可以断定为黑色,这样就回归前边讨论的叔父节点为黑色时的情况处理。

由于发生冲突的两个红色节点位置可能是任意的,因此会出现上述的四种旋转情况。不过我们把靠近叶子的红色节点(g)看作新插入的节点,这样面对A,B情况则把p的父节点gp作为旋转轴,旋转后gp会是新子树的根,而面对C,D情况时把p作为旋转轴即可,旋转后p为新子树的根(因此可以把四种旋转方式封装起来)。

在第二次回溯时,虽然每次遇到红色冲突旋转后都会提升g和gp节点的位置(与根节点的距离减少),但是无论g和gp谁是新子树的根都不会影响新插入节点n到根节点root路径的回溯,而且一旦新子树的根到达根节点(parent指针为空)就可以停止回溯了。

TreeMap数据结构

  1. public class TreeMap<K,V>
  2. extends AbstractMap<K,V>
  3. implements NavigableMap<K,V>, Cloneable, java.io.Serializable

TreeMap继承AbstractMap,实现NavigableMap、Cloneable、Serializable三个接口。其中AbstractMap表明TreeMap为一个Map即支持key-value的集合, NavigableMap则意味着它支持一系列的导航方法,具备针对给定搜索目标返回最接近匹配项的导航方法 。

TreeMap中同时也包含了如下几个重要的属性:

  1. //比较器,因为TreeMap是有序的,通过comparator接口我们可以对TreeMap的内部排序进行精密的控制
  2. private final Comparator<? super K> comparator;
  3. //TreeMap红-黑节点,为TreeMap的内部类
  4. private transient Entry<K,V> root = null;
  5. //容器大小
  6. private transient int size = 0;
  7. //TreeMap修改次数
  8. private transient int modCount = 0;
  9. //红黑树的节点颜色--红色
  10. private static final boolean RED = false;
  11. //红黑树的节点颜色--黑色
  12. private static final boolean BLACK = true;

对于叶子节点Entry是TreeMap的内部类,它有几个重要的属性:

  1. //键
  2. K key;
  3. //值
  4. V value;
  5. //左孩子
  6. Entry<K,V> left = null;
  7. //右孩子
  8. Entry<K,V> right = null;
  9. //父亲
  10. Entry<K,V> parent;
  11. //颜色
  12. boolean color = BLACK;

TreeMap put()方法

在TreeMap的put()的实现方法中主要分为两个步骤,第一:构建排序二叉树,第二:平衡二叉树。

对于排序二叉树的创建,其添加节点的过程如下:

1、以根节点为初始节点进行检索。

2、与当前节点进行比对,若新增节点值较大,则以当前节点的右子节点作为新的当前节点。否则以当前节点的左子节点作为新的当前节点。

3、循环递归2步骤知道检索出合适的叶子节点为止。

4、将新增节点与3步骤中找到的节点进行比对,如果新增节点较大,则添加为右子节点;否则添加为左子节点。

  1. public V put(K key, V value) {
  2. //用t表示二叉树的当前节点
  3. Entry<K,V> t = root;
  4. //t为null表示一个空树,即TreeMap中没有任何元素,直接插入
  5. if (t == null) {
  6. //比较key值,个人觉得这句代码没有任何意义,空树还需要比较、排序?
  7. compare(key, key); // type (and possibly null) check
  8. //将新的key-value键值对创建为一个Entry节点,并将该节点赋予给root
  9. root = new Entry<>(key, value, null);
  10. //容器的size = 1,表示TreeMap集合中存在一个元素
  11. size = 1;
  12. //修改次数 + 1
  13. modCount++;
  14. return null;
  15. }
  16. int cmp; //cmp表示key排序的返回结果
  17. Entry<K,V> parent; //父节点
  18. // split comparator and comparable paths
  19. Comparator<? super K> cpr = comparator; //指定的排序算法
  20. //如果cpr不为空,则采用既定的排序算法进行创建TreeMap集合
  21. if (cpr != null) {
  22. do {
  23. parent = t; //parent指向上次循环后的t
  24. //比较新增节点的key和当前节点key的大小
  25. cmp = cpr.compare(key, t.key);
  26. //cmp返回值小于0,表示新增节点的key小于当前节点的key,则以当前节点的左子节点作为新的当前节点
  27. if (cmp < 0)
  28. t = t.left;
  29. //cmp返回值大于0,表示新增节点的key大于当前节点的key,则以当前节点的右子节点作为新的当前节点
  30. else if (cmp > 0)
  31. t = t.right;
  32. //cmp返回值等于0,表示两个key值相等,则新值覆盖旧值,并返回新值
  33. else
  34. return t.setValue(value);
  35. } while (t != null);
  36. }
  37. //如果cpr为空,则采用默认的排序算法进行创建TreeMap集合
  38. else {
  39. if (key == null) //key值为空抛出异常
  40. throw new NullPointerException();
  41. /* 下面处理过程和上面一样 */
  42. Comparable<? super K> k = (Comparable<? super K>) key;
  43. do {
  44. parent = t;
  45. cmp = k.compareTo(t.key);
  46. if (cmp < 0)
  47. t = t.left;
  48. else if (cmp > 0)
  49. t = t.right;
  50. else
  51. return t.setValue(value);
  52. } while (t != null);
  53. }
  54. //将新增节点当做parent的子节点
  55. Entry<K,V> e = new Entry<>(key, value, parent);
  56. //如果新增节点的key小于parent的key,则当做左子节点
  57. if (cmp < 0)
  58. parent.left = e;
  59. //如果新增节点的key大于parent的key,则当做右子节点
  60. else
  61. parent.right = e;
  62. /*
  63. * 上面已经完成了排序二叉树的的构建,将新增节点插入该树中的合适位置
  64. * 下面fixAfterInsertion()方法就是对这棵树进行调整、平衡,具体过程参考上面的五种情况
  65. */
  66. fixAfterInsertion(e);
  67. //TreeMap元素数量 + 1
  68. size++;
  69. //TreeMap容器修改次数 + 1
  70. modCount++;
  71. return null;
  72. }

上面代码中do{}代码块是实现排序二叉树的核心算法,通过该算法我们可以确认新增节点在该树的正确位置。找到正确位置后将插入即可,这样做了其实还没有完成,因为我知道TreeMap的底层实现是红黑树,红黑树是一棵平衡排序二叉树,普通的排序二叉树可能会出现失衡的情况,所以下一步就是要进行调整。fixAfterInsertion(e); 调整的过程务必会涉及到红黑树的左旋、右旋、着色三个基本操作。代码如下:

  1. /**
  2. * 新增节点后的修复操作
  3. * x 表示新增节点
  4. */
  5. private void fixAfterInsertion(Entry<K,V> x) {
  6. x.color = RED; //新增节点的颜色为红色
  7.  
  8. //循环 直到 x不是根节点,且x的父节点不为红色
  9. while (x != null && x != root && x.parent.color == RED) {
  10. //如果X的父节点(P)是其父节点的父节点(G)的左节点
  11. if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
  12. //获取X的叔节点(U)
  13. Entry<K,V> y = rightOf(parentOf(parentOf(x)));
  14. //如果X的叔节点(U) 为红色
  15. if (colorOf(y) == RED) {
  16. //将X的父节点(P)设置为黑色
  17. setColor(parentOf(x), BLACK);
  18. //将X的叔节点(U)设置为黑色
  19. setColor(y, BLACK);
  20. //将X的父节点的父节点(G)设置红色
  21. setColor(parentOf(parentOf(x)), RED);
  22. x = parentOf(parentOf(x));
  23. }
  24. //如果X的叔节点(U为黑色)
  25. else {
  26. //如果X节点为其父节点(P)的右子树,则进行左旋转
  27. if (x == rightOf(parentOf(x))) {
  28. //将X的父节点作为X
  29. x = parentOf(x);
  30. //右旋转
  31. rotateLeft(x);
  32. } //将X的父节点(P)设置为黑色
  33. setColor(parentOf(x), BLACK);
  34. //将X的父节点的父节点(G)设置红色
  35. setColor(parentOf(parentOf(x)), RED);
  36. //以X的父节点的父节点(G)为中心右旋转
  37. rotateRight(parentOf(parentOf(x)));
  38. }
  39. }
  40. //如果X的父节点(P)是其父节点的父节点(G)的右节点
  41. else {
  42. //获取X的叔节点(U)
  43. Entry<K,V> y = leftOf(parentOf(parentOf(x)));
  44. //如果X的叔节点(U) 为红色
  45. if (colorOf(y) == RED) {
  46. //将X的父节点(P)设置为黑色
  47. setColor(parentOf(x), BLACK);
  48. //将X的叔节点(U)设置为黑色
  49. setColor(y, BLACK);
  50. //将X的父节点的父节点(G)设置红色
  51. setColor(parentOf(parentOf(x)), RED);
  52. x = parentOf(parentOf(x));
  53. }
  54. //如果X的叔节点(U为黑色);这里会存在两种情况
  55. else {
  56. //如果X节点为其父节点(P)的右子树,则进行左旋转
  57. if (x == leftOf(parentOf(x))) {
  58. //将X的父节点作为X
  59. x = parentOf(x);
  60. //右旋转
  61. rotateRight(x);
  62. }
  63. //(情况五)
  64. //将X的父节点(P)设置为黑色
  65. setColor(parentOf(x), BLACK);
  66. //将X的父节点的父节点(G)设置红色
  67. setColor(parentOf(parentOf(x)), RED);
  68. //以X的父节点的父节点(G)为中心右旋转
  69. rotateLeft(parentOf(parentOf(x)));
  70. }
  71. }
  72. }
  73. //将根节点G强制设置为黑色
  74. root.color = BLACK;
  75. }
  • 对这段代码的研究我们发现,其处理过程完全符合红黑树新增节点的处理过程。所以在看这段代码的过程一定要对红黑树的新增节点过程有了解。在这个代码中还包含几个重要的操作。左旋(rotateLeft())、右旋(rotateRight())、着色(setColor())。

  • 左旋:rotateLeft()

  • 所谓左旋转,就是将新增节点(N)当做其父节点(P),将其父节点P当做新增节点(N)的左子节点。即:G.left ---> N ,N.left ---> P。

  1. private void rotateLeft(Entry<K,V> p) {
  2. if (p != null) {
  3. //获取P的右子节点,其实这里就相当于新增节点N(情况四而言)
  4. Entry<K,V> r = p.right;
  5. //将R的左子树设置为P的右子树
  6. p.right = r.left;
  7. //若R的左子树不为空,则将P设置为R左子树的父亲
  8. if (r.left != null)
  9. r.left.parent = p;
  10. //将P的父亲设置R的父亲
  11. r.parent = p.parent;
  12. //如果P的父亲为空,则将R设置为跟节点
  13. if (p.parent == null)
  14. root = r;
  15. //如果P为其父节点(G)的左子树,则将R设置为P父节点(G)左子树
  16. else if (p.parent.left == p)
  17. p.parent.left = r;
  18. //否则R设置为P的父节点(G)的右子树
  19. else
  20. p.parent.right = r;
  21. //将P设置为R的左子树
  22. r.left = p;
  23. //将R设置为P的父节点
  24. p.parent = r;
  25. }
  26. }

右旋:rotateRight()

所谓右旋转即,P.right ---> G、G.parent ---> P。

  1. private void rotateRight(Entry<K,V> p) {
  2. if (p != null) {
  3. //将L设置为P的左子树
  4. Entry<K,V> l = p.left;
  5. //将L的右子树设置为P的左子树
  6. p.left = l.right;
  7. //若L的右子树不为空,则将P设置L的右子树的父节点
  8. if (l.right != null)
  9. l.right.parent = p;
  10. //将P的父节点设置为L的父节点
  11. l.parent = p.parent;
  12. //如果P的父节点为空,则将L设置根节点
  13. if (p.parent == null)
  14. root = l;
  15. //若P为其父节点的右子树,则将L设置为P的父节点的右子树
  16. else if (p.parent.right == p)
  17. p.parent.right = l;
  18. //否则将L设置为P的父节点的左子树
  19. else
  20. p.parent.left = l;
  21. //将P设置为L的右子树
  22. l.right = p;
  23. //将L设置为P的父节点
  24. p.parent = l;
  25. }
  26. }

我是天王盖地虎的分割线

参考:http://blog.csdn.net/chenssy/article/details/26668941

红黑树,TreeMap,插入操作的更多相关文章

  1. JDK1.8 HashMap$TreeNode.balanceInsertion 红黑树平衡插入

    红黑树介绍 1.节点是红色或黑色. 2.根节点是黑色. 3.每个叶子节点都是黑色的空节点(NIL节点). 4 每个红色节点的两个子节点都是黑色.(从每个叶子到根的所有路径上不能有两个连续的红色节点) ...

  2. 红黑树的删除操作---以JDK源码为例

    删除操作需要处理的情况: 1.删除的是红色节点,则删除节点并不影响红黑树的树高,无需处理. 2.删除的是黑色节点,则删除后,删除节点所在子树的黑高BH将减少1,需要进行调整. 节点标记: 正在处理的节 ...

  3. 高级数据结构---红黑树及其插入左旋右旋代码java实现

    前面我们说到的二叉查找树,可以看到根结点是初始化之后就是固定了的,后续插入的数如果都比它大,或者都比它小,那么这个时候它就退化成了链表了,查询的时间复杂度就变成了O(n),而不是理想中O(logn), ...

  4. 红黑树的插入Java实现

    package practice; public class TestMain { public static void main(String[] args) { int[] ao = {5, 1, ...

  5. 第八章 高级搜索树 (xa3)红黑树:插入

  6. 红黑树插入操作原理及java实现

    红黑树是一种二叉平衡查找树,每个结点上有一个存储位来表示结点的颜色,可以是RED或BLACK.红黑树具有以下性质: (1) 每个结点是红色或是黑色 (2) 根结点是黑色的 (3) 如果一个结点是红色的 ...

  7. 算法导论学习---红黑树具体解释之插入(C语言实现)

    前面我们学习二叉搜索树的时候发如今一些情况下其高度不是非常均匀,甚至有时候会退化成一条长链,所以我们引用一些"平衡"的二叉搜索树.红黑树就是一种"平衡"的二叉搜 ...

  8. [转载] 红黑树(Red Black Tree)- 对于 JDK TreeMap的实现

    转载自http://blog.csdn.net/yangjun2/article/details/6542321 介绍另一种平衡二叉树:红黑树(Red Black Tree),红黑树由Rudolf B ...

  9. 关于红黑树(R-B tree)原理,看这篇如何

    学过数据数据结构都知道二叉树的概念,而又有多种比较常见的二叉树类型,比如完全二叉树.满二叉树.二叉搜索树.均衡二叉树.完美二叉树等:今天我们要说的红黑树就是就是一颗非严格均衡的二叉树,均衡二叉树又是在 ...

随机推荐

  1. [ 转载 ]学习笔记-深入剖析Java中的装箱和拆箱

    深入剖析Java中的装箱和拆箱 自动装箱和拆箱问题是Java中一个老生常谈的问题了,今天我们就来一些看一下装箱和拆箱中的若干问题.本文先讲述装箱和拆箱最基本的东西,再来看一下面试笔试中经常遇到的与装箱 ...

  2. PHP7.x新特性

    1.太空船操作符太空船操作符用于比较两个表达式. 当$a小于. 等于或大于$b时它分别返回-1. 0或1. // Integers echo 1 <=> 1; // 0 echo 1 &l ...

  3. Qt Quick快速入门之qml与C++交互

    C++中使用qml对象,直接使用findChild获取qml对象,然后调用setProperty方法设置属性,当然必须在加载qml之后才能使用,不然findChild找不到对象,用法如下. engin ...

  4. hihocoder#1046 K个串 可持久化线段树 + 堆

    首先考虑二分,然后发现不可行.... 注意到\(k\)十分小,尝试从这里突破 首先用扫描线来处理出以每个节点为右端点的区间的权值和,用可持久化线段树存下来 在所有的右端点相同的区间中,挑一个权值最大的 ...

  5. BZOJ 4066 简单题(KD树)

    [题目链接] http://www.lydsy.com/JudgeOnline/problem.php?id=4066 [题目大意] 要求维护矩阵内格子加点和矩阵查询 [题解] 往KD树上加权值点,支 ...

  6. 【贪心】【后缀自动机】Gym - 101466E - Text Editor

    题意:给你两个串A,B,以及一个整数K,让你找到B的一个尽可能长的前缀,使得其在A串中出现的次数不小于K次. 对A串建立后缀自动机,然后把B串放在上面跑,由于每到一个结点,该结点endpos集合的大小 ...

  7. Kali2.0通过xrdp实现windows远程链接Linux

    标题:Kali2.0通过xrdp实现windows远程链接Linux apt-get install xrdp 首先需要安装xrdp 接下来安装xfce4 apt-get install xfce4 ...

  8. [CC-XYHUMOQ]A humongous Query

    [CC-XYHUMOQ]A humongous Query 题目大意: 有一个长度为\(n(n\le32)\)的以\(1\)开头,\(0\)结尾的\(01\)序列\(S\).令\(f(S)\)表示序列 ...

  9. Tomcat篇

    安装tomcat 先从tomcat官网找到最新的版本下载地址,我找的是Core下的安装包,下载到本地: wget http://mirror.bit.edu.cn/apache/tomcat/tomc ...

  10. Android Tasker应用之自动查询并显示话费流量套餐信息

    Android Tasker应用之自动查询并显示话费流量套餐信息 虽然Android平台有非常多的流量监控软件,但最准确的流量数据还是掌握在运营商手里.有些朋友可能像我一样时不时地发短信查询流量信息, ...