Tree--RedBlackTree详解(2 - 3 - 4Tree)(红黑树)
前言
最近看到好多红黑树的东西,英文好的童鞋可以直接点击http://www.cs.princeton.edu/~rs/talks/LLRB/RedBlack.pdf这里查看我之前学习的材料,对理解下面讲的东西肯定也有点帮助(但是不完全一样),英文一般的同学就直接看我的文采飞扬把哈哈。还有大家可以去coursera上学习一些国外比较好的资料。感觉比国内一些学习网站做的好很多。
前面一篇随笔写的binarysearchtree(http://www.cnblogs.com/robsann/p/7567596.html)说了有一个缺点就是不平衡,意思就是插入已经排好序的对象的时候会变成一条链表,链表当然比二叉树要慢很多啦,随机插入的话二叉树的各种方法需要的时间和LgN的成正比。所以红黑树其实是在解决二叉树不平衡的问题的。
度娘(这里稍微看一下)
红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求: 性质1. 节点是红色或黑色。 性质2. 根节点是黑色。 性质3 每个叶节点(NIL节点,空节点)是黑色的。 性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点) 性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。 这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。正文
2 - 3 - 4 Tree
2 - 3 - 4 Tree 我觉得算是一种模型,一种树模型保证了树是平衡的,所谓平衡就是树不会一个枝头长得很高,另外一个枝头长得很矮,那保证平衡有什么用?平衡的情况下,所有操作需要的时间都是和LgN成正比的,你说腻害不腻害。
2 - 3 - 4 树,允许一个节点是 2-nodes 或者是 3 nodes 或者是 4 nodes, 具体的意思就是说
这是一个2-nodes, 有2个触手,能够指向不同的2个子元素,左边的子元素小于A,右边的子元素大于A
3-nodes, 3 分叉,最左边的子元素小于C,中间的子元素between c和e, 最右边的子元素大于E
4-nodes, 3分叉。同上
看看2-3-4tree是如何保证平衡的
假设有 10 7 6 3 8 11 15 要插入 2 -3 4 树中
这里插入还有另外一种选择叫Bottom-up solution,之下而上的一种解法(可以忽略)。就是先找到这个节点会被插入的位置,如果插入后变成了5-nodes,就把其中一个节点往父节点抛出,让父节点和其中的一个子节点结合。
删除同理,为了要保证树的平衡,当删除的时候,如果被删除的节点只有一个元素的话,必须要把父亲元素拉下来形成一个3-nodes然后删除后变成2-nodes(后面还会说)
左倾红黑树(LeftLeaningRedBlackTree)
红黑树和2-3-4树的关系,红黑树是2-3-4树的一种实现方式。2-3-4树只是一个模型。
先介绍一下节点对象Node,看代码,之后会用到这个对象
private class Node { private K k; // 这个节点的key private V v; //节点的value private Node left, right; //左右节点 private int size; //节点为根的树的大小 private boolean color; //节点的颜色,分黑和红 Node(K k, V v, int size, boolean color) { this.k = k; this.v = v; this.size = size; this.color = color; } @Override public String toString() { if (color) return "red " + v; else return "black " + v; } }Node(节点对象)
左倾红黑树和红黑树的区别如下
这是一般的红黑树,3-node 的时候红节点可以左倾或者右倾
左边是2-3-4树的模型,右边是红黑树的实现
这是左倾红黑树, 3-nodes的时候红色的节点必须在左边。
用常量来表示红黑
红黑树的修正
首先左倾红黑树的性质必须要得到保证(就是上图中说的Require that 3 -node be left leaning),但是很多时候可能因为插入或者删除的操作破坏了这个性质,所以我们必须要修正。
这里介绍3个修正的方法
第一个方法的使用场景是这样子的,(为了不破坏平衡,每次插入的都是红节点),下图插入的节点在右边,破坏了左倾的性质,所以必须rotateLeft。rotateLeft是指把红色节点左移
方法如下
//右树是红link的时候,turn this red link to left private Node rotateLeft(Node h) { assert(isRed(h.right)); Node x = h.right; //change the pointers h.right = x.left; x.left = h; x.color = h.color; //change the colors h.color = RED; x.size = h.size; //change the sizes h.size = size(h.left) + size(h.right) + 1; return x; }rotateLeft(仔细看一边)
另外一种情况是最新插入的节点在最左边,把中间节点rotateRight,重新平衡
方法如下
//左树是红link的时候,turn this red link to left private Node rotateRight(Node h) { assert(isRed(h.left)); Node x = h.left; h.left = x.right; x.right = h; x.color = h.color; h.color = RED; x.size = h.size; //size is the same h.size = size(h.left) + size(h.right) + 1; return x; }rotateRight
这张图是破坏了性质之后,修正的办法
还有一个方法是flipColors(),代码如下,就是可能插入的时候需要满足当前节点不是4-nodes,可能就会使用这个方法
有了上面这些辅助的方法后就可以开始下面的学习了
左倾红黑树的put
在2-3-4tree中的介绍中也知道了,put的时候,当前节点如果是4-nodes的话就没有位置留给需要插入的对象了。
所以我们在put的时候,一定要保证当前的节点(currentNode)以后用cn来表示。cn必须不是4-nodes, 如果是4-nodes的话就用flipColor把4-node变成3个2-node
假设我们要插入10 7 6 3 这4个对象, 大片动态图,燃烧的经费。
附上代码
public void put(K k, V v) { root = put(root, k, v); root.color = BLACK; } private Node put(Node cn, K k, V v) { if (cn == null) return new Node(k, v, 1, RED); if(isRed(cn.left) && isRed(cn.right)) split4Node(cn);//是4节点的话 就split int cmp = k.compareTo(cn.k); if (cmp > 0) cn.right = put(cn.right, k, v); // k > node.k go right else if (cmp < 0) cn.left = put(cn.left, k, v); else cn.v = v; //hit //following code is to fix the tree on the way up if (isRed(cn.right) && !isRed(cn.left)) cn = rotateLeft(cn); // right leaning 3nodes的时候 需要变成 left leaning if (isRed(cn.left) && isRed(cn.left.left)) cn = rotateRight(cn); //变成了一个4节点 cn.size = size(cn.left) + size(cn.right) + 1; return cn; }put
左倾红黑树的get
树的get方法其实很简单,就是判断key是不是相等,如果相等就return 这个值。
public V get(K k) { return get(root, k); } //cn means currentNode private V get(Node cn, K k) { if (cn == null) return null; // not find the key int cmp = k.compareTo(cn.k); if(cmp > 0) return get(cn.right, k); else if (cmp < 0) return get(cn.left, k); else return cn.v; // hit }get
左倾红黑树的删除
删除可以说是最难的了把,基本的思想就是,cn节点(当前节点)不会是2-node,(如果root节点是2-nodes,我们需要把root节点变成红节点)
保证其中一个子节点不是2节点(这个保证需要看删除的节点位于当前节点的哪里,比如删除的节点比cn节点小,所以接下来我们会往left走,所以要保证left节点不是2-node)。因为2节点如果删除了的话就不会平衡。所以必须要把红节点从root一步一步carry下去。
先实现一个deleteMin方法,我们要把红节点带向左边。想一想,红节点带向左边后,如果左边有节点删除了可能没办法保持平衡,红节点可以变成黑节点,代替刚才被删除的节点。通过这样子可以保证左边的树是一定会平衡的。
deleteMin的几种情况
1. , 现在可以直接删除掉。也不会影响平衡。删除了后3节点变成了2节点。
2., 需要继续向左走,但是左子节点是2-node,必须要想办法变成不是2-node。所以需要把父亲节点和兄弟节点和cn节点。整合在一起变成4-node
3. , 需要继续向左走,但是左子节点是2-node,必须要想办法变成不是2-node。发现兄弟节点是不是2-nodes。所以把兄弟节点借一个node过来
2和3这2种情况总结在一起就是moveRedLeft的代码
public void deleteMin() { //保证了root节点不是2nodes if (!isRed(root.left) && !isRed(root.right)) root.color = RED; root = deleteMin(root); root.color = BLACK; } public Node deleteMin(Node cn) { if (cn.left == null) return null; if (!isRed(cn.left) && !isRed(cn.left.left)) //判断左边子节点是不是2node,是的话就需要把Red带下去 cn = moveRedLeft(cn); cn.left = deleteMin(cn.left); return fixup(cn); } private Node fixup(Node h) { if (isRed(h.right) && !isRed(h.left)) h = rotateLeft(h); //右倾 if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h); h.size = size(h.left) + size(h.right) + 1; //right the size return h; }deleteMin
随意的delete方法。按照下面的图说一下,基本的思想。
首先删除D。从H开始,H不是2-node(右边有一个红节点),D小于H,左往H的左边找
找到了D。D不是2-node且是红节点。找到了D,处理办法是找到右树中最小的值,发现是E, 把最小的值赋值给当前的node
所以我们要往右边走。但是发现右边节点F是2-node。所以我们要把红色的连接往右边带。
flipColor(D), B D 和 F就变成了一个4-node(可以自己做一下图看看是不是这样子的)。 这时候红链接也往右边带了。
现在到了F 节点。发现F 的左节点是 2-node。把红色的链接往左边带。
flipColor(F),这个时候F 和 E 和 G ,变成了一个4-node。 顺理成章删除左边的节点。没有影响平衡。
接着原路返回,修复节点。
public void delete(K k) { if (k == null) throw new IllegalArgumentException("argument to delete() is null"); if (!contains(k)) return; if (!isRed(root.left) && !isRed(root.right)) root.color = RED; root = delete(root, k); if (root != null) root.color = BLACK; } public boolean contains(K k) { return get(k) != null; } private Node delete(Node cn, K k) { if (cn == null) return null; int cmp = k.compareTo(cn.k); if (cmp < 0) { // k < node.k go left if (!isRed(cn.left) && !isRed(cn.left.left)) //保证了下一个左元素不是2nodes cn = moveRedLeft(cn); cn.left = delete(cn.left, k); } else if (cmp > 0) { // k > node.k go right if (isRed(cn.left) && !isRed(cn.right)) //如果是3节点的话需要 rotate 把red转到右边 cn = rotateRight(cn); if (!isRed(cn.right) && !isRed(cn.right.left)) //保证下一个右节点不是2nodes cn = moveRedRight(cn); cn.right = delete(cn.right, k); } else { //hit if (isRed(cn.left) && !isRed(cn.right)) cn = rotateRight(cn); if (k.compareTo(cn.k) == 0 && (cn.right == null)) //find null just return null return null; if (!isRed(cn.right) && !isRed(cn.right.left)) //保证下一个右节点不是2nodes cn = moveRedRight(cn); if (k.compareTo(cn.k) == 0) { Node x = min(cn.right); cn.k = x.k; cn.v = x.v; cn.right = deleteMin(cn.right); } else cn.right = delete(cn.right, k); } return fixup(cn); }delete
总结
具体的实现可以参考一下https://github.com/Cheemion/algorithms/blob/master/src/com/algorithms/tree/LeftLeaningRedBlackTree.java
可能有地方说的不清楚哈。见谅,可以留言我有不清楚的地方