• 2-3树介绍
  • 2-3树插入元素
  • 红黑树由来(和2-3树等价)
  • 红黑树性质的理解
  • 红黑树中添加元素
  • 添加中维护红黑树的代码
  • 使用LeetCode804. Unique Morse Code Words测试红黑树

一、前言

红黑树之前,可以先看一下二叉搜索树平衡二叉树


二、2-3树介绍

  • 2节点: 容纳1个元素,有2个孩子;
  • 3节点: 容纳2个元素,有3个孩子;
  • 4节点: 容纳3个元素,有4个孩子;
  • ………

红黑树总结 - 图1
红黑树总结 - 图2

绝对平衡表示的是任意子树左右高度之差为0

三、2-3树插入元素

一个原则: 添加节点将永远不会添加到一个空的位置(和二叉搜索树不同)、只会和最后找到的叶子节点做融合。
细分为四种情况:

  • 2节点(2个孩子的节点)中添加1个元素,融合成一个3节点;
  • 3节点(3个孩子的节点)中添加1个元素,先融合成为一个4节点、如果这个4节点是根节点,直接分裂成含有三个2节点的一棵树;
  • 如果这个4节点不是根节点,就不能分裂成含有三个2节点的一棵树;因为这样会破坏2-3树的绝对平衡性。正确的做法是向上和父亲节点做新的融合,这里又分为两种情况:

    • 第一种情况: 父亲为2节点,直接融合到父亲,变成3节点;
    • 第二种情况: 父亲为3节点,先融合到父亲,变成4节点,然后父亲分散,全部变成2节点;

下图的层次关系可能会比较清楚:

红黑树总结 - 图3

看具体的细分的四种情况:
1)、最简单的情况:
红黑树总结 - 图4

2)、插入3节点且是根:

红黑树总结 - 图5

3)、插入3节点且不是根且父亲是2节点:

红黑树总结 - 图6

红黑树总结 - 图7

4)、插入3节点且不是根且父亲是3节点:

红黑树总结 - 图8

红黑树总结 - 图9

红黑树总结 - 图10


四、红黑树由来(和2-3树等价)

<<算法4>>中提到2-3树和红黑树是等价的,则其中2节点和3节点对应红黑树的关系有两种:
1)、第一种 : 2节点转换成红黑树节点 :

红黑树总结 - 图11
2)、第二种 : 3节点转换成红黑树节点

  • 由于一般树结构一个节点只表示一个值,所以要将3节点用一种特殊的方式转换,即转换成红黑树节点;
    红黑树总结 - 图12
  • 上面的摆放只是直观的摆放,我们需要将b节点作为c节点的左孩子,但是又需要去标识b和c的对应关系(是一个3节点而且b < c) 所以我们将b、c中间的线绘制成红色;并且还原成二分搜索树的样子:
    红黑树总结 - 图13
  • 但是在二叉树的实现中没有使用相应的类来表示那条边是红色的,所以我们使用节点来表示这个特性,由于每一个节点只有一个父亲,也就是每一个节点和它父亲相连的边只有一条边,所以我们可以把这条边(红色)的信息存放在结点上,也就是把b描绘成红色,所以这就是红色节点的由来:这个红色的节点b表示的是这个节点之前在2-3树中是在一个3节点中,而且和它的父亲是存放在一起的(并列),而且它的值比它父亲小。
    红黑树总结 - 图14

然后再看一个2-3树和红黑树转换的例子:

红黑树总结 - 图15

更加直观的看:

红黑树总结 - 图16


五、红黑树性质的理解

