理解红黑树(上)插入操作

来源:互联网 发布:淘宝神器返利是真的吗 编辑:程序博客网 时间:2024/06/08 01:17

1. 引言

红黑树的结点增删改查效率非常优良,都为logn,其应用十分广泛:
1. Linux内核进程调度由红黑树管理进程控制块。
2. Epoll用红黑树管理事件块。
3. nginx服务器用红黑树管理定时器。
4. C++ STL中的map和set的底层实现为红黑树。
5. Java中的TreeMap和TreeSet由红黑树实现。
6. Java8开始,HashMap中,当一个桶的链表长度超过8,则会改用红黑树。

红黑树的操作比较复杂,实现起来比较麻烦,看了很多篇别人写的关于红黑树的博客,插入操作比较容易理解,但是删除操作都非常乱,本文不会讲解二叉排序树,左旋右旋等基础知识,将直接从红黑树的插入操作和删除操作开始讲解,并附上源码,算是自己对红黑树的一个总结。

2. 红黑树的性质

  1. 结点不是红色就是黑色。
  2. 根结点一定为黑色。
  3. 所有的NULL指针被认为是黑色结点。
  4. 红色结点的两个孩子一定是黑色。
  5. 从任一节点到其每个叶子的所有简单路径上的黑色结点数都相同。

想要理解红黑树的操作,以上性质必须滚瓜烂熟。
先定义红黑树这个类的成员属性:

public class RBTree<T extends Comparable<T>> {    private RBNode<T> root;//根结点指针    private static final boolean RED = false;    private static final boolean BLACK = true;    class RBNode<T extends Comparable<T>> {//树的结点        boolean color;//红或黑        T key;        RBNode<T> left;//左孩子指针        RBNode<T> right;//右孩子指针        RBNode<T> parent;//父结点指针,红黑树经常涉及到兄弟,叔叔,侄子,有个父结点指针方便操作。        public RBNode(boolean color, T key, RBNode<T> left, RBNode<T> right, RBNode<T> parent) {            this.color = color;            this.key = key;            this.left = left;            this.right = right;            this.parent = parent;        }    }    //左旋    private void leftRotate(RBNode<T> x) {        RBNode<T> y = x.right;        x.right = y.left;        if(y.left != null)            y.left.parent = x;        y.parent = x.parent;        if(x.parent != null) {            if(x.parent.left == x)                x.parent.left = y;            else                 x.parent.right = y;        }else {            this.root = y;        }        y.left = x;        x.parent = y;    }    //右旋    private void rightRotate(RBNode<T> x) {        RBNode<T> y = x.left;        x.left = y.right;        if(y.right != null)            y.right.parent = x;        y.parent = x.parent;        if(x.parent != null) {            if(x.parent.left == x)                x.parent.left = y;            else                 x.parent.right = y;        }else {            this.root = y;        }        y.right = x;        x.parent = y;    }}

3. 红黑树的插入操作

  首先红黑树是二叉排序树,按排序树的方式插入一个结点之后,此结点肯定是叶子结点,那么到底应该给这个新结点涂什么颜色,涂黑的话,当前的分支会多一个黑结点,破坏了性质5,要恢复很麻烦,而涂红则没有影响,所以直接对新插入的结点涂红。
  然后可以判断一下这个新结点是不是根(刚建树),如果是根则涂黑,以满足性质2。
  但是如果它有父结点,那就要根据父结点的颜色分情况讨论了:
  (以下情况都假设父结点在左边,在右边的情况都是对称的)。

  1. 如果新插入结点的父结点为黑色,则什么都不用做。
    插入结点是红的,不会破坏性质。(新结点也可以在右边)
    这里写图片描述

  2. 父结点为红色
      如果父结点为红色,那由于性质2,说明父结点肯定不是根结点,那一定有祖父结点,且为黑色(性质4),叔叔结点可能有也可能为null。
      从祖父结点来看,其父结点分支黑结点数为0(NULL这种东西不想多说),那么叔叔结点分支黑结点数也必须为0,所以进一步推断叔叔为null或者为红结点。那么就再分出这两种情况:
      (1)叔叔为红结点的情况。
      无

      涂色之后,grandparent为根时的样子
      这里写图片描述
      
      grandparent不为根的情况,之后将其当成新结点进行操作
      这里写图片描述

      (2)叔叔为null时的情况
      这里写图片描述
    为了先解决parent这里的问题,肯定也只能将其涂黑再说,但是这样就多了一个黑结点,这里只有一种解决方法,就是对grandparent进行右旋,这样grandparent使右边多了一个黑结点,那么将其涂红即可。


  要注意的是,之前的各种情况,新插入结点都是在左边,其实在右边也是一样的,但是这里就不同了,新插入的结点必须在左边,如果在右边,会在grandparent右旋之后成为其左孩子,这样一来,grandparent为红,新插入结点也为红,破坏了性质4,看下图。
这里写图片描述

所以应先对parent进行左旋操作,再将new和parent指针互换

这里写图片描述

最后对grandparent右旋变成:

这里写图片描述

到这里为止,红黑树的插入操作就全部解析完了。

4. 实现源码  

//二叉排序树的插入private void insertNode(RBNode<T> node) {    RBNode<T> curr = this.root, pre = null;    int comp = 0;    while(curr != null) {        pre = curr;        comp = node.key.compareTo(curr.key);        if(comp < 0)            curr = curr.left;        else if(comp > 0)            curr = curr.right;        else            return;    }    node.parent = pre;    if(pre == null)        this.root = node;    else {        if(comp < 0)            pre.left = node;        else if(comp > 0)            pre.right = node;    }    insertFixUp(node);}//修复private void insertFixUp(RBNode<T> node){    RBNode<T> parent, gparent, uncle;    //父结点不是根,且为红色,才进行修复    while((parent=node.parent) != null && parent.color == RED) {        gparent = parent.parent;        if(parent == gparent.left) {//父结点在左边,右边和左边对称,操作是一样的            if((uncle=gparent.right)!=null && uncle.color==RED) {//叔叔为红色                parent.color = BLACK;                uncle.color = BLACK;                gparent.color = RED;                node = gparent;                continue;//将祖父当成新插入结点继续修复            }            //对于新插入结点在右边的情况,要将其转到左边来            if(parent.right == node) {                leftRotate(parent);                RBNode<T> tmp = parent;                parent = node;                node = tmp;            }            parent.color = BLACK;            gparent.color = RED;            rightRotate(gparent);        }else {//对称操作,一样的            if((uncle=gparent.left)!=null && uncle.color==RED) {                parent.color = BLACK;                uncle.color = BLACK;                gparent.color = RED;                node = gparent;                continue;            }            if(parent.left == node) {                rightRotate(parent);                RBNode<T> tmp = parent;                parent = node;                node = tmp;            }            parent.color = BLACK;            gparent.color = RED;            leftRotate(gparent);        }    }    this.root.color = BLACK;}

5. 总结

  可以看到,红黑树的插入操作还是蛮简单的,情况很少。