红黑树是平衡二叉查找树的一种。为了深入理解红黑树,我们需要从二叉查找树开始讲起。

BST

二叉查找树(Binary Search Tree,简称BST)是一棵二叉树,它的左子节点的值比父节点的值要小,右节点的值要比父节点的值大。它的高度决定了它的查找效率。

在理想的情况下,二叉查找树增删查改的时间复杂度为O(logN)(其中N为节点数),最坏的情况下为O(N)。

BST存在倾斜的问题
平衡的BST:
image.png

倾斜的BST:
image.png

  1. public class BstTest {
  2. static class Node {
  3. public String content;
  4. public Node parent;
  5. public Node left;
  6. public Node right;
  7. public Node(String content) {
  8. this.content = content;
  9. }
  10. }
  11. public Node root;
  12. // BST的查找操作
  13. public Node search (String content) {
  14. Node r = root;
  15. while (r != null) {
  16. if (r.content.equals(content)) {
  17. return r;
  18. } else if (content.compareTo(r.content) > 1) {
  19. r = r.right;
  20. } else if (content.compareTo(r.content) <= 1) {
  21. r = r.left;
  22. }
  23. }
  24. return null;
  25. }
  26. // BST的插入操作
  27. public void insert (String content) {
  28. Node newNode = new Node(content);
  29. Node r = root;
  30. Node parent = null;
  31. if (r == null) {
  32. root = newNode;
  33. return;
  34. }
  35. while (r != null) {
  36. parent = r;
  37. if (newNode.content.compareTo(r.content) > 1) {
  38. r = r.right;
  39. } else if (newNode.content.compareTo(r.content) < 1){
  40. r = r.left;
  41. } else {
  42. r = r.left;
  43. }
  44. }
  45. if (parent.content.compareTo(newNode.content) > 1) {
  46. parent.left = newNode;
  47. newNode.parent = parent;
  48. } else {
  49. parent.right = newNode;
  50. newNode.parent = parent;
  51. }
  52. }
  53. }

红黑树-RBTree

基于BST存在的问题,一种新的树——平衡二叉查找树(Balanced BST)产生了。平衡树在插入和删除的时候,会通过旋转操作将高度保持在logN。其中两款具有代表性的平衡树分别为AVL树和红黑树。AVL树由于实现比较复杂,而且插入和删除性能差,在实际环境下的应用不如红黑树。

红黑树(Red-Black Tree,以下简称RBTree)的实际应用非常广泛,比如Linux内核中的完全公平调度器、高精度计时器、ext3文件系统等等,各种语言的函数库如Java的TreeMap和TreeSet,C++ STL的map、multimap、multiset等。

RBTree也是函数式语言中最常用的持久数据结构之一,在计算几何中也有重要作用。值得一提的是,Java 8中HashMap的实现也因为用RBTree取代链表,性能有所提升。

《算法导论》中对于红黑树的定义如下:

  1. 每个结点或是红的,或是黑的
  2. 根节点是黑的
  3. 每个叶结点是黑的
  4. 如果一个结点是红的,则它的两个儿子都是黑的
  5. 对每个结点,从该结点到其子孙节点的所有路径上包含相同数目的黑结点

对与第4点,网上有些定义是:父子节点之间不能出现两个连续的红节点,这种定义和《算法导论》中定义的效果是一样的

RBTree在理论上还是一棵BST树,但是它在对BST的插入和删除操作时会维持树的平衡,即保证树的高度在[logN,logN+1](理论上,极端的情况下可以出现RBTree的高度达到2*logN,但实际上很难遇到)。这样RBTree的查找时间复杂度始终保持在O(logN)从而接近于理想的BST。RBTree的删除和插入操作的时间复杂度也是O(logN)。RBTree的查找操作就是BST的查找操作。

插入数据

向红黑树中插入新的结点。具体做法是,将新结点的 color 赋为红色,然后以BST的插入方法插入到红黑树中去。之所以将新插入的结点的颜色赋为红色,是因为:如果设为黑色,就会导致根到叶子的路径上有一条路上,多一个额外的黑结点,这个是很难调整的。但是设为红色结点后,可能会导致出现两个连续红色结点的冲突,那么可以通过颜色调换和树旋转来调整,这样简单多了。