红黑树总结 - 图17

  • 第一条: 这个很简单,是红黑树的定义;
  • 第二条: 由于在2-3树中根节点或者是2节点,或者是3节点,这两种节点转换成红黑树节点之后根节点都是黑色的
    红黑树总结 - 图18
  • 第三条: 与其说是性质,不如说是定义,不过要注意这里的叶子节点指的是最后的空节点,都指定为黑色的节点,如果是空树,则同时满足2、3条性质
  • 第四条: 也是和2-3树密切关系,一个红色节点,在2-3树中是3节点的左边节点,它的左右孩子在2-3树中都要么是2节点,要么是3节点,但是他们的根都是黑色的
    红黑树总结 - 图19
  • 第五条: 是最重要的一条性质,最主要的原因是2-3树是绝对平衡的,如果途中经过的是一个3节点也不要紧,反正都是有一个黑色节点,所以经过的黑色节点的数量是一样多的;所以红黑树是保持”黑平衡”的二叉树,本质就是2-3树是绝对平衡的;

红黑树总结 - 图20


六、红黑树中添加元素

一个原则: 新插入的结点永远是红色

红黑树总结 - 图21

有几个子过程步骤以及相关需要维护的地方

  • 第一个:维护根节点为黑色,因为我们第二条性质是根节点永远为黑色,所以我们在插入完节点之后,要维护根节点为黑色;
    1)、插入: 红黑树总结 - 图22
    2)、更一般的情况对于根的维护:
    红黑树总结 - 图23

  • 第二个:也是插入的第二种情况,这个很简单,也不需要维护,直接插入就好 :
    红黑树总结 - 图24

  • 第三个:也是插入的第三种情况,也就是待插入的红色结点比根节点大(插在右边),这个需要维护,和AVL中一样,类似RR型,需要进行左旋转:
    红黑树总结 - 图25

过程如下:

红黑树总结 - 图26

但是这样还不够,我们还需要维护颜色: 此时x成为了新的根节点,所以x.color = node.color(x颜色变成之前根节点的颜色); 而node.color = RED,因为形成的还是3节点:(注意之前的node.color有可能是红色,但是后续还要处理 )

红黑树总结 - 图27

代码:

  1. /**左旋转
  2. node x
  3. / \ --> / \
  4. T1 x node T3
  5. / \ / \
  6. T2 T3 T1 T2
  7. */
  8. private Node leftRotate(Node node){
  9. Node x = node.right;
  10. Node T2 = x.left;
  11. x.left = node;
  12. node.right = T2;
  13. //update color
  14. x.color = node.color;
  15. node.color = RED;
  16. return x;
  17. }
  • 第四个: 也是插入的第四种情况,也是另一种维护情况:>颜色反转, 在根节点的右侧插入: 其实相当于在2-3树中的3节点中插入元素,然后分散:
    红黑树总结 - 图28

在2-3树中散开之后都成为了2节点,所以需要变成全都是黑色节点:

红黑树总结 - 图29

最后融合,根节点又需要变回红色,因为这个根节点要向上去和它的父亲节点去融合:

红黑树总结 - 图30

整体如下图 (需要经过两次换色)

红黑树总结 - 图31

代码:

  1. //三个节点的颜色反转 根->RED 两个孩子->BLACK
  2. private void flipColors(Node node){
  3. node.color = RED;
  4. node.left.color = BLACK;
  5. node.right.color = BLACK;
  6. }
  • 第五个:也是插入的第五种情况,在根节点的左边的左边,也就是向2-3树中的3结点的左边添加元素:

红黑树总结 - 图32

等价于在2-3树中这样插入:
红黑树总结 - 图33

然后需要进行右旋转:
红黑树总结 - 图34

  1. /**右旋转
  2. node x
  3. / \ / \
  4. x T1 --> y node
  5. / \ / \ / \
  6. y T2 T4 T3 T2 T1
  7. / \
  8. T4 T3
  9. */
  10. private Node rightRotate(Node node){
  11. Node x = node.left;
  12. Node T2 = x.right;
  13. x.right = node;
  14. node.left = T2;
  15. //update color
  16. x.color = node.color;
  17. node.color = RED;
  18. return x;
  19. }

然后又需要维护颜色(前两部同左旋转,最后一步是颜色翻转(filpColors))

