引言
上篇文章介绍了红黑树的性质,本篇文章将继续讲解红黑树的插入操作。
基本属性
这里我们先把红黑树的大体属性以 TreeMap 为原型列举出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 public class RBTree <K extends Comparable <K >, V > { private Entry<K, V> root; private static final boolean RED = false ; private static final boolean BLACK = true ; private static class Entry <K , V > { private K k; private V v; private Entry<K,V> parent; private Entry<K,V> left; private Entry<K,V> right; boolean color = BLACK; public Entry (K k, V v, Entry<K, V> parent) { this .k = k; this .v = v; this .parent = parent; } public V setValue (V v) { V old = this .v; this .v = v; return old; } } private static <K,V> Entry<K,V> parentOf (Entry<K,V> p) { return (p == null ? null : p.parent); } private static <K,V> Entry<K,V> leftOf (Entry<K,V> p) { return (p == null ) ? null : p.left; } private static <K,V> Entry<K,V> rightOf (Entry<K,V> p) { return (p == null ) ? null : p.right; } private static <K,V> boolean colorOf (Entry<K,V> p) { return (p == null ? BLACK : p.color); } }
常规操作 变色
节点的颜色由黑变红或者由红变黑。
1 2 3 4 5 6 7 private static <K,V> void setColor (Entry<K,V> p, boolean c) { if (p != null ) p.color = c; }
左旋
以某个节点作为旋转点,其右子节点变为旋转节点的父节点,右子节点的左子节点变为旋转节点的右子节点,左子节点保持不变。
根据上图,我们看下左旋的代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 private void rotateLeft (Entry<K, V> p) { if (p != null ) { Entry<K, V> r = p.right; p.right = r.left; if (r.left != null ) r.left.parent = p; r.parent = p.parent; if (p.parent == null ) root = r; else if (p.parent.left == p) p.parent.left = r; else p.parent.right = r; r.left = p; p.parent = r; } }
整体逻辑比较清晰,唯一要注意的一点是,红黑树的各个节点之间是双向指针,在更改左右节点的同时还要更新其父节点的指向。
右旋
以某个节点作为旋转点,其左子节点变为旋转节点的父节点,左子节点的右子节点变为旋转节点的左子节点,右子节点保持不变。
有了左旋的基础,我们在分析右旋就简单多了,只要把左旋中的方向左右兑换就行了。但是我们为了加深理解,还是在捋一遍吧!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 private void rotateRight (Entry<K, V> p) { if (p != null ) { Entry<K, V> l = p.left; p.left = l.right; if (l.right != null ) l.right.parent = p; l.parent = p.parent; if (p.parent == null ) root = l; else if (p.parent.left == p) p.parent.left = l; else p.parent.right = l; l.right = p; p.parent = l; } }
红黑树的插入 2-3-4 树的插入
要想理解红黑树的插入,我们首先要明白 2-3-4 树的插入操作。
2-3-4 树的插入一定是从叶子开始的,也就是说它是一棵自底向上增长的树。当叶子满足合并条件时,会进行合并。如果不满足条件时,中间的元素会向上增长。我们根据下图来回顾一下 2-3-4 树的增长过程:
还记得上篇文章中介绍的红黑树与 2-3-4 树的等价关系吗?我们搞懂了 2-3-4 树的插入逻辑,那么我们把红黑树的插入逻辑也以图例的形式展现出来(有一点需要特别注意,红黑树的插入节点都是红色的):
只要我们记住红黑树与 2-3-4 树的等价关系,理解红黑树的插入就简单多了。
元素插入
接下来,我们介绍插入的实现。此方法只是简单的将元素插入到指定的位置,对于树的调整,我们稍后介绍。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public V put (K key, V value) { if (key == null ) throw new NullPointerException(); Entry<K, V> t = root; if (t == null ) { root = new Entry<>(key, value, null ); return null ; } int cmp; Entry<K, V> parent; do { parent = t; cmp = key.compareTo(t.k); if (cmp > 0 ) t = t.right; else if (cmp < 0 ) t = t.left; else return t.setValue(value); } while (t != null ); Entry<K, V> newEntry = new Entry<>(key, value, parent); if (cmp > 0 ) parent.right = newEntry; else parent.left = newEntry; fixAfterInsertion(newEntry); return null ; }
红黑树的插入操作就介绍完了,从代码上来看没有什么复杂性。而复杂的核心就在 fixAfterInsertion(newEntry)
方法了。不过不要着急,我们马上开始!
红黑树的调整
在讲代码之前,我们需要先弄清楚需要调整的场景,只有把逻辑捋顺了,看代码才不那么抽象。
3 节点调整
4 节点调整
了解了需要调整的场景之后,我们马上进入代码环节:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 private void fixAfterInsertion (Entry<K, V> x) { x.color = RED; while (x != null && x != root && x.parent.color == RED) { if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { Entry<K, V> y = rightOf(parentOf(parentOf(x))); if (colorOf(y) == RED) { setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); setColor(y, BLACK); x = parentOf(parentOf(x)); } else { if (x == rightOf(parentOf(x))) { x = parentOf(x); rotateLeft(x); } setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateRight(parentOf(parentOf(x))); } } else { Entry<K, V> y = leftOf(parentOf(parentOf(x))); if (colorOf(y) == RED) { setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); setColor(y, BLACK); x = parentOf(parentOf(x)); } else { if (x == leftOf(parentOf(x))) { x = parentOf(x); rotateRight(x); } setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateLeft(parentOf(parentOf(x))); } } } root.color = BLACK; }
根据注释中的各种场景,在理解代码就简单很多了。
尾声 至此,红黑树的插入操作就介绍完了。整个插入过程还是比较简单的,通过迭代确定好插入的位置,然后进行指向。难点其实在于插入之后的平衡调节,相比 AVL 树,由于红黑树属于黑色平衡的弱平衡,其实只有在 3 节点的场景中才会涉及到旋转。4 节点的分裂也只是单纯的染色而已。另外需要注意的一点是,红黑树所有的插入节点一定是红色,同时节点之间是双向指针。在旋转变换时,一定要记得修改!
下一篇文章就是红黑树系列的最后一篇【删除】了,也是整个红黑树中最难的部分,不过不要紧,我还是以最简单的方式为大家呈现出来。好,我们下篇见!