Java中红黑树的性质及其基本插入实现(TreeSet和TreeMap的底层原理)

红黑树的性质

红黑树是一种自平衡的二叉搜索树,它通过在每个节点上增加一个表示颜色的属性来保证树的大致平衡。红黑树有五个基本性质,这些性质共同确保了树的高度始终保持在 O(log n),从而保证了基本操作的时间复杂度为 O(log n)。

红黑树的五个基本性质:

  1. 节点颜色 每个节点要么是红色,要么是黑色。
  2. 根节点 根节点总是黑色的。
  3. 叶节点(NIL节点) 每个叶节点(NIL节点,空节点)都是黑色的。 注意:在许多实现中,叶节点会使用一个特殊的黑色哨兵节点来表示。
  4. 红色节点的子节点 如果一个节点是红色的,则它的两个子节点都必须是黑色的。 (也就是说,不能有两个连续的红色节点)
  5. 黑色深度对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。
  6. 前提需要是二叉搜索树

简单总结记忆口诀:左根右,根叶黑,不红红,黑路同

性质的作用:

  1. 性质 4 确保了从根到叶子的最长可能路径不会超过最短可能路径的两倍长。因为最短的路径全是黑色节点,最长的路径是红黑相间的。
  2. 性质 5 确保了树是大致平衡的。因为任意简单路径上黑色节点的数量相同,这就限制了树可能的不平衡程度。
  3. 这些性质共同确保了树的高度始终保持在 O(log n),其中 n 是树中节点的数量。
  4. 由于树的高度被限制在 O(log n),所以基本操作(如插入、删除和查找)的时间复杂度也就被限制在 O(log n)。

实际操作:

  • 插入操作:新插入的节点初始为红色。插入后,可能需要通过重新着色和旋转来恢复红黑树的性质。
  • 删除操作:删除节点后,可能需要进行一系列的重新着色和旋转操作来恢复红黑树的性质。
  • 查找操作:与普通的二叉搜索树相同。

重新着色和旋转来恢复红黑树的性质:

  1. 插入新节点

首先,我们像在普通的二叉搜索树中那样插入新节点,并将其染成红色。为什么是红色?因为这样不会违反性质5(每条路径上的黑色节点数量相同)。

  1. 检查和修复

插入后,我们需要检查是否违反了红黑树的性质,主要关注是否出现了连续的红色节点。我们从新插入的节点开始,向上检查。

  1. 情况分析

我们主要关注新节点(我们称为N)、它的父节点(P)、叔叔节点(U,即父节点的兄弟)和祖父节点(G)。

情况 1:N是根节点

  • 直接将N染成黑色。完成!

情况 2:P是黑色

  • 不需要做任何事。红黑树的性质没有被违反。完成!

情况 3:P是红色(这意味着G一定存在,且为黑色) 这里我们需要考虑U的颜色:

a. U是红色:

  • 将P和U都染成黑色
  • 将G染成红色
  • 将G作为新的N节点,继续向上检查

b. U是黑色(或不存在): 这里有四种子情况,取决于N和P相对于G的位置:

  • 左左(LL):P是G的左子节点,N是P的左子节点
  • 左右(LR):P是G的左子节点,N是P的右子节点
  • 右右(RR):P是G的右子节点,N是P的右子节点
  • 右左(RL):P是G的右子节点,N是P的左子节点

对于LL情况:

  1. 右旋G
  2. 交换P和G的颜色
 

对于RR情况:

  1. 左旋G
  2. 交换P和G的颜色
 

对于LR情况:

  1. 左旋P
  2. 将新的情况当作LL处理

对于RL情况:

  1. 右旋P
  2. 将新的情况当作RR处理
  3. 重复检查

每次调整后,我们都要继续向上检查,直到根节点,确保整棵树都符合红黑树的性质。

检查完成后,总是将根节点染成黑色,以确保性质2(根是黑色)成立。

这个过程看起来复杂,但实际上就是不断检查和调整,直到所有的红黑树性质都得到满足。我们只要时刻牢记关键的性质:

  • 新节点总是红色
  • 我们主要关心连续的红色节点
  • 叔叔的颜色决定了我们是重新着色还是旋转
  • 旋转用于平衡树,着色用于保持性质
  • 最后总是确保根是黑色的

红黑树的代码基本实现

public class RedBlackTree<T extends Comparable<T>> {

    // 定义红黑树节点的颜色
    private static final boolean RED = true;
    private static final boolean BLACK = false;

    // 内部节点类
    private class Node {
        T data;  // 节点存储的数据
        Node left, right, parent;  // 左子节点、右子节点和父节点
        boolean color;  // 节点的颜色

        Node(T data) {
            this.data = data;
            this.color = RED;  // 新节点默认为红色
        }
    }

    private Node root;  // 红黑树的根节点

    // 插入操作的公共方法
    public void insert(T data) {
        Node node = new Node(data);  // 创建新节点
        root = insert(root, node);  // 插入节点
        fixViolation(node);  // 修复可能违反的红黑树性质
    }

    // 递归插入节点的私有方法
    private Node insert(Node root, Node node) {
        if (root == null) return node;  // 如果树为空,新节点成为根节点

        // 根据比较结果决定插入到左子树还是右子树
        if (node.data.compareTo(root.data) < 0) {
            root.left = insert(root.left, node);
            root.left.parent = root;
        } else if (node.data.compareTo(root.data) > 0) {
            root.right = insert(root.right, node);
            root.right.parent = root;
        }

        return root;
    }

