二叉树实战(九):红黑树

我们知道2-3树能保证在插入元素后,树依然保持平衡状态,它最坏的情况就是所有的子节点都是2-节点,这样树的高度为logN,但是和普通的二叉查找树相比,最坏的情况下树的高度为N,确实能保证最坏情况下的时间复杂度,但是2-3树实现起来过于复杂,所以我们这里介绍一种基于2-3树实现的树结构 — 红黑树

一、红黑树的定义

红黑树主要是对2-3树进行编码,红黑树的基本思想就是用标准的二叉查找树(完全有2-结点组成)和一些额外的信息(替换3-结点)来表示2-3树。

红黑树将树中的链接分为两种类型:

  • 红链接:将两个2-结点链接起来构成一个3-结点;
  • 黑链接:2-3树中的普通链接。

确切的说,红黑树就是将3-结点表示为由一条左斜的红色链接(两个2-结点,一个是另一个的左子节点)相连的两个2-结点。具体的结构图如下:

这样表示2-3树的优点就是,我们无需修改,就可以使用标准二叉查找树的get方法。

这里再说明一下红黑树的特点:

  1. 红链接皆为左链接;
  2. 没有任何一个结点能同时和两条红链接相连(如果相连了就是4-结点了);
  3. 该树是完美黑色平衡的,即在任意空链接到根连接的黑链数量是相同的。

这里我们把上图的红色链接进行水平绘制,可以看出来,就是一个标准的2-3树了。

这也说明红黑树就是基于2-3树进行实现的。

二、红黑树的结点定义

因为每个结点都会只有一条指向自己的链接(从父节点指向自己),我们可以在Node结点中添加一个Boolean类型的color来表示链接的颜色。如果指向它的链接是红色的,那么该变量的值为true,如果链接是黑色的,那么该变量的值为false.

结点定义的示意图如下:

代码实现:

    private class Node<Key, Value> {

        public Key key; // 存储结点的键
        public Value value; // 存储结点的值

        public Node left; // 记录左子节点
        public Node right; // 记录右子节点

        public boolean color; // 记录其父结点指向它的链接的颜色

        public Node(Key key, Value value, Node left, Node right, boolean color) {
            this.key = key;
            this.value = value;
            this.left = left;
            this.right = right;
            this.color = color;
        }
    }

三、红黑树的平衡化

在上面我讲了红黑树的特点:

  1. 红链接皆为左链接;
  2. 没有任何一个结点能同时和两条红链接相连(如果相连了就是4-结点了);
  3. 该树是完美黑色平衡的,即在任意空链接到根连接的黑链数量是相同的。

当我们在对红黑树进行了一些增删操作之后,很有可能出现红色的右链接或者两条连续红色的链接,但是出现这些情况会使得树不满足我们对红黑树的定义,所以我们需要通过旋转操作来进行修复,让红黑树继续保持平衡。

1、左旋

当某个结点的左子结点为黑色,右子结点为红色的时候,需要进行左旋,具体示意图如下:

这里的E值对应的结点为h,它的右子结点为x。左旋过程如下:

  1. 让x的左子节点变为h的右子结点:h.right = x.left
  2. 让h称为x的左子结点:x.left = h
  3. 让x的color属性变为h的color属性值:x.color = h.color
  4. 让h的color属性变为RED:h.color = true

2、右旋

当某个结点的左子结点是红色,且左子结点的左子结点也是红色的时候,需要进行右旋。具体示意图如下:

 这里当前结点为h,它的左子结点为x。右旋过程如下:

  1. 让x的右子结点作为h的左子结点:h.left = x.right
  2. 让h称为x的右子结点:x.right = h
  3. 让x的color变为h的color:x.color = h.color
  4. 让h的color为RED

3、颜色反转

当一个结点的左子结点和右子结点的color都为RED的时候,也就是出现了临时的4-结点,此时只需要把左子结点和右子结点的颜色变为Black,同时让当前结点的颜色变为Red即可。

这里需要注意的是,结点的color属性指的是父结点指向当前结点的连接的颜色额,由于根结点不存在父结点,所以每次插入操作后,我们都需要把根节点的颜色设置为黑色。

