杨斌
发布于 2026-04-22 / 3 阅读
0
0

红黑树原理

image-kkQs.png

HashMap中出现哈希冲突的时候,链表长度>=8且数组长度>=64时,才会启用红黑树,而当红黑树节点减少到6个以下时,则会重新退化成链表。那哈希值相同时,红黑树的比较规则(源码实际逻辑)如下:

当两个 key 的 hashCode() 相等时(例如 "Aa""BB" 的哈希值可能相同),HashMap 会按以下顺序决定大小:

  1. 如果 key 实现了 Comparable 接口(比如 String、Integer 等):
    调用 key1.compareTo(key2)

    • 返回负数 → key1 放在左边

    • 返回正数 → key1 放在右边

    • 返回 0 → 说明两个 key 真正相等(equals 也应为 true),此时不会插入新节点,而是覆盖旧值。

  2. 如果 key 没有实现 Comparable(比如自定义的普通类):
    HashMap 会调用 System.identityHashCode(key) 获得一个基于对象内存地址的原始哈希值(不会重写,所以基本唯一)。
    然后用这个值来比较,保证总能分出左右。

实际源码中还有一个 tieBreakOrder 方法,当上述方法仍然无法区分时(极少情况),还会进一步用类名等做最终比较。

结论:哈希值相同并不会导致红黑树混乱,因为还有第二、第三层比较规则,直到能决定左右顺序为止。

分出了红黑树的左右之后,我们来思考下一组数据到底是如何存入到红黑树中的?以下数据类似于哈希值。

下面我把这组数据:

10, 20, 30, 15, 25, 5

一步一步插入红黑树,详细说明:

  • 插入位置怎么找

  • 节点颜色怎么染

  • 为什么变色

  • 为什么旋转

  • 每一步树长什么样


一、红黑树插入规则

插入时固定流程:

1. 先按二叉搜索树插入:小的放左边,大的放右边
2. 插入的新节点默认红色
3. 若破坏规则,再调整:叔叔红色则变色,叔叔为空则旋转

核心规则:

根节点必须黑
红节点不能连红
黑高度一致:每条路径上黑色节点的个数一致,红黑树中,黑色节点为真正骨架,红色节点为临时节点。

注意:红黑树规定所有null都看作黑色

二、插入 10


插入位置

树为空,所以:

10 成为根节点

新节点默认红色:

10(R)

但根必须黑:

所以改成:

10(B)

最终:

10(B)

三、插入 20


查找位置

比较:

20 > 10

放右边


新节点默认红:

10(B)
   \
   20(R)

检查:

父节点 10 是黑色:

合法,无需调整

四、插入 30


查找位置

比较:

30 > 10
30 > 20

放到20右边


插入后:

10(B)
   \
   20(R)
      \
      30(R)

问题:

20(R)
30(R)

连续红色违规


五、判断关系

当前:

30 = 当前节点
20 = 父节点
10 = 祖父节点
叔叔节点 = null(黑)

结构:

右 → 右

属于:

RR 型

六、RR型处理:左旋 + 重新染色


对 10 左旋:

旋转前:

10
  \
  20
    \
    30

旋转后:

   20
  /  \
10   30

重新染色:

20 -> 黑
10 -> 红
30 -> 红

结果:

    20(B)
   /    \
10(R)  30(R)

七、插入 15


查找位置

比较:

15 < 20
15 > 10

放10右边


插入后:

    20(B)
   /    \
10(R)  30(R)
   \
   15(R)

问题:

10 红
15 红

违规


八、找叔叔

当前:

15 = 当前节点
10 = 父节点
20 = 祖父节点
30 = 叔叔节点

叔叔:

30(R)

叔叔是红色


九、叔叔红 → 只变色

规则:

父变黑
叔变黑
祖父变红

变色:

20(R)
10(B)
30(B)

变成:

     20(R)
    /    \
10(B)   30(B)
   \
   15(R)

根必须黑:

20 -> 黑

最终:

     20(B)
    /    \
10(B)   30(B)
   \
   15(R)

十、插入 25


查找位置

比较:

25 > 20
25 < 30

放30左边


插入:

      20(B)
     /     \
 10(B)    30(B)
    \      /
   15(R) 25(R)

检查:

父节点:

30(B)

父是黑色:

无需调整

十一、插入 5


查找位置

比较:

5 < 20
5 < 10

放10左边


插入后:

        20(B)
       /     \
   10(B)     30(B)
   /   \      /
 5(R) 15(R) 25(R)

父节点:

10(B)

父黑:

合法

十二、最终红黑树

        20(B)
       /     \
   10(B)     30(B)
   /   \      /
 5(R) 15(R) 25(R)

十三、每一步颜色变化总结


插入10

10(R) → 10(B)

插入20

20 = 红

插入30

旋转前:

10(B)
  \
 20(R)
   \
   30(R)

旋转后:

20(B)
10(R)
30(R)

插入15

叔叔红:

10(R) → 黑
30(R) → 黑
20(B) → 红 → 再变黑

插入25

25 = 红

插入5

5 = 红

十四、为什么这样设计?

设计思想:


红色节点

表示:

临时扩展

黑色节点

表示:

稳定主干

所以树会自动形成:

黑色骨架
+ 红色叶子

像这样:

主结构稳定
局部灵活

十五、查找15的路径

查找:

15

路径:

20 → 10 → 15

比较:

15 < 20
15 > 10
15 = 15

找到。


十六、时间复杂度

树高度保持:

[
O(\log n)
]

所以查找:

[
O(\log n)
]


十七、一句话总结

这组数据插入过程核心是:

新节点先红
红红冲突:
    叔叔红 → 变色
    叔叔黑 → 旋转
最后根变黑


评论