红黑树简介之java实现

声明

  1. 本文最后会使用java语言实现红黑树的插入和删除的算法实现,所以本文是站在java的角度来描述红黑树,比如叶子节点会用null表示,而不是nil。

  2. 红黑树其实是一颗自平衡二叉搜索树,对二叉搜索树不了解的,可以先看《二叉搜索树简介之java实现》这篇文章,下面的内容假定已经了解了二叉搜索树,所以不再重点关注二叉搜索树的特性。

1. 红黑树的定义

1.1 红黑树相比二叉搜索树的优点

红黑树本身就是一棵二叉搜索树。

二叉搜索树是不平衡的,在极不平衡的时候,也就是最坏的情况下,会退化成链表,此时的时间复杂度为O(n)。

红黑树是在二叉搜索树的每个节点绘制黑色或红色(通过一个标志位来描述),通过满足某些规则的方式来使树达到平衡(并不是非常完美的平衡),这便可以使树的插入和删除的查询操作即使在最坏的情况下,也能在O(log n)次完成(n是树的节点数)。

1.2 红黑树命名的历史

这是在维基上看到的一句话,原文翻译如下:

在1978年的一篇论文中,“A Dichromatic Framework for Balanced Trees”,Leonidas J. Guibas和Robert Sedgewick从对称二元B树中得到了红黑树。选择颜色“红色”是因为它是作者在Xerox PARC工作时可用的彩色激光打印机产生的最佳色彩。Guibas的另一个回应是,由于可以使用红色和黑色的笔来绘制树木。

1.3 不同于二叉搜索树的额外规则

红黑树在基于二叉搜索树的规则上,还有下面几条新增的规则:

  1. 每个节点的颜色要么是红色,要么黑色(只有两种颜色,非黑即红)

  2. 根节点永远是黑色(有时候可以忽略该规则,因为根节点可以很容易从红色变成黑色,不一定会很容易从黑色变成红色)

  3. 所有的叶子节点都是黑色的(用java语言的null表示叶子节点)

  4. 红色节点的子节点必须都是黑色(即同一条路径上不允许出现两个连续的红色节点)

  5. 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点(从任一节点到其后代的叶子节点的路径上的黑色数量,这里简称黑高,也就是说,任一节点的黑高必须一样)

1.4 红黑树的示例

下图是红黑树的一个示例,它的中序遍历的结果是:1,6,8,11,13,15,17,22,25,27,本文就围绕它进行算法描述的示例。

2. 红黑树的操作

本文主要介绍下红黑树的插入与删除操作,红黑树其实是二叉搜索树的特殊情况,如果查询类操作参照二叉搜索树实现即可。而插入与删除操作会违反红黑的规则,破坏树的平衡性,需要相关的左旋或右旋操作来维持树的平衡性。恢复红黑树的属性规则,需要O(log n)或O(1)颜色变化和不超过3次的树旋转。尽管插入和删除操作很复杂,但它们的时间复杂度仍为O(log n)。

声明以下几种标记表示待操作的节点:

  • N:当前操作节点

  • P:当前操作节点的父节点

  • U:当前操作节点的父节点的兄弟节点(叔叔节点)(UL:左叔叔、UR:右叔叔)

  • S:当前操作节点的兄弟节点(SL:左兄弟节点、SR:右兄弟节点)

  • G:当前操作节点的父节点的父节点,祖父节点

2.1 插入

插入的方式类似于二叉搜索树的插入,但是新增的节点标记为红色(首先要满足规则5,黑高一致),后面可以通过旋转来满足其它规则。如果新增的是叶子节点,红黑树的叶子节点为null节点,因此替换叶子节点,并在该节点下新增两个叶子节点。

插入的情况有以下几种需要处理:

  1. 插入节点N是根节点

  2. 插入节点N的父节点P是黑色

  3. 插入节点N的父节点P和叔叔U节点都是红色

  4. 插入节点N的父节点P是红色,但是叔叔节点U是黑色

下面进行说明:

1. N是根节点

设置颜色为黑色,不用做其它处理

2. N的父节点是黑色

N标记为红色,直接插入即可,不会违背红黑的规则。

3. 插入节点N的父节点P和叔叔U节点都是红色

N为红色,插入之后违背规则4,同一路径,不能连续有2个红色节点,则设置P和U为黑色,设置G为红色。然后,如果发现G为根节点,则再设置G为黑色,否则,将G节点作为N节点,递归调用(可能需要旋转)重新调整树的结构来满足红黑树的规则。

