2-3查找树与红黑二叉查找树

2-3树

1. 2-3树的定义

一颗2-3查找树或为一颗空树,或由以下节点组成:

  • 2-节点,含有一个键(及其对应的值)和两条链接,左链接指向的2-3树中的键都小于该节点,右链接指向的2-3树中的键大于该节点。
  • 3-节点,含有两个键(及其对应的值)和三条链接,左连接指向的2-3树中的键都小于该节点,中链接指向的2-3树中的键都位于该节点的两个键之间,右链接指向的2-3树中的键大于该节点。
2-3查找树示意图.png

一颗完美平衡的2-3查找树中的所有空连接到根节点的距离都是相同的。

2. 2-3树的查找

将二叉查找树的查找算法一般化就能够直接得到2-3树的查找算法。要判断一个键是否存在树中,我们先将它和根节点中的键进行比较。如果它和其中任意一个相等,查找命中;否则我们就根据比较的结果找到指向相应区间的链接,并在其指向的子树中递归地继续查找。如果这是个空链接,查找未命中。


2-3树中的查找命中(左)和未命中(右).png

3. 向2-3树中插入新键

要在2-3树中插入一个新节点,可以和二叉树一样先进行一次未命中的查找,然后把新节点挂在树的底部。但是这样的话无法保持完美平衡性。我们使用2-3树的主要原因就在于它能够在插入后继续保持平衡。

3.1 向2-节点中插入新键

只要把这个2-节点替换成一个3-节点,将要插入的键保存在其中即可。


向2-节点中插入新的键.png
3.2 向一颗只含有一个3-节点的树中插入新键
向一颗只含有一个3-节点的树中插入新键.png
3.3 向一个父节点为2-节点的3-节点中插入新键

先构建一个临时的4-节点并将其分解,将中键移动至原来的父节点中,形成一个新的3-节点。


向一个父节点为2-节点的3-节点中插入新键.png
3.4 向一个父节点为3-节点的3-节点中插入新键

推广到一般情况,一直向上不断分解临时的4-节点并将中键插入到更高层的父节点,直到遇到一个2-节点并将它替换为一个不需要继续分解的3-节点,或者是到达3-节点的根。


向一个父节点为3-节点的3-节点中插入新键.png

如果从插入节点到根节点的路径上全都是3-节点,我们的根节点最终变成一个临时的4-节点。此时可以按照向一颗只有一个3-节点的树中插入新键的方法处理这个问题。将临时4-节点分解为3个2-节点,使得树高加1。请注意,这次最后的变换仍然保持了树的完美平衡性,因为它变换的是根节点。

插入新键总结
  1. 总是先将新键融合进查找结束的节点,如果是2-节点,直接保存到其中;
  2. 如果是3-节点,先构建成一个临时的4-节点,然后对父节点分解为3个2节点,将中键与原父节点融合;
  3. 若原父节点是2-节点,则融合为一个新的3-节点,3-节点符合2-3树定义;
  4. 若源父节点是3-节点,融合成的是一个4-节点,4-节点需要分解,回到步骤2,以此类推,不断向上分解,直到遇到一个2-节点,将它融合后一个新的3-节点;
  5. 若一直向上分解到根节点,根节点是一个3-节点(根节点融合成临时的4-节点),则需要将根节点分解成3个2-节点,树高加1。

红黑树

红黑二叉查找树背后的思想是用标准的二叉查找树(完全由2-节点构成)和一些额外的信息(替换3-节点)来表示2-3树。将树中的链接分为两种类型:红链接将两个2-节点连接起来构成一个3-节点,黑链接则是2-3树中的普通链接。确切地说,我们将3-节点表示为由一条左斜的红色链接相连的两个2-节点。这种表示法的一个优点是,无需修改就可以直接使用标准二叉树的get()方法。对于2-3树,只需要对节点进行转换,就可以立即派生出一颗对应的二叉查找树。我们将这种表示2-3树的二叉查找树称为红黑二叉查找树。所有基于红黑树的符号表实现都能保证操作的运行时间未对数级别。

1.红黑树的性质

  1. 每个节点或者是红色的,或者是黑色的;
  2. 根节点是黑色的;
  3. 每个叶子节点(最后的空节点)是黑色的;
  4. 如果 一个节点是红色的,那么它的孩子节点都是黑色的;
  5. 从任意一个节点到叶子节点,经过的黑色节点是一样的(红黑树以保持黑平衡的二叉树)。
    红黑树既是二叉查找树,也是2-3树,它综合了二叉查找树中简洁高效的查找方法和2-3树中高效的平生插入算法。

2.红黑树与2-3树的一一对应关系

红黑树和2-3树的一一对应关系.png

3.红黑树的节点表示

红黑树的节点表示.png

代码表示

// 定义颜色的两个常量
private static final boolean RED = true;
private static final boolean BLACK = false;

