数据结构——红黑树

在计算机科学中,平衡二叉搜索树是许多高效数据结构的基础。红黑树(Red-Black Tree)作为一种自平衡二叉搜索树,在保证操作时间复杂度为O(log n)的同时,通过巧妙的颜色约束实现了相对较低的维护成本。本文将深入剖析红黑树的原理、核心操作,并结合Java集合框架中的TreeMap源码,带你彻底掌握这一经典数据结构。

一、为什么需要红黑树?

二叉搜索树在极端情况下会退化为链表,导致操作复杂度降至O(n)。为了保持平衡,人们发明了多种平衡树,如AVL树、红黑树等。与AVL树严格的高度平衡不同,红黑树通过"近似平衡"来减少旋转次数,从而在插入、删除频繁的场景下表现更优。Java的TreeMapTreeSet以及HashMap中的链表树化(当链表长度超过阈值时,会将链表转换为红黑树)都基于红黑树实现。

二、红黑树的定义与性质

红黑树是一棵满足以下五条性质的二叉搜索树:

  1. 每个节点是红色或黑色。

  2. 根节点是黑色。

  3. 每个叶子节点(NIL节点,即空节点)是黑色。

    通常实现中,我们用null代表叶子节点,并认为其颜色为黑色。

  4. 如果一个节点是红色,则它的两个子节点都是黑色。

    这条性质保证了没有两个连续的红色节点。

  5. 对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。

    这条性质称为"黑色平衡",它确保了红黑树的最长路径不超过最短路径的两倍,从而保证了操作的时间复杂度为O(log n)。

这些性质共同保证了红黑树的高度最多为2log₂(n+1),因此查找、插入、删除的时间复杂度均为O(log n)。

三、红黑树节点定义

在Java中,红黑树节点通常包含键、值、左子节点、右子节点、父节点以及颜色标记。以TreeMap中的Entry类为例:

java

复制代码
static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK; // 默认为黑色

    // 构造函数、getter、setter等省略
}

四、核心操作:旋转

红黑树的插入和删除操作会破坏其性质,需要通过重新着色和旋转来修复。旋转分为左旋和右旋,它们是局部调整树结构的原子操作,不会改变二叉搜索树的性质。

左旋

java

复制代码
// 左旋操作:将节点p向右旋转,使p的右子节点成为新的父节点
private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> r = p.right;
        p.right = r.left;
        if (r.left != null)
            r.left.parent = p;
        r.parent = p.parent;
        if (p.parent == null)
            root = r;
        else if (p.parent.left == p)
            p.parent.left = r;
        else
            p.parent.right = r;
        r.left = p;
        p.parent = r;
    }
}

右旋

java

复制代码
// 右旋操作:将节点p向左旋转,使p的左子节点成为新的父节点
private void rotateRight(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> l = p.left;
        p.left = l.right;
        if (l.right != null)
            l.right.parent = p;
        l.parent = p.parent;
        if (p.parent == null)
            root = l;
        else if (p.parent.right == p)
            p.parent.right = l;
        else
            p.parent.left = l;
        l.right = p;
        p.parent = l;
    }
}

五、插入操作与修复

插入新节点时,我们首先按照二叉搜索树的规则找到插入位置,然后将新节点染成红色(这样不会破坏性质5,但可能破坏性质4)。如果父节点是黑色,插入完成;如果父节点是红色,则出现连续红色,需要根据叔叔节点的颜色进行修复。

插入修复的三种情况(假设新节点为z,父节点为p,祖父节点为g,叔叔节点为u):

  1. 叔叔节点u是红色

    将p和u染黑,g染红,然后将z指向g,继续向上修复。

  2. 叔叔节点u是黑色,且z是p的右子节点(LL双红)

    对p进行左旋,转化为情况3,然后按情况3处理。

  3. 叔叔节点u是黑色,且z是p的左子节点(LR双红)

    将p染黑,g染红,然后对g进行右旋。

插入修复代码(来自TreeMap

java

复制代码
private void fixAfterInsertion(Entry<K,V> x) {
    x.color = RED;

    while (x != null && x != root && x.parent.color == RED) {
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {                     // 情况1:叔叔为红色
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == rightOf(parentOf(x))) {         // 情况2:当前节点是右孩子
                    x = parentOf(x);
                    rotateLeft(x);
                }
                // 情况3:当前节点是左孩子
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            // 对称处理
            // ...
        }
    }
    root.color = BLACK;
}

六、删除操作与修复

删除操作相对复杂。首先找到实际被删除的节点(如果待删除节点有两个子节点,则用后继节点替换,然后转为删除后继节点)。删除后,如果被删除节点是黑色,则会破坏性质5,需要进行修复。

删除修复的核心是处理"双黑"节点,通过旋转和着色恢复平衡。修复过程根据兄弟节点的颜色和子节点的情况分为多种情形。由于篇幅所限,这里只列出修复的主要思想,具体可参考TreeMap中的fixAfterDeletion方法。

七、红黑树 vs. AVL树

