红黑树是在实际工程中被普遍应用的一种数据结构,好比Linux中的线程调度就是使用的红黑树来管理进程控制块,而Nginx中也是使用红黑树来管理的timer,Java中的TreeMap和TreeSet也是基于红黑树来实现的。红黑树相比普通二叉查找树的一个优点就是它的树高为~lgN,因此无论是查找/插入/删除操做它均能保证可以在对数时间以内完成。本文咱们就先来了解一下红黑树插入算法的实现。java
红黑树能够定义成含有红黑连接而且知足下列条件的二叉查找树:node
好比下图就是一棵典型的红黑树,若是以前了解过2-3树的话(不了解也没有关系,咱们下面的内容并不会涉及到2-3树),能够发现若是将红黑树中的红色结点画平,实际上它就是2-3树的一种变形,不过相比2-3树,红黑树的查找操做要简单的多,但它同时也结合了2-3树中高效的插入操做,因此说红黑树实际上是普通的二叉查找树和2-3树两种数据结构优势的结合。算法
在实现红黑树以前咱们先来定义一棵红黑树:数据结构
public class RedBlackLiteBST<Key extends Comparable<Key>, Value> { private static final boolean RED = true; private static final boolean BLACK = false; private Node root; // root of the BST private int n; // number of key-value pairs in BST // BST helper node data type private class Node { private Key key; // key private Value val; // associated data private Node left, right; // links to left and right subtrees private boolean color; // color of parent link public Node(Key key, Value val, boolean color) { this.key = key; this.val = val; this.color = color; } } }
在上面的代码中,咱们将连接的颜色保存在表示该结点的Node
对象中的color
变量中。若是指向它的连接是红色的,那么该变量为true,黑色则为false,咱们规定空连接都为黑色。以下图所示,指向结点C
的连接是红色,那么咱们就将h.left.color
设置为红色,指向结点J
的连接是黑色的,咱们就将h.right.color
设置为黑色。this
红黑树相比普通的二叉查找树的一个重要优点就是插入的高效性,可是正由于如此红黑树的插入操做的算法实现相比普通的二叉树要复杂一些。在正式实现插入算法以前,咱们有必要先了解一下对于红黑树的几种基本操做。spa
以下图所示,如今咱们有一条红色的右连接,如今咱们想要将这条红色右连接转换为左连接,这个操做过程就叫作左旋转:线程
咱们要作的就是在保持红黑树平衡性的同时,将上图的结构变为下面这样:code
仔细观察能够发现,咱们要实现的其实就是将红色连接关联的两个节点中由较小的节点E
做为根节点转换成由较大的节点S
做为根节点。同时在这个过程当中为了保持二叉树中左子树都要小于根节点,右子树都要大于根节点,因此若是S
节点还存在的话左连接咱们还须要将它变成E
节点的右连接。具体的实现代码以下:对象
private Node rotateLeft(Node h) { assert (h != null) && isRed(h.right); Node x = h.right; h.right = x.left; x.left = h; x.color = h.color; h.color = RED; return x; }
右旋转的实现和左旋转的实现是相似的,就是将一个红色左连接转换成一个红色右连接:进程
与左旋转的时候相反,咱们要作的其实就是红色连接关联的两个节点中较大的节点S
做为根节点转换成由较小的节点E
做为根节点:
因此转换成具体的代码,咱们只须要将left和right相互转换就好了:
private Node rotateRight(Node h) { assert (h != null) && isRed(h.left); Node x = h.left; h.left = x.right; x.right = h; x.color = h.color; h.color = RED; return x; }
上面咱们提到红黑树的一个重要特性就是红连接均为作左连接,因此对于下面这种情形,若是一个结点的两个子结点均为红色连接,咱们就将子结点的颜色所有由红色转换成黑色,同时将父结点的颜色由黑变红。
具体的实现代码很是简单:
private void flipColors(Node h) { assert !isRed(h) && isRed(h.left) && isRed(h.right); h.color = RED; h.left.color = BLACK; h.right.color = BLACK; }
上面说了这么多,其实都是为接下来的插入操做作的预热。接下来结合下图,咱们来分析红黑树插入操做过程咱们会遇到的三种情形:
c
大于树中现存的两个键,因此咱们要将它链接到b
结点的右连接。由于这个时候b
的两条连接都是红连接,因此咱们要进行flipColors
。接下来能够发现,咱们下面的两种状况都会转换成这种情形。a
要比树中现存的两个键都要小,因此咱们先将它链接到结点b
的左连接,前面咱们提到红黑树的一个特性就是没有任何一个节点能够同时和两个红色连接相连,而如今b
结点却违背了这一原则,因此咱们要进行右旋转操做,接下来情形1是如出一辙的了。b
在树中现存的两个键之间,因此咱们先将它链接到结点a
的右连接,前面咱们提到红黑树中红色连接都是左连接,因此咱们首先要进行左旋转操做,接下来就和情形2如出一辙了。插入操做的具体实现代码以下,下面的代码直到// fix-up any right-leaning right
以前咱们作的都是为了找到合适的插入位置,而以后的3个if
语句实际上就是对上图中情形3的一种总结。
public void put(Key key, Value val) { root = insert(root, key, val); root.color = BLACK; // assert check(); // Check integrity of red-black tree data structure. } private Node insert(Node h, Key key, Value val) { if (h == null) { n++; return new Node(key, val, RED); } int cmp = key.compareTo(h.key); if (cmp < 0) h.left = insert(h.left, key, val); else if (cmp > 0) h.right = insert(h.right, key, val); else h.val = val; // fix-up any right-leaning links if (isRed(h.right) && !isRed(h.left)) h = rotateLeft(h); if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h); if (isRed(h.left) && isRed(h.right)) flipColors(h); return h; }
isRed
的实现很是简单,我就不解释了:
// is node x red (and non-null) ? private boolean isRed(Node x) { if (x == null) return false; return x.color == RED; }
本文为做者原创,转载请于开头明显处声明博客出处:)