图解集合7:红黑树概念、红黑树的插入及旋转操做详细解读

原文地址http://www.cnblogs.com/xrq730/p/6867924.html,转载请注明出处,谢谢!html

 

初识TreeMap算法

以前的文章讲解了两种Map,分别是HashMap与LinkedHashMap,它们保证了以O(1)的时间复杂度进行增、删、改、查,从存储角度考虑,这两种数据结构是很是优秀的。另外,LinkedHashMap还额外地保证了Map的遍历顺序能够与put顺序一致,解决了HashMap自己无序的问题。安全

尽管如此,HashMap与LinkedHashMap仍是有本身的局限性----它们不具有统计性能,或者说它们的统计性能时间复杂度并非很好才更准确,全部的统计必须遍历全部Entry,所以时间复杂度为O(N)。好比Map的Key有一、二、三、四、五、六、7,我如今要统计:数据结构

  1. 全部Key比3大的键值对有哪些
  2. Key最小的和Key最大的是哪两个

就相似这些操做,HashMap和LinkedHashMap作得比较差,此时咱们可使用TreeMap。TreeMap的Key按照天然顺序进行排序或者根据建立映射时提供的Comparator接口进行排序。TreeMap为增、删、改、查这些操做提供了log(N)的时间开销,从存储角度而言,这比HashMap与LinkedHashMap的O(1)时间复杂度要差些;可是在统计性能上,TreeMap一样能够保证log(N)的时间开销,这又比HashMap与LinkedHashMap的O(N)时间复杂度好很多。性能

所以总结而言:若是只须要存储功能,使用HashMap与LinkedHashMap是一种更好的选择;若是还须要保证统计性能或者须要对Key按照必定规则进行排序,那么使用TreeMap是一种更好的选择。学习

 

红黑树的一些基本概念测试

在讲TreeMap前仍是先说一下红黑树的一些基本概念,这样能够更好地理解以后TreeMap的源代码。spa

