一文讲通--从树到红黑树

1. 什么是树?

树是一种有上下层级的数据结构,就像电脑里的文件夹:

text 复制代码
资料
├── 工作
│   ├── 合同
│   └── 报表
└── 生活
    ├── 照片
    └── 账单

每一个位置叫作节点 ,节点之间的连接叫作边。

名称 含义 上图中的例子
根节点 最顶层的节点 资料
父节点 某个节点的直接上级 工作是合同的父节点
子节点 某个节点的直接下级 合同是工作的子节点
兄弟节点 拥有相同父节点的节点 合同和报表
叶子节点 没有子节点的节点 合同、照片
子树 某个节点及其下面的所有节点 工作、合同、报表组成一棵子树

普通的树,一个节点可以有很多个孩子。


2. 什么是二叉树?

每个节点最多有两个孩子的树,叫作二叉树。

两个位置分别叫左孩子、右孩子:

text 复制代码
        A
       / \
      B   C
     /     \
    D       E

"最多两个"意味着:

  • 可以没有孩子。
  • 可以只有左孩子或右孩子。
  • 可以同时有左右两个孩子。

二叉树只限制孩子的数量,没有规定数据的大小顺序。


3. 什么是二叉搜索树?

为了方便查找,可以给二叉树增加一个排列规则。

先假设数字不重复:

对于每个节点,左边整棵子树里的数字都比它小,右边整棵子树里的数字都比它大。

例如:

text 复制代码
          8
        /   \
       4     12
      / \   /  \
     2   6 10  14

它不仅要求 4 比 8 小,还要求 4 下面的 2、6 也都比 8 小。

这样的树叫作二叉搜索树。

找数字 10 时:

text 复制代码
10 比 8 大  → 往右走
10 比 12 小 → 往左走
找到 10

每次比较,都能排除另一边的整棵子树。

这就是它能加快查找的原因。


4. 数据怎么插进去?

插入时,同样遵守"左小右大"。

例如在刚才的树中插入 5:

text 复制代码
5 比 8 小 → 往左
5 比 4 大 → 往右
5 比 6 小 → 往左
这里是空位置 → 放进去

结果:

text 复制代码
          8
        /   \
       4     12
      / \   /  \
     2   6 10  14
        /
       5

普通二叉搜索树的插入,就是这样一路比较,找到空位置。

但这会带来一个问题:树的形状受插入顺序影响。


5. 二叉搜索树为什么会长歪?

从空树开始,依次插入:

text 复制代码
1、2、3、4、5

每个新数字都比之前的大,所以一直往右放:

text 复制代码
1
 \
  2
   \
    3
     \
      4
       \
        5

它仍然符合"左小右大",但已经很像链表了。

找 5,需要经过:

text 复制代码
1 → 2 → 3 → 4 → 5

如果有一百万个节点,最坏就可能逐个比较一百万次。

而同样的数据,可以组织成:

text 复制代码
        3
       / \
      2   4
     /     \
    1       5

这样查找走的路就短了。

所以,只保证大小顺序还不够,还需要控制树的高度。


6. 高度是什么?平衡又是什么?

高度可以理解为:从这个位置往下,最长能经过多少层节点。

这里约定:

text 复制代码
空树高度为 0
只有一个节点,高度为 1

例如:

text 复制代码
         8
        / \
       4   12
      /
     2

对于节点 8:

text 复制代码
左子树:4 → 2,高度为 2
右子树:12,高度为 1
整棵树高度为 3

平衡的目的,就是避免一侧长得特别深,让整棵树的高度保持较小。

平衡不等于左右完全对称,也不要求每一层都填满。


7. 什么是平衡二叉搜索树?

平衡二叉搜索树会在插入、删除之后,主动调整结构,控制高度。

不同的树,对"允许歪多少"有不同规定。

一种严格的标准是:

每个节点的左右子树,高度差都不能超过 1。

按这个标准维护的二叉搜索树,叫作 AVL 树。中文教材里常把它称为"平衡二叉树"。

例如:

text 复制代码
         8
        / \
       4   12
      /
     2

它符合 AVL 的要求。

但下面这棵就不符合:

text 复制代码
10
  \
   20
     \
      30

对于 10:

text 复制代码
左子树高度 = 0
右子树高度 = 2
高度差 = 2

注意,每个节点都要检查,不能只看根节点。


8. 失衡之后,怎么调整?

主要通过旋转。

先看最简单的左旋:

text 复制代码
调整前:             调整后:

10                      20
  \                    /  \
   20                 10   30
     \
      30

具体做法是:

text 复制代码
把右孩子 20 提上来;
让原来的 10 成为 20 的左孩子;
30 继续留在 20 的右边。

为什么这样调整是合法的?

因为大小关系始终是:

text 复制代码
10 < 20 < 30

调整后仍然符合"左小右大",但树变矮了。

旋转的作用,就是改变树的形状,同时保留二叉搜索树的大小顺序。

向左长歪时,可以反过来右旋:

text 复制代码
调整前:             调整后:

      30                20
     /                 /  \
    20                10   30
   /
  10

复杂一点时,被提上来的节点可能还带着其他孩子,这些孩子也需要安排到正确的位置。

例如左旋:

text 复制代码
调整前:             调整后:

    10                    20
      \                  /  \
       20               10   30
      /  \                \
     15   30               15

15 比 10 大、比 20 小,所以调整后放在 10 的右边。

有些形状需要组合两次旋转,但原则一样。


9. 什么叫"自动平衡"?

不是等树全部建完,才统一整理,而是在数据变化后执行:

text 复制代码
按大小关系插入或删除
          ↓
检查受影响的节点
          ↓
必要时旋转
          ↓
继续检查上层节点

这里有两个不同的角色:

内容 解决什么问题
平衡规则 判断什么时候需要调整
旋转操作 真正改变树的形状

AVL 用高度差判断是否需要调整。

红黑树则使用另一套判断规则。


10. 有了 AVL,为什么还需要红黑树?

因为维持平衡本身也有成本。

AVL 的要求比较严格:每个节点左右子树高度差都不能超过 1。

红黑树允许更宽松的形态:不必时刻保持那么整齐,只要保证树不会长得过高。

对比 AVL 树 红黑树
都是什么 自平衡二叉搜索树 自平衡二叉搜索树
主要判断依据 左右子树高度差 节点颜色及相关规则
平衡要求 比较严格 比较宽松
调整手段 旋转、更新高度等 旋转、变色

宽松一些,给维护操作留下了更多余地,同时仍然能够保证对数级别的树高。

具体使用哪个更快,还取决于读写比例、实现方式等,不能只凭树的名字判断。

红黑树也属于自平衡二叉搜索树,但不遵守 AVL 的"高度差最多为 1"规则。


11. 红黑树里的颜色是做什么的?

红黑树给每个节点增加了一个颜色属性:

text 复制代码
        10黑
       /    \
     5红    15红

颜色可以用一个布尔值记录。

它与数字大小、奇偶、业务优先级都没有关系,也不是固定按层交替。

数字大小决定节点放在哪里,颜色帮助程序检查和维护平衡。

先记住两个核心要求:

  • 红色节点不能直接连接红色孩子。
  • 从任意节点出发,到它下面各个空位置的路径,黑色节点数量必须相同。

接下来通过插入过程理解它们。


12. 第一个节点是什么颜色?新节点又是什么颜色?

从空树开始,插入 10:

text 复制代码
10黑

它是根节点,所以设为黑色。

再插入 5:

text 复制代码
    10黑
    /
   5红

通常,新插入的节点先设为红色;如果它是根节点,就设为黑色。

但这只是初始颜色,后续调整时可能改变。

为什么优先设为红色?需要结合"黑色节点数量相同"来理解。


13. 为什么新节点先设红色?

节点没有孩子的位置,在红黑树的规则中,会被看作黑色空节点,称为 NIL。

例如:

text 复制代码
      10黑
      /   \
    NIL黑 NIL黑

这些 NIL 用于统一描述规则,实际实现不一定需要为每个空位置创建对象。

插入一个红色的 5:

text 复制代码
        10黑
       /    \
      5红   NIL黑
     /   \
   NIL黑 NIL黑

从 10 出发:

text 复制代码
往左:10黑 → 5红 → NIL黑   共 2 个黑节点
往右:10黑 → NIL黑        共 2 个黑节点

各条路径的黑色节点数量依然相同。

如果把新加入的 5 直接设为黑色:

text 复制代码
往左:10黑 → 5黑 → NIL黑   共 3 个黑节点
往右:10黑 → NIL黑        共 2 个黑节点

就破坏了这个要求。

所以,新节点先设红,可以保持原有路径上的黑色节点数量,减少影响。

接下来只需要检查:它有没有和红色父节点直接相连。


14. 父节点是黑色时,可以直接保留红色

例如:

text 复制代码
    10黑
    /
   5红

父黑、子红,没有问题。

再插入 15:

text 复制代码
       10黑
      /    \
     5红   15红

15 的父节点也是黑色,同样不需要调整。

这里 5 和 15 可以同时为红色,因为它们是兄弟。

不允许的是直接相连的父子都为红色。


15. 父节点也是红色时,需要看"叔叔"

继续插入 1,先设为红色:

text 复制代码
       10黑
      /    \
     5红   15红
    /
   1红

5 和 1 连续为红,违反规则。

此时:

text 复制代码
新节点:1
父节点:5
爷爷节点:10
叔叔节点:15,也就是父节点的兄弟

如果父节点和叔叔都是红色,通常先通过变色处理:

text 复制代码
父节点 5:红 → 黑
叔叔 15:红 → 黑
爷爷 10:黑 → 红

为什么爷爷要变红?

因为父节点和叔叔都变黑了,两边的路径各多出一个黑节点;把爷爷从黑改成红,可以抵消这个增加,保持这片子树原有的黑色节点数量。