接下来,讨论一下插入以后,红黑树的情况。设要插入的结点为N,其父结点为P,其 祖父结点为G,其父亲的兄弟结点为U(即P和U 是同一个结点的两个子结点)。如果P是黑色的,则整棵树不必调整就已经满足了红黑树的所有性质。如果P是红色的(可知,其父结点G一定是黑色的),则插入N后,违背了红色结点只能有黑色孩子的性质,需要进行调整。

调整时分以下三种情况:

新结点N的叔叔结点U是红色的

处理方式是:将P和U修改为黑色,G修改为红色

现在新结点N有了一个黑色的父结点P,因为通过父结点P或叔父结点U的任何路径都必定通过祖父结点G,在这些路径上的黑结点数目没有改变。

但是,红色的祖父结点G的父结点也有可能是红色的,这就违反了性质3。为了解决这个问题,我们从祖父结点G开始递归向上调整颜色。如图2红黑树笔记 - 图3

新结点N的叔叔结点U是黑色的,且N是左孩子。

处理方式:对祖父结点G进行一次右旋转

在旋转后产生的树中,以前的父结点P现在是新结点N和以前的祖父节点G的父结点,然后交换以前的父结点P和祖父结点G的颜色,结果仍满足红黑树性质。如图 2.15。在(b)中,虚线代表原来的指针,实线代表旋转过后的指针。所谓旋转就是改变图中所示的两个指针的值即可。当然,在实际应用中,还有父指针p也需要修改,这里为了图示的简洁而省略掉了。
红黑树笔记 - 图4

新结点N的叔叔结点U是黑色的,且N是右孩子。

处理方式:对P进行一次左旋转,就把问题转化成了第二种情况。如图 2.16所示。
红黑树笔记 - 图5

红黑树插入数据的代码与二叉查找树是相同的,只是在插入以后,会对不满足红黑树性质的结点进行调整。

HashMap中红黑树的插入操作

  1. static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
  2. TreeNode<K,V> x) {
  3. // 新节点默认为红色
  4. x.red = true;
  5. // xp表示x的父结点,xpp表示x的祖父结点,xppl表示xpp的左孩子结点,xppr表示xpp的右孩子结点
  6. for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
  7. // 如果x没有父结点,则表示x是第一个结点,自动为根节点,根节点为黑色
  8. if ((xp = x.parent) == null) {
  9. x.red = false;
  10. return x;
  11. }
  12. // 如果父结点不是红色(就是黑色),或者x没有祖父节点,那么就证明x是第二层节点,父节点为根节点
  13. // 这种情况无需就行操作
  14. else if (!xp.red || (xpp = xp.parent) == null)
  15. return root;
  16. // 进入到这里,表示x的父节点为红色
  17. // 如果x的父节点是祖父结点的左孩子
  18. if (xp == (xppl = xpp.left)) {
  19. // 祖父结点的右孩子,也就是x的叔叔节点不为空,且为红色
  20. if ((xppr = xpp.right) != null && xppr.red) {
  21. // 父节点和叔叔节点都为红色,只需要变色,且将x替换为祖父节点然后进行递归
  22. xppr.red = false;
  23. xp.red = false;
  24. xpp.red = true;
  25. x = xpp;
  26. }
  27. // 如果叔叔节点为空,或者为黑色
  28. else {
  29. // 如果x节点为xp的右孩子
  30. if (x == xp.right) {
  31. // 先进行左旋,并且把x替换为xp进行递归,在左旋的过程中产生了新的root节点
  32. root = rotateLeft(root, x = xp);
  33. // x替换后,修改xp和xpp
  34. xpp = (xp = x.parent) == null ? null : xp.parent;
  35. }
  36. // 如果x本来是左孩子,或者已经经过了上面的左旋之后,进行变色加右旋
  37. if (xp != null) {
  38. xp.red = false;
  39. if (xpp != null) {
  40. xpp.red = true;
  41. root = rotateRight(root, xpp);
  42. }
  43. }
  44. }
  45. }
  46. // 如果x的父节点是祖父结点的右孩子
  47. else {
  48. if (xppl != null && xppl.red) {
  49. xppl.red = false;
  50. xp.red = false;
  51. xpp.red = true;
  52. x = xpp;
  53. }
  54. else {
  55. if (x == xp.left) {
  56. root = rotateRight(root, x = xp);
  57. xpp = (xp = x.parent) == null ? null : xp.parent;
  58. }
  59. if (xp != null) {
  60. xp.red = false;
  61. if (xpp != null) {
  62. xpp.red = true;
  63. root = rotateLeft(root, xpp);
  64. }
  65. }
  66. }
  67. }
  68. }
  69. }