二叉查找树是在生成的时候是很是容易失衡的,形成的最坏状况就是一边倒(即只有左子树/右子树),这样会致使树检索的效率大大下降。(关于树和二叉查找树能够看我以前写的一篇文章树型结构线程

红黑树是为了维护二叉查找树的平衡而产生的一种树,根据维基百科的定义,红黑树有五个特性,但我以为讲得不太易懂,我本身总结一下,红黑树的特性大体有三个(换句话说,插入、删除节点后整个红黑树也必须知足下面的三个性质,若是不知足则必须进行旋转):翻译

  1. 根节点与叶节点都是黑色节点,其中叶节点为Null节点
  2. 每一个红色节点的两个子节点都是黑色节点,换句话说就是不能有连续两个红色节点
  3. 从根节点到全部叶子节点上的黑色节点数量是相同的

上述的性质约束了红黑树的关键:从根到叶子的最长可能路径很少于最短可能路径的两倍长。获得这个结论的理由是:

  1. 红黑树中最短的可能路径是所有为黑色节点的路径
  2. 红黑树中最长的可能路径是红黑相间的路径

此时(2)正好是(1)的两倍长。结果就是这个树大体上是平衡的,由于好比插入、删除和查找某个值这样的操做最坏状况都要求与树的高度成比例,这个高度的理论上限容许红黑树在最坏状况下都是高效的,而不一样于普通的二叉查找树,最终保证了红黑树可以以O(log2 n) 的时间复杂度进行搜索、插入、删除

下面展现一张红黑树的实例图:

能够看到根节点到全部NULL LEAF节点(即叶子节点)所通过的黑色节点都是2个。

另外从这张图上咱们还能获得一个结论:红黑树并非高度的平衡树。所谓平衡树指的是一棵空树或它的左右两个子树的高度差的绝对值不超过1,可是咱们看:

  • 最左边的路径0026-->0017-->0012-->0010-->0003-->NULL LEAF,它的高度为5
  • 最后边的路径0026-->0041-->0047-->NULL LEAF,它的高度为3

左右子树的高度差值为2,所以红黑树并非高度平衡的,它放弃了高度平衡的特性而只追求部分平衡,这种特性下降了插入、删除时对树旋转的要求,从而提高了树的总体性能。而其余平衡树好比AVL树虽然查找性能为性能是O(logn),可是为了维护其平衡特性,可能要在插入、删除操做时进行屡次的旋转,产生比较大的消耗。

 

四个关注点在TreeMap上的答案

关 注 点 结  论
TreeMap是否容许键值对为空 Key不容许为空,Value容许为空 
TreeMap是否容许重复数据 Key重复会覆盖,Value容许重复 
TreeMap是否有序 按照Key的天然顺序排序或者Comparator接口指定的排序算法进行排序 
TreeMap是否线程安全  非线程安全

 

TreeMap基本数据结构

TreeMap基于红黑树实现,既然是红黑树,那么每一个节点中除了Key-->Value映射以外,必然存储了红黑树节点特有的一些内容,它们是:

  1. 父节点引用
  2. 左子节点引用
  3. 右子节点引用
  4. 节点颜色

TreeMap的节点Java代码定义为:

1 static final class Entry<K,V> implements Map.Entry<K,V> {
2         K key;
3         V value;
4         Entry<K,V> left = null;
5         Entry<K,V> right = null;
6         Entry<K,V> parent;
7         boolean color = BLACK;
8         ...
9 }

因为颜色只有红色和黑色两种,所以颜色可使用布尔类型(boolean)来表示,黑色表示为true,红色为false。

 

TreeMap添加数据流程总结

首先看一下TreeMap如何添加数据,测试代码为:

 1 public class MapTest {
 2 
 3     @Test
 4     public void testTreeMap() {
 5         TreeMap<Integer, String> treeMap = new TreeMap<Integer, String>();
 6         treeMap.put(10, "10");
 7         treeMap.put(85, "85");
 8         treeMap.put(15, "15");
 9         treeMap.put(70, "70");
10         treeMap.put(20, "20");
11         treeMap.put(60, "60");
12         treeMap.put(30, "30");
13         treeMap.put(50, "50");
14 
15         for (Map.Entry<Integer, String> entry : treeMap.entrySet()) {
16             System.out.println(entry.getKey() + ":" + entry.getValue());
17         }
18     }
19     
20 }

本文接下来的内容会给出插入每条数据以后红黑树的数据结构是什么样子的。首先看一下treeMap的put方法的代码实现:

 1 public V put(K key, V value) {
 2     Entry<K,V> t = root;
 3     if (t == null) {
 4         compare(key, key); // type (and possibly null) check
 5 
 6         root = new Entry<>(key, value, null);
 7         size = 1;
 8         modCount++;
 9         return null;
10     }
11     int cmp;
12     Entry<K,V> parent;
13     // split comparator and comparable paths
14     Comparator<? super K> cpr = comparator;
15     if (cpr != null) {
16         do {
17             parent = t;
18             cmp = cpr.compare(key, t.key);
19             if (cmp < 0)
20                 t = t.left;
21             else if (cmp > 0)
22                 t = t.right;
23             else
24                 return t.setValue(value);
25         } while (t != null);
26     }
27     else {
28         if (key == null)
29             throw new NullPointerException();
30         Comparable<? super K> k = (Comparable<? super K>) key;
31         do {
32             parent = t;
33             cmp = k.compareTo(t.key);
34             if (cmp < 0)
35                 t = t.left;
36             else if (cmp > 0)
37                 t = t.right;
38             else
39                 return t.setValue(value);
40         } while (t != null);
41     }
42     Entry<K,V> e = new Entry<>(key, value, parent);
43     if (cmp < 0)
44         parent.left = e;
45     else
46         parent.right = e;
47     fixAfterInsertion(e);
48     size++;
49     modCount++;
50     return null;
51 }

从这段代码,先总结一下TreeMap添加数据的几个步骤:

  1. 获取根节点,根节点为空,产生一个根节点,将其着色为黑色,退出余下流程
  2. 获取比较器,若是传入的Comparator接口不为空,使用传入的Comparator接口实现类进行比较;若是传入的Comparator接口为空,将Key强转为Comparable接口进行比较
  3. 从根节点开始逐一依照规定的排序算法进行比较,取比较值cmp,若是cmp=0,表示插入的Key已存在;若是cmp>0,取当前节点的右子节点;若是cmp<0,取当前节点的左子节点
  4. 排除插入的Key已存在的状况,第(3)步的比较一直比较到当前节点t的左子节点或右子节点为null,此时t就是咱们寻找到的节点,cmp>0则准备往t的右子节点插入新节点,cmp<0则准备往t的左子节点插入新节点
  5. new出一个新节点,默认为黑色,根据cmp的值向t的左边或者右边进行插入
  6. 插入以后进行修复,包括左旋、右旋、从新着色这些操做,让树保持平衡性

第1~第5步都没有什么问题,红黑树最核心的应当是第6步插入数据以后进行的修复工做,对应的Java代码是TreeMap中的fixAfterInsertion方法,下面看一下put每一个数据以后TreeMap都作了什么操做,借此来理清TreeMap的实现原理。

 

put(10, "10")

首先是put(10, "10"),因为此时TreeMap中没有任何节点,所以10为根且根节点为黑色节点,put(10, "10")以后的数据结构为:

 

put(85, "85")

接着是put(85, "85"),这一步也不难,85比10大,所以在10的右节点上,可是因为85不是根节点,所以会执行fixAfterInsertion方法进行数据修正,看一下fixAfterInsertion方法代码实现:

 1 private void fixAfterInsertion(Entry<K,V> x) {
 2     x.color = RED;
 3 
 4     while (x != null && x != root && x.parent.color == RED) {
 5         if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
 6             Entry<K,V> y = rightOf(parentOf(parentOf(x)));
 7             if (colorOf(y) == RED) {
 8                 setColor(parentOf(x), BLACK);
 9                 setColor(y, BLACK);
10                 setColor(parentOf(parentOf(x)), RED);
11                 x = parentOf(parentOf(x));
12             } else {
13                 if (x == rightOf(parentOf(x))) {
14                     x = parentOf(x);
15                     rotateLeft(x);
16                 }
17                 setColor(parentOf(x), BLACK);
18                 setColor(parentOf(parentOf(x)), RED);
19                 rotateRight(parentOf(parentOf(x)));
20             }
21         } else {
22             Entry<K,V> y = leftOf(parentOf(parentOf(x)));
23             if (colorOf(y) == RED) {
24                 setColor(parentOf(x), BLACK);
25                 setColor(y, BLACK);
26                 setColor(parentOf(parentOf(x)), RED);
27                 x = parentOf(parentOf(x));
28             } else {
29                 if (x == leftOf(parentOf(x))) {
30                     x = parentOf(x);
31                     rotateRight(x);
32                 }
33                 setColor(parentOf(x), BLACK);
34                 setColor(parentOf(parentOf(x)), RED);
35                 rotateLeft(parentOf(parentOf(x)));
36             }
37         }
38     }
39     root.color = BLACK;
40 }