不过,这里的爷爷恰好是根节点。根必须是黑色,所以最终再将它设回黑色:

text 复制代码
       10黑
      /    \
     5黑   15黑
    /
   1红

这会让整棵树所有根到 NIL 的路径统一多一个黑节点,因此仍然符合要求。

如果爷爷上面还有节点,就可能出现新的红红冲突,需要继续向上检查。

颜色不是节点的永久身份,会随着维护过程改变。


16. 叔叔是黑色时,为什么会用到旋转?

换一个例子,依次插入:

text 复制代码
10、20、30

插入 10:

text 复制代码
10黑

插入 20,先设红色:

text 复制代码
10黑
   \
   20红

父节点是黑色,不需要处理。

插入 30,先设红色:

text 复制代码
10黑
   \
   20红
      \
      30红

20 和 30 连续为红。

这次,30 的叔叔是 10 的左孩子位置,也就是黑色 NIL。

对于这个向右排列的例子,可以通过左旋配合变色处理:

text 复制代码
       20黑
      /    \
    10红   30红

这就是前面已经讲过的旋转:

text 复制代码
把 20 提上来;
10 放左边;
30 放右边。

然后配合颜色调整,使规则重新成立。

如果父子节点拐了一个弯,例如:

text 复制代码
    10黑
       \
       30红
       /
      20红

通常需要先旋转一次,把这种"折线"变成前面的"直线",再进一步旋转、变色。

具体分支可以后面再学。现在要理解的是:

周围节点的颜色和位置,共同决定该变色、旋转一次,还是旋转两次。


17. 现在再看红黑树的完整规则

规则 含义
每个节点要么红,要么黑 节点具有颜色标记
根节点是黑色 调整结束时必须保证
所有 NIL 空节点是黑色 统一处理路径终点
红色节点的孩子必须是黑色 不允许连续两个红节点
从任意节点到下面各个 NIL 的路径,黑节点数相同 维护各条路径的黑色节点数量

这些规则描述的是维护完成后必须满足的状态。

而"新节点先设红、检查父节点、检查叔叔、变色、旋转",是恢复这些规则的操作过程。

不要把"规则"和"操作步骤"混成一件事。


18. 为什么这些规则能保证树不会长歪?

把黑色节点想成路径上的"检查站"。

两条核心要求意味着:

text 复制代码
各条路径上的黑色检查站数量相同;

两个黑色检查站之间,最多夹一个红色节点。

于是:

  • 不能在一条路径上多塞很多黑节点。
  • 也不能靠连续塞红节点,把一条路径无限拉长。

最短的路径可以主要由黑节点组成,最长的路径可以红黑交替,但长度不会无限分化。

这些约束能保证:按根到 NIL 的路径长度计算,最长路径不超过最短路径的两倍。

因此,红黑树允许一定程度的歪斜,但树高始终保持在 O(log n),查找、插入、删除也能保持 O(log n)。

它不是每次都把树整理得完全对称,而是始终把高度控制在可接受的范围内。


19. 删除节点时也要维护吗?

要,而且通常比插入更复杂。

插入时,我们优先放入红节点,尽量不改变黑色节点数量。

删除时,如果实际移除的是黑色节点,就可能让某些路径少一个黑节点:

text 复制代码
原来各条路径黑节点数相同
              ↓
某一条路径少了一个黑节点
              ↓
需要通过变色、旋转等操作修复

这里还会涉及兄弟节点及其孩子的颜色。

入门时先掌握插入即可。理解了"为什么要维持黑色节点数量",以后学习删除就能知道每一步在修复什么,而不是只能背步骤。

相关推荐
octopus_c1 小时前
C++ 入门
开发语言·c++·算法
爱吃香菜的初学者1 小时前
八.数据结构——哈希表(1)
数据结构·散列表
ZhengEnCi1 小时前
2026年10月30日AI早报:Gemini 4 Carbon登顶、DeepSeek融资1055亿、智元机器人上岗实测
人工智能·算法
强哥聊算力1 小时前
《算力江湖·Intel帝国往事》第三集:安迪·格鲁夫的铁血时代:Intel为什么能把对手逼到墙角?
运维·人工智能·科技·算法·ai·gpu算力
Shaoshing2 小时前
LeetCode100 11. 盛最多水的容器
算法·leetcode
骑着蜗牛撵大象3272 小时前
558 万美元训出 671B 模型:DeepSeek-V3 算法与系统的协同拆解
分布式·算法·moe·deepseek-v3·mla·fp8训练
Eyfcom2 小时前
〖阶段性回顾〗物流系统架构与算法的 Day 1-5 知识图谱与实战答疑
算法·系统架构·知识图谱·物流管理系统·快递管理系统
xunberg2 小时前
【Agent 生产化手记-02】Agent 怎么向人提问
人工智能·算法·架构
海绵宝宝转agent2 小时前
Leetcode二叉树算法个人理解
笔记·学习·算法·leetcode·深度优先