四、红黑树的代码实现

/**
 * 红黑树的实现
 */
public class RedBlackTree<Key extends Comparable<Key>, Value> {

    public static void main(String[] args) {
        //创建红黑树
        RedBlackTree<String, String> tree = new RedBlackTree<>();

        //往树中插入元素
        tree.put("1","张三");
        tree.put("2","李四");
        tree.put("3","王五");
        //从树中获取元素
        String r1 = tree.get("1");
        System.out.println(r1);

        String r2 = tree.get("2");
        System.out.println(r2);

        String r3 = tree.get("3");
        System.out.println(r3);
    }

    private static final boolean RED = true;  // 红色链接标识
    private static final boolean BLACK = false;  // 黑色链接标识

    private Node root;  //  记录根结点
    private int N; // 记录树中元素的个数


    // 左旋转
    private Node rotateLeft(Node h) {
        // 获取h节点的右子结点,标为x
        Node x = h.right;
        // 让x结点的左子结点为h的右子结点
        h.right = x.left;
        // 让h称为x结点的左子结点
        x.left = h;
        // 让x的color属性 = h的color属性
        x.color = h.color;
        // 让h的color = RED
        h.color = RED;
        return x;
    }

    // 右旋转
    private Node rotateRight(Node h) {
        // 获取h结点的左子结点,标为x
        Node x = h.left;
        // 让x节点的右子结点称为h结点的左子结点
        h.left = x.right;
        // 让h结点称为x结点的右子结点
        x.right = h;
        // 让x的color = h的color
        x.color = h.color;
        // 让h的color = RED
        h.color = RED;
        return x;
    }

    // 颜色反转,相当于拆分4-结点
    private void flipColors(Node h) {
        // 当前结点变为红色
        h.color = RED;
        // 左子结点和右子结点变为黑色
        h.left.color = BLACK;
        h.right.color = BLACK;
    }

    // API - 插入操作
    public void put(Key key, Value value) {
        root = put(root, key, value);
        // 保证根结点的颜色总为黑色
        root.color = BLACK;
    }

    // 内部 - 向指定树插入元素,并返回添加元素后的新树
    private Node put(Node h, Key key, Value value) {
        // 判断h是否为空,如果为空,则直接返回一个红色的结点
        if (h == null) {
            N++;
            return new Node(key, value, null, null, RED);
        }
        // 比较h的键和k的大小
        int cmp = key.compareTo(h.key);
        if (cmp < 0) {
            // 继续往左
            h.left = put(h.left, key, value);
        } else if (cmp > 0) {
            // 继续往右
            h.right = put(h.right, key, value);
        } else if (cmp == 0) {
            // 替换
            h.value = value;
        }
        // 进行左旋
        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;
    }

    // 判断当前结点的父节点指向链接是否为红色
    private boolean isRed(Node x) {
        if (x == null) {
            return false;
        }
        return x.color == RED;
    }

    // 获取树中元素的个数
    private int getSize() {
        return N;
    }

    //根据key,从树中找出对应的值
    public Value get(Key key) {
        return get(root,key);
    }

    //从指定的树x中,查找key对应的值
    public Value get(Node x, Key key) {
        if (x == null){
            return null;
        }

        //比较x结点的键和key的大小
        int cmp = key.compareTo(x.key);
        if (cmp<0){
            return get(x.left,key);
        }else if (cmp>0){
            return get(x.right,key);
        }else{
            return x.value;
        }
    }

    private class Node {

        public Key key; // 存储结点的键
        public Value value; // 存储结点的值

        public Node left; // 记录左子节点
        public Node right; // 记录右子节点

        public boolean color; // 记录其父结点指向它的链接的颜色

        public Node(Key key, Value value, Node left, Node right, boolean color) {
            this.key = key;
            this.value = value;
            this.left = left;
            this.right = right;
            this.color = color;
        }
    }

}

输出结果:

张三
李四
王五

 

posted @ 2020-07-05 16:18  灰色飘零  阅读(126)  评论(0)    收藏  举报