红黑树总结 - 图35

  • 第六个: 在根节点的左孩子的右边添加,类似2-3树中添加在3节点的中间,这个只需要运用前面的那些步骤综合即可完成:

红黑树总结 - 图36

a)、先对37进行左旋转 :

红黑树总结 - 图37

b)、对上面的结果进行右旋转,并且调整颜色 :

红黑树总结 - 图38

c)、最后进行之前的颜色翻转(filpColors):

红黑树总结 - 图39

  • 总结 : 所以我们发现,其实就是三种基本操作: 左旋转,右旋转,颜色翻转的结合

红黑树总结 - 图40

所以这是一个维护链,放在add中代码如下:

  1. /** 向红黑树中添加新的元素(key, value) */
  2. public void add(K key, V value) {//相当于JDK的put操作
  3. root = add(root, key, value);
  4. root.color = BLACK; /**子过程一: 保持最终根节点为黑色 --> 第二条性质 */
  5. }
  6. // 向以node为根的红黑树树中插入元素(key, value),递归算法,返回插入新节点后红黑树树的根
  7. private Node add(Node node, K key, V value) {
  8. if (node == null) {
  9. size++;
  10. return new Node(key, value);
  11. }
  12. if (key.compareTo(node.key) < 0)
  13. node.left = add(node.left, key, value);
  14. else if (key.compareTo(node.key) > 0)
  15. node.right = add(node.right, key, value);
  16. else // key.compareTo(node.key) == 0
  17. node.value = value;
  18. /**维护链: 注意三个if不是互斥的 就是一个维护链的关系 */
  19. if(isRed(node.right) && !isRed(node.left)){ //右红,左黑 -> 左旋
  20. node = leftRotate(node);
  21. }
  22. if(isRed(node.left) && isRed(node.left.left)){ //左红 左左红 -> 右旋
  23. node = rightRotate(node);
  24. }
  25. if(isRed(node.left) && isRed(node.right)){//左红右红自己黑 -->颜色翻转
  26. flipColors(node);
  27. }
  28. return node;
  29. }

重点在这段代码,就是三种基本的维护组成的维护链:

  1. /**维护链: 注意三个if不是互斥的 就是一个维护链的关系 */
  2. if(isRed(node.right) && !isRed(node.left)){ //右红,左黑 -> 左旋
  3. node = leftRotate(node);
  4. }
  5. if(isRed(node.left) && isRed(node.left.left)){ //左红 左左红 -> 右旋
  6. node = rightRotate(node);
  7. }
  8. if(isRed(node.left) && isRed(node.right)){//左红右红自己黑 -->颜色翻转
  9. flipColors(node);
  10. }

七、添加中维护红黑树的代码