4. 插入节点N的父节点P是红色,但是叔叔节点U是黑色

这又分两种情况:

4.1 P是红色,U是黑色。并且,N是P是右子节点,P是G的左子节点,如下图结构:

 进行一次左旋操作,左旋操作可以这样理解:将N节点替换P节点的位置(N节点成了P节点),P节点成为N节点的左子节点,而N节点原本的左子节点成为P节点的右子节点,如下图:

然后,将旋转后的结点作为情况4.2处理。

其实,这里可以这样理解,可以看上图,G、P、N不在一条直线上,通过左旋处于一条直线然后才往下处理。所以,如果是另一种情况(不是4.2那种),即P是G的右子节点,N是P的左子节点,可以先做下右旋,让G、P、N处在一条直线上时再进行4.2情况的处理。

4.2 P是红色,U是黑色。并且,N是P的左子节点,P是G的左子结点,如下图:

这种情况,先针对G进行一次右旋,右旋操作:将G作为P的右子节点,P原先的右子节点成为G的左子节点,然后,切换P和G的颜色。如下图所示(这样操作是为将左子树的一个红色节点分配到右子树上):

经过以上几种情况的处理(有时候需要递归调用),来调整树的结构达到满足红黑树的规则。

2.2 删除

删除的情况要比插入多些,所以删除更为复杂点,但是也可以类似二叉搜索树的删除操作(如果被删除节点的左右节点都非空节点(即都不是叶子节点),找到待删除节点的前趋或后继节点代替进行删除),然后通过旋转等操作调整树的结构来进行恢复达到满足红黑树的规则。

如果使用被删除节点的前趋节点替代,则这个前趋节点应该是被删除节点的左子树中的最大节点,另外可以肯定的是,这个前趋节点,至多有一个左子节点(原因很简单,如果它有右子节点,那它就不是左子树中的最大节点了,而是它的右子节点了)。同理,后继节点应当是被删除节点的右子树中的最小节点,后继节点至多有一个右子节点。

本文就使用前趋节点来说明了。删除的时候,使用前趋节点代替被删除节点,那就把前趋节点看作N。如果被删除节点至多有一个非空的叶子节点,那就把被删除节点直接看作替换节点N来处理吧,这样也好分析。

针对节点N的删除之后,调整树的结构,有以下几种情况要处理:

1. N是根节点(被替换节点,所以不可能有两个非空叶子节点)

这种情况下,直接删除N即可。因为N是替换节点,说明N最多有一个红色的非空叶子节点。如果N的子节点都为空,那就直接删除,如果有一个红色的叶子节点,那就用红色的叶子节点覆盖它即可。

2. N是红色节点

和情形1一样处理就行。

3. N是黑色节点

这种情况就比较复杂了,因为N是黑色节点,删除之后所有路径上少了一个黑色节点,破坏了规则5,这时候就需要调整树的结构来满足规则了,比如问它的父母兄弟节点借一个黑色节点,这个时候就要看它的父母兄弟节点有没有黑色节点来借了,这样的话就又有好几种情况需要处理了。注意,这个过程可能是个递归调用的工程,直到节点N不是根节点并且是黑色结束。

3.1 N是左子节点,N的兄弟节点S是红色

如下图:

此时,将P设置为红色,将U设置为黑色,以P节点进行左旋:

其实上面的操作,是因为P-n1路径上少了个黑色节点,那就借一个过来,同时还不能影响原来P的右子树下的黑色节点数量,但是这样处理的话还不能算完,还要往下走。

3.2 N是左子节点,S是黑色,S的两个子节点都是黑色

这种情况下,将兄弟节点S设置为红色即可,因为N作为黑色节点失去了,那它的兄弟节点就也要失去一个黑色来一致,但是这样从P节点下的路径都失去了一个黑色。不用担心,如果是从情况一过来的,那么P就是红色,结束递归调用并设置P为黑色来满足规则5。

如果P不是红色,那就继续递归调用处理。

3.3 N是左子节点,S是黑色,S的其中一个子节点是红色

这个时候有两种情况的,其一是红色子节点是左子节点,其二是红色子节点是右子节点。

如果是情况一的话,我们将红色左子节点调整到情况二,如下图,因为是红色的,所以并不影响原来的黑高:

上图,以S节点进行了右旋,并切换S和n3的颜色。这样处理后变成红色节点为右子节点的情况来处理,现在只有右子节点是红色的如下图:

这时候,P的左子树因为删除N节点的原因少一个黑色节点,此时就要以P节点进行左旋,并设置P为黑色借一个黑色节点给左子树。这样的话原本P的右子树会少一个节点(因为现在P被借到左子树当黑色节点了,而原本P的颜色并不知道),所以设置S节点为原本P节点的颜色,但这样可能导致右边少一个黑色节点,那就再设置SR为黑色来补充S节点丢失的黑色即可。这样调整就完成了,结束递归调用。

如下图:

3.4 N是右子节点,N的兄弟节点S是红色

3.5 N是右子节点,S是黑色,S的两个子节点都是黑色

3.6 N是右子节点,S是黑色,S的其中一个子节点是红色

对于3.4、3.5、3.6其实是3.1、3.2、3.3的相反情形,不具体说明了。

其实,根据我个人的理解,其实红黑树的插入删除时,为维护树的规则,需要复杂操作的就两个:

  1. 插入的时候,遇到2个红色节点,就进行相关旋转,把其中一个红色放到别的地方

  2. 删除的时候,删了一个黑色节点,那就从父母兄弟那里借一个过来。

3. 源码

import java.util.ArrayList;
import java.util.List;

public class RedBlackTree<T extends Comparable> {

    private static final boolean RED = false;
    private static final boolean BLACK = true;
    private Node root;

    public RedBlackTree() {
        root = null;
    }

    public RedBlackTree(T t) {
        this.root = new Node(t, null, null, null);
    }

    /**
     * 增加一个数据
     */
    public void add(T e) {
        // 如果根节点为空设置为根节点,默认是黑色
        if (root == null) {
            this.root = new RedBlackTree.Node(e);
        } else {
            // 根节点不为空的情况
            RedBlackTree.Node current = root;
            RedBlackTree.Node parent = null;
            int cmp = 0;
            do {
                // 二叉搜索树的查找方法,找到合适的节点
                parent = current;
                cmp = e.compareTo(current.data);
                if (cmp > 0) {
                    current = current.right;
                } else {
                    current = current.left;
                }
            } while (current != null);
            RedBlackTree.Node newNode = new RedBlackTree.Node(e, parent, null, null);
            if (cmp > 0) {
                parent.right = newNode;
            } else {
                parent.left = newNode;
            }
            // 新增一个黑色节点,肯定违反了红黑树的黑高一致规则,重新调整树结构
            fixAfterInsertion(newNode);
        }
    }