private class Node {
      private K key;
      private V value;
      private Node left;
      private Node right;
      private boolean color;    // 节点颜色

      public Node(K key, V value) {
          this.key = key;
          this.value = value;
          this.left = null;
          this.right = null;
          this.color = RED;      // 添加节点时,默认是红节点,相当于2-3树的添加时的融合操作
      }
}

/**
  * 定义一个辅助方法,判断节点的颜色
  */
private boolean isRed(Node node) {
      if (node == null)
          return BLACK;
      return node.color;
}

4.节点的旋转

4.1 左旋转

红黑树插入一个新键,必定是红色的节点,因为在2-3树中插入一个新键总是先融合到已有的节点,这里红节点即代表节点要进行融合操作。
若新键插入到已有节点(假设是黑色的)的右侧,形成一个右红链接,则需要进行性左旋转没确保红链接保持是左连接。

/**
 *       h                     x
 *     /  \    左旋转         /  \
 *    T1   X   -----------> h   T3
 *        / \              / \
 *       T2 T3            T1 T2
 *
 */
private Node leftRotate(Node h) {
    Node x = h.right;

    // 左旋转
    h.right = x.left;
    x.left = h;

    x.color = h.color;
    h.color = RED;

    return x;
}
4.2 右旋转

若需要插入节点A,经查找要插入到节点E的左侧,假设E节点是红色的,E节点的父节点S节点是黑色的,即E、S节点在2-3树中是一个3节点,此时A、E、S形成一个临时的4-节点,需要拆分成3个2节点。故需要对S节点的左链接进行右旋转,E经过右旋转后变成根节点,颜色要与原根节点保持一致(S节点颜色是黑色的),S节点变为E节点的右孩子,在E节点还未向上融合之前,A、E、S还是一个临时4-节点,所以S节点需要置为红色,代表与E还是融合在一起的。如图所示。

代码实现

/**
 *       h                     x
 *     /  \    右旋转         /  \
 *    x    T2   -----------> y   h
 *   / \                        / \
 *  y  T1                     T1 T2
 *
 */
private Node rightRotate(Node h) {
    Node x = h.left;

    // 右旋转
    h.left = x.right;
    x.right = h;

    x.color = h.color;
    h.color = RED;

    return x;
}
红黑树的左旋转与右旋转.png

5.颜色的翻转

接4.2,A、E、S拆分成3个2-节点之后,E这个2-节点需要向上与其父节点进行融合(2-3的性质),融合的2-节点必须是红色的,但是此时E节点是黑色的,与此同时A、S这两个节点是红色的,故可以对A、E、S这三个节点进行颜色的翻转,即E(黑->红),A、S(红->黑)。
代码实现

/**
 * 颜色翻转
 */
private void filpColor(Node node) {
    node.color = RED;
    node.left.color = BLACK;
    node.right.color = BLACK;
}

6.向红黑树中插入新键

红黑树插入新键可以等价于向2-3树的2-节点或者3-节点插入新键。

6.1 向2-节点插入新键
  • 若插在2-节点的左侧,则直接插入;
  • 若插在2-节点的右侧,则需要进行一次左旋转。
6.2 向3-节点插入新键
红黑树添加新键.png

向一个3-节点插入新键有三种情况,如上图所示:

  1. 新键所在位置为根节点的左节点的右节点,需要依次进行左旋转->右旋转->颜色翻转;
  2. 新键所在位置为根节点的左节点的左节点,需要依次进行右旋转->颜色翻转;
  3. 新键所在位置为根节点的右节点,只需要进行颜色翻转。
    可以看出三种情况可以公用一个流程链,最坏的情况①需要进行三个步骤,才能正确的添加新键。

代码实现

/**
 * 向以node为根节点的红黑树中插入元素,递归算法
 * 返回插入新节点后红黑树的根
 */
private Node addNode(Node node, K key, V value) {
    if (node == null) {
        size++;
        return new Node(key, value);
    }

    if (key.compareTo(node.key) < 0)
        node.left = addNode(node.left, key, value);
    else if (key.compareTo(node.key) > 0)
        node.right = addNode(node.right, key, value);
    else
        node.value = value;

    // 如果h的右节点是红色的,左节点不是红色的,需要进行性左旋转
    if (isRed(node.right) && !isRed(node.left))
        node = leftRotate(node);

    // 如果h的左节点是红色的,h的左节点的左节点也是红色的,需要进行性右旋转
    if (isRed(node.left) && isRed(node.left.left))
        node = rightRotate(node);

    // 如果h的左右节点都是红色的,进行颜色翻转
    if (isRed(node.left) && isRed(node.right))
        filpColor(node);

    return node;
}

7.红黑树的完整实现

public class RBT, V> {

    private static final boolean RED = true;

    private static final boolean BLACK = false;