这里完成了左倾红黑树的add操作 : ,没有实现remove(),完整测试源码如下:

  1. package DataStructure.Tree.RB;
  2. import java.util.Arrays;
  3. /**
  4. * 红黑树实现 BSTMap中拷贝过来的
  5. */
  6. public class RBTree<K extends Comparable<K>, V> {
  7. private final static boolean RED = true;
  8. private final static boolean BLACK = false;
  9. private class Node {
  10. public K key;
  11. public V value;
  12. public Node left, right;
  13. public boolean color;
  14. public Node(K key, V value) {
  15. this.key = key;
  16. this.value = value;
  17. left = null;
  18. right = null;
  19. // 一开始添加的时候都是红结点
  20. color = RED; //为什么一开始是红色,因为在2-3结点中添加一个结点,永远是和叶子结点进行融合,所以是3节点,所以设置成红色
  21. }
  22. }
  23. private Node root;
  24. private int size;
  25. public RBTree() {
  26. root = null;
  27. size = 0;
  28. }
  29. public int getSize() {
  30. return size;
  31. }
  32. public boolean isEmpty() {
  33. return size == 0;
  34. }
  35. /** 判断节点node的颜色 */
  36. private boolean isRed(Node node){
  37. if(node == null) return BLACK;
  38. return node.color;
  39. }
  40. /**左旋转
  41. node x
  42. / \ --> / \
  43. T1 x node T3
  44. / \ / \
  45. T2 T3 T1 T2
  46. */
  47. private Node leftRotate(Node node){
  48. Node x = node.right;
  49. Node T2 = x.left;
  50. x.left = node;
  51. node.right = T2;
  52. //update color
  53. x.color = node.color;
  54. node.color = RED;
  55. return x;
  56. }
  57. /**右旋转
  58. node x
  59. / \ / \
  60. x T1 --> y node
  61. / \ / \ / \
  62. y T2 T4 T3 T2 T1
  63. / \
  64. T4 T3
  65. */
  66. private Node rightRotate(Node node){
  67. Node x = node.left;
  68. Node T2 = x.right;
  69. x.right = node;
  70. node.left = T2;
  71. //update color
  72. x.color = node.color;
  73. node.color = RED;
  74. return x;
  75. }
  76. /**三个节点的颜色反转 根->RED 两个孩子->BLACK */
  77. private void flipColors(Node node){
  78. node.color = RED;
  79. node.left.color = BLACK;
  80. node.right.color = BLACK;
  81. }
  82. /** 向红黑树中添加新的元素(key, value) */
  83. public void add(K key, V value) {//相当于JDK的put操作
  84. root = add(root, key, value);
  85. root.color = BLACK; /**子过程一: 保持最终根节点为黑色 --> 第二条性质 */
  86. }
  87. // 向以node为根的红黑树树中插入元素(key, value),递归算法,返回插入新节点后红黑树树的根
  88. private Node add(Node node, K key, V value) {
  89. if (node == null) {
  90. size++;
  91. return new Node(key, value);
  92. }
  93. if (key.compareTo(node.key) < 0)
  94. node.left = add(node.left, key, value);
  95. else if (key.compareTo(node.key) > 0)
  96. node.right = add(node.right, key, value);
  97. else // key.compareTo(node.key) == 0
  98. node.value = value;
  99. /**维护链: 注意三个if不是互斥的 就是一个维护链的关系 */
  100. if(isRed(node.right) && !isRed(node.left)){ //右红,左黑 -> 左旋
  101. node = leftRotate(node);
  102. }
  103. if(isRed(node.left) && isRed(node.left.left)){ //左红 左左红 -> 右旋
  104. node = rightRotate(node);
  105. }
  106. if(isRed(node.left) && isRed(node.right)){
  107. flipColors(node);
  108. }
  109. return node;
  110. }
  111. // 返回以node为根节点的红黑树中,key所在的节点
  112. private Node getNode(Node node, K key) {
  113. if (node == null)
  114. return null;
  115. if (key.equals(node.key))
  116. return node;
  117. else if (key.compareTo(node.key) < 0)
  118. return getNode(node.left, key);
  119. else // if(key.compareTo(node.key) > 0)
  120. return getNode(node.right, key);
  121. }
  122. public boolean contains(K key) {
  123. return getNode(root, key) != null;
  124. }
  125. public V get(K key) {
  126. Node node = getNode(root, key);
  127. return node == null ? null : node.value;
  128. }
  129. public void set(K key, V newValue) {
  130. Node node = getNode(root, key);
  131. if (node == null)
  132. throw new IllegalArgumentException(key + " doesn't exist!");
  133. node.value = newValue;
  134. }
  135. // 返回以node为根的红黑树的最小值所在的节点
  136. private Node minimum(Node node) {
  137. if (node.left == null)
  138. return node;
  139. return minimum(node.left);
  140. }
  141. // 删除掉以node为根的红黑树中的最小节点,返回删除节点后新的红黑树的根
  142. private Node removeMin(Node node) {
  143. if (node.left == null) {
  144. Node rightNode = node.right;
  145. node.right = null;
  146. size--;
  147. return rightNode;
  148. }
  149. node.left = removeMin(node.left);
  150. return node;
  151. }
  152. //没有实现remove中的调整
  153. public V remove(K key) {
  154. throw new UnsupportedOperationException("No remove in RBTree!");
  155. }
  156. public void printTree(){
  157. printTree(root,0,"H",8);
  158. }
  159. public void printTree(Node head,int height,String to,int len){
  160. if(head == null)return;
  161. printTree(head.right,height + 1,"v",len);
  162. String val = to + head.key + to; //两边指示的字符
  163. int lenV = val.length();
  164. int lenL = (len - lenV)/2; //左边的空格(分一半)
  165. int lenR = len - lenV - lenL; // 右边的空格
  166. System.out.println( getSpace(len * height) + getSpace(lenL) + val + getSpace(lenR));
  167. printTree(head.left,height + 1,"^",len);
  168. }
  169. public static String getSpace(int len){
  170. StringBuffer str = new StringBuffer();
  171. for(int i = 0; i < len; i++) str.append(" ");
  172. return str.toString();
  173. }
  174. public static void main(String[] args) {
  175. Integer[] arr = {42,33,50,17,37,48,88,12,18,66,6};
  176. // Arrays.sort(arr); //排序后插入也会保持平衡 但是BST会退化成链表
  177. RBTree<Integer,Integer>rbTree = new RBTree<>();
  178. for(int i = 0; i < arr.length; i++){
  179. rbTree.add(arr[i],null);
  180. }
  181. rbTree.printTree();
  182. }
  183. }