    /**
     * 插入后修复红黑树
     *
     * @param x 插入的节点
     */
    private void fixAfterInsertion(Node x) {
        // 调整考虑如下几种情况:
        // 1. x肯定不是根节点了,根节点的话就不需要调整,也就走不到这一步了
        // 2. x的父节点是根节点,那么只需要将x设置为红色就行了,不会违反红黑树的规则
        // 3. 所以重点调整就不需要考虑1和2了
        // 4. x的父节点和叔叔节点都是红色,这个时候,只需要将x叔、父节点设置为黑色,祖父节点设置为红色
        // 然后,将祖父节点设置为x递归处理
        // 5. x的叔叔节点是黑色,这时候就需要进行旋转处理了

        // 新节点设置为红色
        x.color = RED;
        while (x != null && x != root && x.parent.color == RED) {
            // x的父节点是x的祖父节点的左子节点
            if (parentOf(x).isLeftChild()) {
                // x 的叔叔节点是右子节点
                Node uncle = rightOf(grandfatherOf(x));
                if (colorOf(uncle) == RED) {
                    // 叔、父都是红色,按上面注释的情形4处理
                    setColor(parentOf(x), BLACK);
                    setColor(uncle, BLACK);
                    setColor(grandfatherOf(x), RED);
                    x = grandfatherOf(x);
                } else {
                    // x是父节点的右子节点
                    if (x.isRightChild()) {
                        x = parentOf(x);
                        // x和x的父节点不在一样“直线路径”上,对x的父节点左旋一下弄成看起来是一条直线的路径
                        rotateLeft(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(grandfatherOf(x), RED);
                    rotateRight(grandfatherOf(x));
                }
            } else { // x的父节点是右结点
                // x 的叔叔节点
                Node uncle = leftOf(grandfatherOf(x));
                if (colorOf(uncle) == RED) {
                    // 叔、父都是红色,按上面注释的情形4处理
                    setColor(parentOf(x), BLACK);
                    setColor(uncle, BLACK);
                    setColor(grandfatherOf(x), RED);
                    x = grandfatherOf(x);
                } else {
                    // x是左节点
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        // x和x的父节点不在一样“直线路径”上,对x的父节点右旋一下弄成看起来是一条直线的路径
                        rotateRight(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(grandfatherOf(x), RED);
                    rotateLeft(grandfatherOf(x));
                }
            }
        }
        // 根节点永远为黑
        root.color = BLACK;
    }

    private void rotateLeft(Node p) {
        if (p != null) {
            Node r = p.right;
            Node q = r.left;
            p.right = q;
            if (q != null) {
                q.parent = p;
            }
            r.parent = p.parent;
            if (p.parent == null) {
                root = r;
            } else if (p == p.parent.left) {
                p.parent.left = r;
            } else {
                p.parent.right = r;
            }
            r.left = p;
            p.parent = r;
        }
    }

    private void rotateRight(Node p) {
        if (p != null) {
            Node l = p.left;
            Node q = l.right;
            p.left = q;
            if (q != null) {
                q.parent = p;
            }
            l.parent = p.parent;
            if (p.parent == null) {
                root = l;
            } else if (p == p.parent.left) {
                p.parent.left = l;
            } else {
                p.parent.right = l;
            }
            l.right = p;
            p.parent = l;
        }
    }

    private Node parentOf(Node p) {
        return p == null ? null : p.parent;
    }

    private Node grandfatherOf(Node p) {
        return parentOf(parentOf(p));
    }

    private Node leftOf(Node p) {
        return p == null ? null : p.left;
    }

    private Node rightOf(Node p) {
        return p == null ? null : p.right;
    }

    private boolean colorOf(Node p) {
        return p != null ? p.color : BLACK;
    }

    private void setColor(Node p, boolean c) {
        if (p != null) {
            p.color = c;
        }
    }

    public Node getNode(T e) {
        Node p = root;
        while (p != null) {
            int cmp = e.compareTo(p.data);
            if (cmp < 0) {
                p = p.left;
            } else if (cmp > 0) {
                p = p.right;
            } else {
                return p;
            }
        }
        return null;
    }

    /**
     * 移除一个节点
     */
    public void remove(T e) {
        Node target = getNode(e);
        if (target == null) {
            return;
        }
        // 如果待移除节点的两个子节点都存在,那么找到它的前趋节点代替它
        if (target.left != null && target.right != null) {
            Node node = target.left;
            while (node.right != null) {
                node = node.right;
            }
            // 将前趋节点的数据覆盖待删除节点的数据
            target.data = node.data;
            // 针对代替节点进行操作
            target = node;
        }
        Node relacement = target.left != null ? target.left : target.right;
        // 如果target还有一个子节点,那这里需要调整下,但是如果target是从上面找到的原本待删除节点的前趋节点,
        // 那么target至多有一个左子节点
        if (relacement != null) {
            relacement.parent = target.parent;
            if (target.isRoot()) {
                root = relacement;
            } else if (target.isLeftChild()) {
                target.parent.left = relacement;
            } else {
                target.parent.right = relacement;
            }
            // 删除target
            target.left = target.right = target.parent = null;
            if (colorOf(target) == BLACK) {
                // 删除了一个黑色节点,需要修复红黑树来满足黑高一致的规则
                fixAfterDeletion(relacement);
            }
        } else if (target.isRoot()) {
            root = null;
        } else {
            // target没有非空子节点了
            // 删除了一个黑色节点,需要修复红黑树来满足黑高一致的规则,红色就不用了
            if (colorOf(target) == BLACK) {
                fixAfterDeletion(target);
            }
            // 删除target
            if (target.isLeftChild()) {
                target.parent.left = null;
            } else {
                target.parent.right = null;
            }
            target.parent = null;
        }
    }

    public void fixAfterDeletion(Node x) {
        // 直到x是根节点或者x 的颜色是红色为止,否则一直处理
        while (x != root && colorOf(x) == BLACK) {
            if (x.isLeftChild()) {
                // s是x的兄弟节点
                Node sib = rightOf(parentOf(x));
                if (colorOf(sib) == RED) {
                    // 因为x 路径上将要少一个黑色,所以这时候就需要从它的父母兄弟那里借一个过来帮个忙了
                    setColor(sib, BLACK);
                    // sib是红色,则它的父节点原本一定是黑色,否则就违反了红黑树不能两个连续红色节点的规则了
                    setColor(parentOf(sib), RED);
                    rotateLeft(parentOf(x));
                    sib = rightOf(parentOf(x));
                }
                // sib的两个子节点都是黑色
                if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) {
                    // 设置sib 是红色是因为,x的子树下将少一个黑色节点,那就让它的兄弟节点的子树下也先少一个黑色节点
                    setColor(sib, RED);
                    // 让x指向它的父节点,如果x的父节点是红色,那么结束循环,然后将其设置为黑色,丢失的那个黑色节点也就补回来了
                    // 如果不是红色,那就继续往下调整
                    x = parentOf(x);
                } else {
                    //sib的子节点中只有一个是黑色
                    // 那就把这个红色子节点右旋放到右边子节点上,这是因为,x这边还少一个黑色节点呢,需要借一个黑色
                    // 最终sib 这边少一个黑色,将这个红色设置为黑色补上
                    if (colorOf(rightOf(sib)) == BLACK) {
                        setColor(leftOf(sib), BLACK);
                        setColor(sib, RED);
                        rotateRight(sib);
                        sib = rightOf(parentOf(x));
                    }
                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    setColor(rightOf(x), BLACK);
                    rotateLeft(parentOf(x));
                    x = root;
                }
            } else {
                // 如果x 是右子节点,就是上面的情况反过来
                Node sib = leftOf(parentOf(x));
                if (colorOf(sib) == RED) {
                    setColor(sib, BLACK);
                    setColor(parentOf(x), RED);
                    rotateRight(parentOf(x));
                    sib = leftOf(parentOf(x));
                }
                // sib的两个子节点都是黑色
                if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) {
                    // 设置sib 是红色是因为,x的子树下将少一个黑色节点,那就让它的兄弟节点的子树下也先少一个黑色节点
                    setColor(sib, RED);
                    // 让x指向它的父节点,如果x的父节点是红色,那么结束循环,然后将其设置为黑色,丢失的那个黑色节点也就补回来了
                    // 如果不是红色,那就继续往下调整
                    x = parentOf(x);
                } else {
                    if (colorOf(leftOf(sib)) == BLACK) {
                        setColor(rightOf(sib), BLACK);
                        setColor(sib, RED);
                        rotateLeft(sib);
                        sib = leftOf(parentOf(x));
                    }
                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    setColor(leftOf(x), BLACK);
                    rotateRight(parentOf(x));
                    x = root;
                }
            }
        }
        setColor(x, BLACK);
    }

    // 中序遍历
    public List<Node> inIterator(Node node) {
        List<Node> nodes = new ArrayList<Node>();
        if (node.left != null) {
            nodes.addAll(inIterator(node.left));
        }
        nodes.add(node);
        if (node.right != null) {
            nodes.addAll(inIterator(node.right));
        }
        return nodes;
    }

    public List<Node> inIterator() {
        return root != null ? inIterator(root) : new ArrayList<Node>(0);
    }

    private static class Node {
        Object data;
        Node parent;
        Node left;
        Node right;
        boolean color = BLACK;

        public Node(Object data) {
            this.data = data;
        }

        public Node(Object data, Node parent, Node left, Node right) {
            this.data = data;
            this.parent = parent;
            this.left = left;
            this.right = right;
        }

        public boolean isLeftChild() {
            return this == this.parent.left;
        }

        public boolean isRightChild() {
            return this == this.parent.right;
        }

        public boolean isRoot() {
            return this.parent == null;
        }

        @Override
        public String toString() {
            return data.toString();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj.getClass() == Node.class) {
                Node target = (Node) obj;
                return data.equals(target.data) && left == target.left && right == target.right && parent == target.parent && color == target.color;
            }
            return false;
        }
    }

    public static void main(String[] args) {
        RedBlackTree<Integer> tree = new RedBlackTree<Integer>();
        // 遍历的结果就该是:1,6,8,11,13,15,17,22,25,27
        Integer[] integers = {6, 15, 25, 8, 17, 22, 11, 1, 13, 27};
        for (Integer i :
                integers) {
            tree.add(i);
            System.out.println(tree.inIterator());
        }
        for (Integer i :
                integers) {
            tree.remove(i);
            System.out.println(tree.inIterator());
        }
    }
}

参考资料:

红黑树(red–black tree)- Wikipedia

《疯狂java:突破程序员基本功的16课》

评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不识君的荒漠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值