咱们看第2行的代码,它将默认的插入的那个节点着色成为红色,这很好理解:

根据红黑树的性质(3),红黑树要求从根节点到叶子全部叶子节点上通过的黑色节点个数是相同的,所以若是插入的节点着色为黑色,那必然有可能致使某条路径上的黑色节点数量大于其余路径上的黑色节点数量,所以默认插入的节点必须是红色的,以此来维持红黑树的性质(3

固然插入节点着色为红色节点后,有可能致使的问题是违反性质(2),即出现连续两个红色节点,这就须要经过旋转操做去改变树的结构,解决这个问题。

接着看第4行的判断,前两个条件都知足,可是由于85这个节点的父节点是根节点的,根节点是黑色节点,所以这个条件不知足,while循环不进去,直接执行一次30行的代码给根节点着色为黑色(由于在旋转过程当中有可能致使根节点为红色,而红黑树的根节点必须是黑色,所以最后无论根节点是否是黑色,都要从新着色确保根节点是黑色的)。

那么put(85, "85")以后,整个树的结构变为:

 

fixAfterInsertion方法流程

在看put(15, "15")以前,必需要先过一下fixAfterInsertion方法。第5行~第21行的代码和第21行~第38行的代码是同样的,无非一个是操做左子树另外一个是操做右子树而已,所以就看前一半:

 1 while (x != null && x != root && x.parent.color == RED) {
 2     if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
 3         Entry<K,V> y = rightOf(parentOf(parentOf(x)));
 4         if (colorOf(y) == RED) {
 5             setColor(parentOf(x), BLACK);
 6             setColor(y, BLACK);
 7             setColor(parentOf(parentOf(x)), RED);
 8             x = parentOf(parentOf(x));
 9         } else {
10             if (x == rightOf(parentOf(x))) {
11                 x = parentOf(x);
12                 rotateLeft(x);
13             }
14             setColor(parentOf(x), BLACK);
15             setColor(parentOf(parentOf(x)), RED);
16             rotateRight(parentOf(parentOf(x)));
17         }
18     }
19     ....
20 }