测试结果:
红黑树总结 - 图41

和这颗红黑树是一样的:

红黑树总结 - 图42

二叉树打印见这里


八、使用LeetCode804. Unique Morse Code Words测试红黑树

题目链接

题目

红黑树总结 - 图43

解析

题目很简单,就是要你解析字符串之后看有多少种不同的翻译,使用HashSet和TreeSet都可以做,这里用RBTree改装的Set来测试(类似JDK中的TreeSet):

  1. class Solution {
  2. public int uniqueMorseRepresentations(String[] words) {
  3. String[] codes = {".-","-...","-.-.","-..",".","..-.","--.","....","..",".---","-.-",".-..","--","-.","---",".--.","--.-",".-.","...","-","..-","...-",".--","-..-","-.--","--.."};
  4. RBTree<String, Object> set = new RBTree<>();
  5. for(String word: words){
  6. StringBuilder res = new StringBuilder();
  7. for(int i = 0 ; i < word.length() ; i ++)
  8. res.append(codes[word.charAt(i) - 'a']);
  9. set.add(res.toString(), null);
  10. }
  11. return set.getSize();
  12. }
  13. private class RBTree<K extends Comparable<K>, V> {
  14. private final static boolean RED = true;
  15. private final static boolean BLACK = false;
  16. private class Node {
  17. public K key;
  18. public V value;
  19. public Node left, right;
  20. public boolean color;
  21. public Node(K key, V value) {
  22. this.key = key;
  23. this.value = value;
  24. left = null;
  25. right = null;
  26. // 一开始添加的时候都是红结点
  27. color = RED; //为什么一开始是红色,因为在2-3结点中添加一个结点,永远是和叶子结点进行融合,所以是3节点,所以设置成红色
  28. }
  29. }
  30. private Node root;
  31. private int size;
  32. public RBTree() {
  33. root = null;
  34. size = 0;
  35. }
  36. public int getSize() {
  37. return size;
  38. }
  39. public boolean isEmpty() {
  40. return size == 0;
  41. }
  42. /** 判断节点node的颜色 */
  43. private boolean isRed(Node node){
  44. if(node == null) return BLACK;
  45. return node.color;
  46. }
  47. /**左旋转
  48. node x
  49. / \ --> / \
  50. T1 x node T3
  51. / \ / \
  52. T2 T3 T1 T2
  53. */
  54. private Node leftRotate(Node node){
  55. Node x = node.right;
  56. Node T2 = x.left;
  57. x.left = node;
  58. node.right = T2;
  59. //update color
  60. x.color = node.color;
  61. node.color = RED;
  62. return x;
  63. }
  64. /**右旋转
  65. node x
  66. / \ / \
  67. x T1 --> y node
  68. / \ / \ / \
  69. y T2 T4 T3 T2 T1
  70. / \
  71. T4 T3
  72. */
  73. private Node rightRotate(Node node){
  74. Node x = node.left;
  75. Node T2 = x.right;
  76. x.right = node;
  77. node.left = T2;
  78. //update color
  79. x.color = node.color;
  80. node.color = RED;
  81. return x;
  82. }
  83. /**三个节点的颜色反转 根->RED 两个孩子->BLACK */
  84. private void flipColors(Node node){
  85. node.color = RED;
  86. node.left.color = BLACK;
  87. node.right.color = BLACK;
  88. }
  89. /** 向红黑树中添加新的元素(key, value) */
  90. public void add(K key, V value) {//相当于JDK的put操作
  91. root = add(root, key, value);
  92. root.color = BLACK; /**子过程一: 保持最终根节点为黑色 --> 第二条性质 */
  93. }
  94. // 向以node为根的红黑树树中插入元素(key, value),递归算法,返回插入新节点后红黑树树的根
  95. private Node add(Node node, K key, V value) {
  96. if (node == null) {
  97. size++;
  98. return new Node(key, value);
  99. }
  100. if (key.compareTo(node.key) < 0)
  101. node.left = add(node.left, key, value);
  102. else if (key.compareTo(node.key) > 0)
  103. node.right = add(node.right, key, value);
  104. else // key.compareTo(node.key) == 0
  105. node.value = value;
  106. /**维护链: 注意三个if不是互斥的 就是一个维护链的关系 */
  107. if(isRed(node.right) && !isRed(node.left)){ //右红,左黑 -> 左旋
  108. node = leftRotate(node);
  109. }
  110. if(isRed(node.left) && isRed(node.left.left)){ //左红 左左红 -> 右旋
  111. node = rightRotate(node);
  112. }
  113. if(isRed(node.left) && isRed(node.right)){
  114. flipColors(node);
  115. }
  116. return node;
  117. }
  118. // 返回以node为根节点的红黑树中,key所在的节点
  119. private Node getNode(Node node, K key) {
  120. if (node == null)
  121. return null;
  122. if (key.equals(node.key))
  123. return node;
  124. else if (key.compareTo(node.key) < 0)
  125. return getNode(node.left, key);
  126. else // if(key.compareTo(node.key) > 0)
  127. return getNode(node.right, key);
  128. }
  129. public boolean contains(K key) {
  130. return getNode(root, key) != null;
  131. }
  132. public V get(K key) {
  133. Node node = getNode(root, key);
  134. return node == null ? null : node.value;
  135. }
  136. public void set(K key, V newValue) {
  137. Node node = getNode(root, key);
  138. if (node == null)
  139. throw new IllegalArgumentException(key + " doesn't exist!");
  140. node.value = newValue;
  141. }
  142. // 返回以node为根的红黑树的最小值所在的节点
  143. private Node minimum(Node node) {
  144. if (node.left == null)
  145. return node;
  146. return minimum(node.left);
  147. }
  148. // 删除掉以node为根的红黑树中的最小节点,返回删除节点后新的红黑树的根
  149. private Node removeMin(Node node) {
  150. if (node.left == null) {
  151. Node rightNode = node.right;
  152. node.right = null;
  153. size--;
  154. return rightNode;
  155. }
  156. node.left = removeMin(node.left);
  157. return node;
  158. }
  159. //没有实现remove中的调整
  160. public V remove(K key) {
  161. throw new UnsupportedOperationException("No remove in RBTree!");
  162. }
  163. }
  164. }