HashMap中红黑树的左右旋操作

  1. static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
  2. TreeNode<K,V> p) {
  3. // pp是祖父结点
  4. // p是待旋转结点
  5. // r是p的右孩子结点
  6. // rl是r的左孩子结点
  7. TreeNode<K,V> r, pp, rl;
  8. if (p != null && (r = p.right) != null) {
  9. // 如果rl不为空,则设置p.right=rl
  10. if ((rl = p.right = r.left) != null)
  11. rl.parent = p;
  12. // 如果祖父结点为null,那么r设置为黑色,r左旋之后即为root节点
  13. if ((pp = r.parent = p.parent) == null)
  14. (root = r).red = false;
  15. // 如果待旋转结点是左孩子节点
  16. else if (pp.left == p)
  17. pp.left = r;
  18. // 如果待旋转结点为右孩子
  19. else
  20. pp.right = r;
  21. r.left = p;
  22. p.parent = r;
  23. }
  24. return root;
  25. }
  26. static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
  27. TreeNode<K,V> p) {
  28. TreeNode<K,V> l, pp, lr;
  29. if (p != null && (l = p.left) != null) {
  30. if ((lr = p.left = l.right) != null)
  31. lr.parent = p;
  32. if ((pp = l.parent = p.parent) == null)
  33. (root = l).red = false;
  34. else if (pp.right == p)
  35. pp.right = l;
  36. else
  37. pp.left = l;
  38. l.right = p;
  39. p.parent = l;
  40. }
  41. return root;
  42. }

HashMap中的树化

  1. final void treeify(Node<K,V>[] tab) {
  2. TreeNode<K,V> root = null;
  3. // 遍历当前链表
  4. for (TreeNode<K,V> x = this, next; x != null; x = next) {
  5. next = (TreeNode<K,V>)x.next;
  6. x.left = x.right = null;
  7. if (root == null) {
  8. x.parent = null;
  9. x.red = false;
  10. root = x;
  11. }
  12. else {
  13. K k = x.key;
  14. int h = x.hash;
  15. Class<?> kc = null;
  16. // 每遍历一个链表上的元素就插入到红黑树中
  17. for (TreeNode<K,V> p = root;;) {
  18. int dir, ph;
  19. K pk = p.key;
  20. // 判断待插入结点应该插入在左子树还是右子树
  21. // 先比较hash值
  22. if ((ph = p.hash) > h)
  23. dir = -1;
  24. else if (ph < h)
  25. dir = 1;
  26. // 如果hash值相等,然后比较k.compareTo(pk)
  27. else if ((kc == null &&
  28. (kc = comparableClassFor(k)) == null) ||
  29. (dir = compareComparables(kc, k, pk)) == 0)
  30. // 如果还相等则再比较identityHashCode
  31. dir = tieBreakOrder(k, pk);
  32. // 根据dir的值就知道了待插入结点该插在左子树还是右子树了
  33. TreeNode<K,V> xp = p;
  34. if ((p = (dir <= 0) ? p.left : p.right) == null) {
  35. x.parent = xp;
  36. if (dir <= 0)
  37. xp.left = x;
  38. else
  39. xp.right = x;
  40. root = balanceInsertion(root, x);
  41. break;
  42. }
  43. }
  44. }
  45. }
  46. moveRootToFront(tab, root);
  47. }