特性 红黑树 AVL树
平衡条件 近似平衡(最长路径不超过最短路径的两倍) 严格平衡(左右子树高度差≤1)
查找效率 O(log n),略逊于AVL O(log n),更严格,查找更快
插入/删除效率 旋转次数少,平均性能更好 可能需要多次旋转,但总体仍是O(log n)
适用场景 插入删除频繁,如TreeMapHashMap内部 查找操作远多于插入删除的场景

八、Java中的红黑树应用

1. TreeMap和TreeSet

TreeMap使用红黑树存储键值对,所有操作(put、get、remove)都依赖于红黑树的算法。TreeSet则基于TreeMap实现,只使用键。

2. HashMap中的树化

当HashMap中某个桶的链表长度超过TREEIFY_THRESHOLD(默认8)时,会将链表转换为红黑树,以提高查找效率。当红黑树节点数量减少到UNTREEIFY_THRESHOLD(默认6)时,又会退化为链表。

九、示例:手写一个简单的红黑树

下面给出一个极简的红黑树实现,只包含插入操作,帮助理解核心流程。

java

复制代码
public class RBTree<K extends Comparable<K>, V> {
    private static final boolean RED = true;
    private static final boolean BLACK = false;

    private Node root;

    private class Node {
        K key;
        V value;
        Node left, right, parent;
        boolean color;

        Node(K key, V value, Node parent) {
            this.key = key;
            this.value = value;
            this.parent = parent;
            this.color = RED; // 新节点为红色
        }
    }

    public void put(K key, V value) {
        if (root == null) {
            root = new Node(key, value, null);
            root.color = BLACK;
            return;
        }
        Node parent = null;
        Node cur = root;
        while (cur != null) {
            parent = cur;
            int cmp = key.compareTo(cur.key);
            if (cmp < 0) cur = cur.left;
            else if (cmp > 0) cur = cur.right;
            else {
                cur.value = value; // 更新值
                return;
            }
        }
        Node newNode = new Node(key, value, parent);
        if (key.compareTo(parent.key) < 0) parent.left = newNode;
        else parent.right = newNode;
        fixAfterInsertion(newNode);
    }

    private void fixAfterInsertion(Node x) {
        x.color = RED;
        while (x != null && x != root && x.parent.color == RED) {
            if (x.parent == x.parent.parent.left) {
                Node y = x.parent.parent.right; // 叔叔节点
                if (y != null && y.color == RED) {
                    // 情况1
                    x.parent.color = BLACK;
                    y.color = BLACK;
                    x.parent.parent.color = RED;
                    x = x.parent.parent;
                } else {
                    if (x == x.parent.right) {
                        // 情况2
                        x = x.parent;
                        rotateLeft(x);
                    }
                    // 情况3
                    x.parent.color = BLACK;
                    x.parent.parent.color = RED;
                    rotateRight(x.parent.parent);
                }
            } else {
                // 对称处理
                Node y = x.parent.parent.left;
                if (y != null && y.color == RED) {
                    x.parent.color = BLACK;
                    y.color = BLACK;
                    x.parent.parent.color = RED;
                    x = x.parent.parent;
                } else {
                    if (x == x.parent.left) {
                        x = x.parent;
                        rotateRight(x);
                    }
                    x.parent.color = BLACK;
                    x.parent.parent.color = RED;
                    rotateLeft(x.parent.parent);
                }
            }
        }
        root.color = BLACK;
    }

    // 左旋与右旋实现(与TreeMap类似,此处省略具体代码)
    private void rotateLeft(Node p) { /* 实现略 */ }
    private void rotateRight(Node p) { /* 实现略 */ }
}

十、总结

红黑树通过引入颜色属性和五条约束,实现了近似平衡的二叉搜索树。其插入、删除操作虽然需要处理多种情况,但通过旋转和着色可以在常数次操作内恢复平衡。在Java中,TreeMapTreeSetHashMap(在冲突严重时)都依赖于红黑树,是学习数据结构与算法的经典范例。理解红黑树不仅能帮助我们更好地使用这些集合类,也为设计高性能的数据结构提供了坚实的理论基础。

相关推荐
CoovallyAIHub2 小时前
传感器数据相互矛盾时,无人机蜂群如何做出可靠的管道泄漏检测决策?
算法·架构·无人机
CoovallyAIHub2 小时前
Claude Code Review:多 Agent 自动审查 PR,代码产出翻倍后谁来把关?
算法·架构·github
jyan_敬言2 小时前
【算法】高精度算法(加减乘除)
c语言·开发语言·c++·笔记·算法
树獭叔叔2 小时前
内存价格被Google打下来了?: TurboQuant对KVCache的量化
算法·aigc·openai
旖-旎2 小时前
前缀和(矩阵区域和)(8)
c++·算法·leetcode·前缀和·动态规划
月落归舟2 小时前
排序算法---(一)
数据结构·算法·排序算法
liuyao_xianhui2 小时前
优选算法_翻转链表_头插法_C++
开发语言·数据结构·c++·算法·leetcode·链表·动态规划
Book思议-3 小时前
【数据结构实战】循环队列FIFO 特性生成六十甲子(天干地支纪年法),实现传统文化里的 “时间轮回”
数据结构·算法·
im_AMBER3 小时前
Leetcode 147 零钱兑换 | 单词拆分
javascript·学习·算法·leetcode·动态规划