    // 修复红黑树性质的方法
    private void fixViolation(Node node) {
        Node parent = null;
        Node grandParent = null;

        // 当节点不是根节点,且节点是红色,其父节点也是红色时,需要修复
        while (node != root && node.color != BLACK && node.parent.color == RED) {
            parent = node.parent;
            grandParent = parent.parent;

            if (parent == grandParent.left) {  // 父节点是祖父节点的左子节点
                Node uncle = grandParent.right;

                if (uncle != null && uncle.color == RED) {
                    // Case 1: 叔叔节点是红色
                    grandParent.color = RED;
                    parent.color = BLACK;
                    uncle.color = BLACK;
                    node = grandParent;
                } else {
                    if (node == parent.right) {
                        // Case 2: 节点是父节点的右子节点
                        rotateLeft(parent);
                        node = parent;
                        parent = node.parent;
                    }

                    // Case 3: 节点是父节点的左子节点
                    rotateRight(grandParent);
                    boolean tempColor = parent.color;
                    parent.color = grandParent.color;
                    grandParent.color = tempColor;
                    node = parent;
                }
            } else {  // 父节点是祖父节点的右子节点
                Node uncle = grandParent.left;

                if (uncle != null && uncle.color == RED) {
                    // Case 1: 叔叔节点是红色
                    grandParent.color = RED;
                    parent.color = BLACK;
                    uncle.color = BLACK;
                    node = grandParent;
                } else {
                    if (node == parent.left) {
                        // Case 2: 节点是父节点的左子节点
                        rotateRight(parent);
                        node = parent;
                        parent = node.parent;
                    }

                    // Case 3: 节点是父节点的右子节点
                    rotateLeft(grandParent);
                    boolean tempColor = parent.color;
                    parent.color = grandParent.color;
                    grandParent.color = tempColor;
                    node = parent;
                }
            }
        }

        root.color = BLACK;  // 确保根节点始终为黑色
    }

    // 左旋操作
    private void rotateLeft(Node node) {
        Node rightChild = node.right;
        node.right = rightChild.left;

        if (node.right != null)
            node.right.parent = node;

        rightChild.parent = node.parent;

        if (node.parent == null)
            root = rightChild;
        else if (node == node.parent.left)
            node.parent.left = rightChild;
        else
            node.parent.right = rightChild;

        rightChild.left = node;
        node.parent = rightChild;
    }

    // 右旋操作
    private void rotateRight(Node node) {
        Node leftChild = node.left;
        node.left = leftChild.right;

        if (node.left != null)
            node.left.parent = node;

        leftChild.parent = node.parent;

        if (node.parent == null)
            root = leftChild;
        else if (node == node.parent.right)
            node.parent.right = leftChild;
        else
            node.parent.left = leftChild;

        leftChild.right = node;
        node.parent = leftChild;
    }

    // 其他方法如删除、查找等可以在这里实现
}

HashSet和HashMap

HashSet和HashMap在Java中都使用了红黑树作为其底层数据结构的一部分,但它们的使用方式略有不同。

  1. HashMap 中的红黑树:

HashMap 主要使用数组 + 链表 + 红黑树的结构:

  • 基本结构是一个数组,数组的每个元素是一个桶(bucket)。
  • 当发生哈希冲突时,相同哈希值的元素会被放到同一个桶中,形成一个链表。
  • 当一个桶中的元素数量超过一定阈值(默认为8)且数组长度大于等于64时,这个链表会被转换为红黑树。

具体实现:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
    // ... 其他方法
}

转换过程

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 如果数组为空或长度小于最小树化容量,则进行扩容而不是树化
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 计算桶的索引,并获取该桶的第一个节点
        
        TreeNode<K,V> hd = null, tl = null;  // 初始化树的头节点和尾节点
        do {
            // 将当前链表节点转换为TreeNode
            TreeNode<K,V> p = replacementTreeNode(e, null);
            
            if (tl == null)
                hd = p;  // 如果是第一个节点,设置为头节点
            else {
                p.prev = tl;  // 设置新节点的前驱
                tl.next = p;  // 将新节点链接到链表末尾
            }
            tl = p;  // 更新尾节点为当前节点
        } while ((e = e.next) != null);  // 遍历链表中的所有节点
        
        // 将转换后的TreeNode链表的头节点设置为该桶的第一个元素
        if ((tab[index] = hd) != null)
            hd.treeify(tab);  // 调用treeify方法将链表结构转换为红黑树结构
    }
}
  1. HashSet 中的红黑树:

HashSet 实际上是基于 HashMap 实现的。它的所有操作都是通过内部的 HashMap 来完成的。

public class HashSet<E> extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

    public HashSet() {
        map = new HashMap<>();
    }

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    // ... 其他方法
}

因此,HashSet 中的红黑树实现实际上就是 HashMap 的实现。

主要区别:

  1. HashMap 直接使用红黑树来存储键值对。
  2. HashSet 通过 HashMap 间接使用红黑树,只存储键(HashSet 的元素),值都是同一个虚拟对象 PRESENT。

使用红黑树的好处:

  1. 提高性能:当桶中元素较多时,红黑树的查询性能(O(log n))比链表(O(n))更好。
  2. 平衡:红黑树保证了各种操作的最坏情况时间复杂度为 O(log n)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值