第2行的判断注意一下,用语言描述出来就是:判断当前节点的父节点与当前节点的父节点的父节点的左子节点是否同一个节点。翻译一下就是:当前节点是否左子节点插入,关于这个不明白的我就不解释了,能够本身多思考一下。对这整段代码我用流程图描述一下:

这里有一个左子树内侧插入与左子树点外侧插入的概念,我用图表示一下:

其中左边的是左子树外侧插入,右边的是左子树内侧插入,能够从上面的流程图上看到,对于这两种插入方式的处理是不一样的,区别是后者也就是左子树内侧插入多一步左旋操做

能看出,红黑树的插入最多只须要进行两次旋转,至于红黑树的旋转,后面结合代码进行讲解。

 

put(15, "15")

看完fixAfterInsertion方法流程以后,继续添加数据,此次添加的是put(15, "15"),15比10大且比85小,所以15最终应当是85的左子节点,默认插入的是红色节点,所以首先将15做为红色节点插入85的左子节点后的结构应当是:

可是显然这里违反了红黑树的性质(2),即连续出现了两个红色节点,所以此时必须进行旋转。回看前面fixAfterInsertion的流程,上面演示的是左子树插入流程,右子树同样,能够看到这是右子树内侧插入,须要进行两次旋转操做:

  1. 对新插入节点的父节点进行一次右旋操做
  2. 新插入节点的父节点着色为黑色,新插入节点的祖父节点着色为红色
  3. 对新插入节点的祖父节点进行一次左旋操做

旋转是红黑树中最难理解也是最核心的操做,右旋和左旋是对称的操做,我我的的理解,以右旋为例,对某个节点x进行右旋,其实质是:

  • 下降左子树的高度,增长右子树的高度
  • 将x变为当前位置的右子节点

左旋是一样的道理,在旋转的时候必定要记住这两句话,这将会帮助咱们清楚地知道在不一样的场景下旋转如何进行。

先看一下(1)也就是"对新插入节点的父节点进行一次右旋操做",源代码为rotateRight方法:

 1 private void rotateRight(Entry<K,V> p) {
 2     if (p != null) {
 3         Entry<K,V> l = p.left;
 4         p.left = l.right;
 5         if (l.right != null) l.right.parent = p;
 6         l.parent = p.parent;
 7         if (p.parent == null)
 8            root = l;
 9         else if (p.parent.right == p)
10             p.parent.right = l;
11         else p.parent.left = l;
12         l.right = p;
13         p.parent = l;
14     }
15 }

右旋流程用流程图画一下其流程:

再用一张示例图表示一下右旋各节点的变化,旋转不会改变节点颜色,这里就不区分成色节点和黑色节点了,a是须要进行右旋的节点:

左旋与右旋是一个对称的操做,你们能够试试看把右图的b节点进行左旋,就变成了左图了。这里多说一句,旋转必定要说明是对哪一个节点进行旋转,网上看不少文章讲左旋、右旋都是直接说旋转以后怎么样怎么样,我认为脱离具体的节点讲旋转是没有任何意义的。

这里可能会有的一个问题是:b有左右两个子节点分别为d和e,为何右旋的时候要将右子节点e拿到a的左子节点而不是b的左子节点d?

一个很简单的解释是:若是将b的左子节点d拿到a的左子节点,那么b右旋后右子节点指向a,b原来的右子节点e就成为了一个游离的节点,游离于整个数据结构以外

回到实际的例子,对85这个节点进行右旋以后还有一次着色操做(2),分别是将x的父节点着色为黑色,将x的祖父节点着色为红色,那么此时的树形结构应当为:

而后对节点10进行一次左旋操做(3),左旋以后的结构为:

最后无论根节点是否是黑色,都将根节点着色为黑色,那么插入15以后的数据结构就变为了上图,知足红黑树的三条特性。

 

put(70, "70")

put(70, "70")就很简单了,70是85的左子节点,因为70的父节点以及叔父节点都是红色节点,所以直接将70的父节点8五、将70的叔父节点10着色为黑色便可,70这个节点着色为红色,即知足红黑树的特性,插入70以后的结构图为:

 

put(20, "20")

put(20, "20"),插入的位置应当是70的左子节点,默认插入红色,插入以后的结构图为:

问题很明显,出现了连续两个红色节点,20的插入位置是一种左子树外侧插入的场景,所以只须要进行着色+对节点85进行一次右旋便可,着色+右旋以后数据结构变为:

 

put(60, "60")

下面进行put(60, "60")操做,节点60插入的位置是节点20的右子节点,因为节点60的父节点与叔父节点都是红色节点,所以只须要将节点60的父节点与叔父节点着色为黑色,将节点60的组父节点着色为红色便可。

那么put(60, "60")以后的结构为:

 

put(30, "30")

put(30, "30"),节点30应当为节点60的左子节点,所以插入节点30以后应该是这样的:

显然这里违反了红黑树性质(2)即连续出现了两个红色节点,所以这里要进行旋转。

put(30, "30")的操做和put(15, "15")的操做相似,一样是右子树内侧插入的场景,那么须要进行两次旋转:

  1. 对节点30的父节点节点60进行一次右旋
  2. 右旋以后对节点60的祖父节点20进行一次左旋

右旋+着色+左旋以后,put(30, "30")的结果应当为:

 

put(50, "50")

下一个操做是put(50, "50"),节点50是节点60的左子节点,因为节点50的父亲节点与叔父节点都是红色节点,所以只须要将节点50的父亲节点与叔父节点着色为黑色,将节点50的祖父节点着色为红色便可:

节点50的父节点与叔父节点都是红色节点(注意不要被上图迷糊了!上图是从新着色以后的结构而不是从新着色以前的结构,从新着色以前的结构为上上图),所以插入节点50只须要进行着色,自己这样的操做是没有任何问题的,但问题的关键在于,着色以后出现了连续的红色节点,即节点30与节点70。这就是为何fixAfterInsertion方法的方法体是while循环的缘由:

1 private void fixAfterInsertion(Entry<K,V> x) {
2     x.color = RED;
3 
4     while (x != null && x != root && x.parent.color == RED) {
5     ...
6     }
7 }

由于这种着色方式是将插入节点的祖父节点着色为红色,所以着色以后必须将当前节点指向插入节点的祖父节点,判断祖父节点与父节点是否连续红色的节点,是就进行旋转,从新让红黑树平衡。

接下来的问题就是怎么旋转了。咱们能够把节点15-->节点70-->节点30连起来看,是否是很熟悉?这就是上面重复了两次的右子树内侧插入的场景,那么首先对节点70进行右旋,右旋后的结果为:

下一步,节点70的父节点着色为黑色,节点70的祖父节点着色为红色(这一步不理解或者忘了为何的,能够去看一下以前对于fixAfterInsertion方法的解读),从新着色后的结构为:

最后一步,对节点70的父节点节点15进行一次左旋,左旋以后的结构为:

从新恢复红黑树的性质:

  1. 根节点为黑色节点
  2. 没有连续红色节点
  3. 根节点到全部叶子节点通过的黑色节点都是2个

 

后记

本文经过不断向红黑树的右子树插入数据,演示了红黑树右侧插入时可能出现的各类状况且应当如何处理这些状况,左侧插入同理。

红黑树仍是有点难,所以我我的建议在学习红黑树的时候必定要多画(像我我的就画了3张A4纸)+多想,这样才能更好地理解红黑树的原理,尤为是旋转的原理。

TreeMap的插入操做和旋转操做已经讲完,后文会着眼于TreeMap的删除操做以及一些统计操做(好比找到节点比50大的全部节点)是如何实现的。

相关文章
相关标签/搜索