【java-core-collections】红黑树深度解析

红黑树深度解析

一、什么是红黑树

红黑树是一种自平衡二叉搜索树,每个节点额外存储一位颜色(红或黑),通过颜色约束来保持树的近似平衡,保证最坏情况下操作的时间复杂度为 O(log n)。

为什么需要红黑树? 二叉搜索树在极端情况下退化为链表(O(n)),AVL 树平衡条件太严格导致频繁旋转。红黑树在两者之间取了折中。

二、红黑树的五条性质

编号 性质 说明
1 每个节点是红色或黑色 -
2 根节点是黑色 -
3 NIL 叶子节点(空节点)是黑色 -
4 红色节点的两个子节点都是黑色 不能有连续红节点
5 从任一节点到其所有叶子节点的路径都包含相同数目的黑色节点 黑高相同

推论: 最长路径不超过最短路径的 2 倍。最短路径全黑,最长路径红黑交替。

复制代码
        B(13)
       /      \
     R(8)     R(17)
    /   \     /   \
  B(1) B(11) B(15) B(25)
  / \   / \   / \   / \
 NIL NIL ...  ...   ...

三、核心操作

3.1 左旋与右旋

旋转是红黑树维持平衡的基本操作,不改变 BST 性质。

复制代码
左旋(以 x 为轴):           右旋(以 y 为轴):

    x                           y
   / \                         / \
  a   y        ←→             x   c
     / \                     / \
    b   c                   a   b
java 复制代码
// 左旋伪代码
void leftRotate(Node x) {
    Node y = x.right;
    x.right = y.left;       // y 的左子树挂到 x 右边
    if (y.left != NIL) y.left.parent = x;
    y.parent = x.parent;    // y 接替 x 的位置
    if (x.parent == NIL) root = y;
    else if (x == x.parent.left) x.parent.left = y;
    else x.parent.right = y;
    y.left = x;             // x 变成 y 的左子
    x.parent = y;
}

3.2 插入

规则:新插入的节点默认为红色(避免违反性质 5)。

插入后通过变色 + 旋转修复平衡:

情况 父节点 叔叔节点 操作
1 - - 新节点是根 → 变黑
2 黑色 - 无需修复(不违反任何性质)
3 红色 红色 父、叔变黑,祖父变红,递归修复祖父
4 红色(左) 黑色/NIL 父变黑,祖父变红,以祖父为轴右旋
5 红色(右) 黑色/NIL 先以父为轴左旋,转为情况 4
复制代码
插入示例(依次插入 10, 20, 30):

1. 插入 10: [B(10)]                    → 根变黑
2. 插入 20: [B(10) → R(20)]            → 父黑,无需修复
3. 插入 30: [B(10) → R(20) → R(30)]    → 父红叔黑
   修复: 左旋 10,变色
   结果:
       B(20)
      /    \
    R(10)  R(30)

3.3 删除

删除比插入复杂,分两步:

  1. BST 删除:找到后继节点替换,实际被删除的最多只有一个子节点
  2. 修复平衡:如果删除的是黑色节点,违反性质 5,需要修复

修复核心思想:将兄弟节点的"多余黑色"转移上来。

情况 兄弟颜色 兄弟子节点 操作
1 - 兄变黑,父变红,旋转,转换其他情况
2 两子均黑 兄变红,向上递归
3 远子黑近子红 旋转使远子变红,转情况 4
4 远子红 调整颜色 + 旋转,完成修复

四、红黑树 vs AVL 树

特性 红黑树 AVL 树
平衡条件 最长路径 ≤ 2 倍最短路径 左右子树高度差 ≤ 1
插入旋转次数 最多 2 次 最多 O(log n) 次
删除旋转次数 最多 3 次 最多 O(log n) 次
查找性能 略慢(不那么平衡) 更快(更严格平衡)
适用场景 增删频繁(HashMap、TreeMap) 查找密集(数据库内存索引)

HashMap 为什么选红黑树? HashMap 中链表转树后,增删操作频繁(put/remove),红黑树增删性能更优。

五、Java 中的红黑树应用

5.1 TreeMap

java 复制代码
TreeMap<String, Integer> map = new TreeMap<>();
map.put("banana", 2);  // 红黑树插入,O(log n)
map.put("apple", 1);
map.put("cherry", 3);

// 利用有序性
map.firstKey();          // "apple"
map.lastKey();           // "cherry"
map.subMap("apple", "cherry");  // {"apple"=1, "banana"=2}

5.2 HashMap 的TreeNode

java 复制代码
// HashMap 中链表转红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;     // 链表 → 红黑树
static final int UNTREEIFY_THRESHOLD = 6;   // 红黑树 → 链表
static final int MIN_TREEIFY_CAPACITY = 64; // 数组最小长度(不够则扩容不树化)

5.3 TreeSet

java 复制代码
// TreeSet 底层就是 TreeMap
TreeSet<Integer> set = new TreeSet<>();
set.add(3); set.add(1); set.add(2);
set.first();  // 1
set.last();   // 3
set.subSet(1, 3);  // [1, 2]

六、面试高频题

Q1: 红黑树为什么比 AVL 树更适合 HashMap?

难度:⭐⭐⭐⭐

答案:HashMap 中增删频繁(put/remove 是常态),红黑树增删最多 2~3 次旋转,AVL 树可能需要 O(log n) 次旋转。虽然红黑树查找略慢,但 HashMap 场景下综合性能更优。

Q2: 红黑树插入新节点为什么是红色?

难度:⭐⭐⭐

答案:红色节点不影响黑高(性质 5),最多只违反性质 4(连续红节点),修复成本低。如果插入黑色节点,必然违反性质 5(路径黑高不等),修复更复杂。

Q3: HashMap 链表转红黑树的阈值为什么是 8?

难度:⭐⭐⭐⭐

答案:泊松分布计算,负载因子 0.75 时,链表长度达到 8 的概率仅为 0.00000006(亿分之六)。正常情况下不会触发树化,只有 hash 碰撞极端严重时才需要,是防御性设计。

Q4: 红黑树的查找、插入、删除时间复杂度?

难度:⭐⭐

答案:均为 O(log n)。红黑树保证最长路径不超过最短路径 2 倍,n 个节点的树高 h ≤ 2log₂(n+1),因此所有操作都是对数级别。

相关推荐
云烟成雨TD4 小时前
Spring AI Alibaba 1.x 系列【69】Token 用量统计
java·人工智能·spring
JAVA9654 小时前
JAVA面试-并发篇 03-使用synchronized doublecheck实现单例有什么坑
java·单例模式·面试
在繁华处4 小时前
Java从零到熟练(四):面向对象基础
java·开发语言
Unbelievabletobe4 小时前
解决了股票api接口盘后数据更新慢的问题
大数据·开发语言·python
不会C语言的男孩6 小时前
C++ Primer 第2章:变量和基本类型
开发语言·c++
小江的记录本6 小时前
【JVM虚拟机】堆内存分代模型:年轻代(Eden+Survivor)、老年代、元空间Metaspace(附《思维导图》+《面试高频考点清单》)
java·前端·jvm·后端·python·spring·面试
在繁华处6 小时前
Java从零到熟练(三):流程控制
java·开发语言·python
唐青枫6 小时前
Java Optional 实战指南:优雅处理空值与链式转换
java
一起学开源6 小时前
一文读懂 ReAct 范式:让 AI Agent 真正学会“思考+行动“
java·javascript·react.js·ecmascript·react·alibaba·智能体开发
云泽8087 小时前
C++ 可调用对象通关指南:深度解析 Lambda 表达式、function 包装器与 bind 绑定器
开发语言·c++·算法