    private class Node {
        private K key;
        private V value;
        private Node left;
        private Node right;
        private boolean color;    // 节点颜色

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
            this.left = null;
            this.right = null;
            this.color = RED;      // 添加节点时,默认是红节点,相当于2-3树的添加时的融合操作
        }
    }

    private Node root;
    private int size;

    public RBT() {
        root = null;
        size = 0;
    }

    public int getSize() {
        return size;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 判断节点的颜色
     */
    private boolean isRed(Node node) {
        if (node == null)
            return BLACK;
        return node.color;
    }

    /**
     *       h                     x
     *     /  \    左旋转         /  \
     *    T1   X   -----------> h   T3
     *        / \              / \
     *       T2 T3            T1 T2
     *
     */
    private Node leftRotate(Node h) {
        Node x = h.right;
    
        // 左旋转
        h.right = x.left;
        x.left = h;
    
        x.color = h.color;
        h.color = RED;
    
        return x;
    }
    
    /**
     *       h                     x
     *     /  \    右旋转         /  \
     *    x    T2   -----------> y   h
     *   / \                        / \
     *  y  T1                     T1 T2
     *
     */
    private Node rightRotate(Node h) {
        Node x = h.left;
    
        // 右旋转
        h.left = x.right;
        x.right = h;
    
        x.color = h.color;
        h.color = RED;
    
        return x;
    }
    
    /**
     * 颜色翻转
     */
    private void filpColor(Node node) {
        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }

    /**
     * 向红黑树中添加新的元素
     */
    public void addNode(K key, V value) {
        root = addNode(root, key, value);
        root.color = BLACK;     // 最终根节点为黑色节点
    }

/**
 * 向以node为根节点的红黑树中插入元素,递归算法
 * 返回插入新节点后红黑树的根
 */
private Node addNode(Node node, K key, V value) {
    if (node == null) {
        size++;
        return new Node(key, value);
    }

    if (key.compareTo(node.key) < 0)
        node.left = addNode(node.left, key, value);
    else if (key.compareTo(node.key) > 0)
        node.right = addNode(node.right, key, value);
    else
        node.value = value;

    // 如果右节点是红色的,左节点是黑色的,需要进行性左旋转
    if (isRed(node.right) && !isRed(node.left))
        node = leftRotate(node);

    // 如果左节点是红色的,左节点的左节点也是红色的,需要进行性右旋转
    if (isRed(node.left) && isRed(node.left.left))
        node = rightRotate(node);

    // 如果左右节点都是红色的,进行颜色翻转
    if (isRed(node.left) && isRed(node.right))
        filpColor(node);

    return node;
}

    /**
     * 返回以node为根节点的红黑树中,key所在的节点
     * 与二分搜索树一致
     */
    public Node getNode(Node node, K key) {
        if (node == null)
            return null;

        if (key.equals(node.key))
            return node;
        if (key.compareTo(node.key) < 0)
            return getNode(node.left, key);
        else
            return getNode(node.right, key);
    }

    public boolean contains(K key) {
        return getNode(root, key) != null;
    }

    public void set(K key, V newValue) {
        Node node = getNode(root, key);
        if (node == null)
            throw new IllegalArgumentException(key + "doesn't exist");

        node.value = newValue;
    }

    // 返回以node为根的二分搜索树的最小值所在的节点
    private Node minimum(Node node){
        if(node.left == null)
            return node;
        return minimum(node.left);
    }

    // 删除掉以node为根的二分搜索树中的最小节点
    // 返回删除节点后新的二分搜索树的根
    private Node removeMin(Node node){
        if (node.left == null) {
            Node rightNode = node.right;
            node.right = null;  // 删除根节点
            size--;
            return rightNode;
        }

        removeMin(node.left);
        return node;
    }

    // 从二分搜索树中删除键为key的节点
    public V remove(K key) {
        Node node = getNode(root, key);
        if (node != null) {
            root = remove(root, key);
            return node.value;
        }
        return null;
    }

    private Node remove(Node node, K key){
        if (node == null)
            return null;

        if (key.compareTo(node.key) < 0) {
            node.left = remove(node.left, key);
            return node;
        } else if (key.compareTo(node.key) > 0) {
            node.right = remove(node.right, key);
            return node;
        } else {
            // 左节点为空
            if (node.left == null) {
                Node rightNode = node.right;
                node.right = null;  // 删除根节点
                size--;
                return rightNode;
            }

            // 右节点为空
            if (node.right == null) {
                Node leftNode = node.left;
                node.left = null;
                size--;
                return leftNode;
            }

            // 待删除节点左右子树均不为空的情况

            // 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点
            // 用这个节点顶替待删除节点的位置
            Node successor = minimum(node.right);
            successor.right = removeMin(node.right);
            successor.left = node.left;

            node.left = node.right = null;

            return successor;
        }
    }
}

你可能感兴趣的:(2-3查找树与红黑二叉查找树)