红黑树(Red-Black Tree)

一句话说明

红黑树的目标不是“绝对平衡”,而是“别退化得太难看”,用较少调整把二叉搜索树的高度稳定在 O(log n)。

先别急着背 5 条性质

理解红黑树,最重要的一句话是:

红黑树是一棵“近似平衡”的二叉搜索树。

它为什么不追求像 AVL 那样严格平衡?

  • 因为严格平衡意味着更新时要做更多旋转
  • 红黑树选择放松一点平衡要求
  • 换来插入、删除时更稳定的工程表现

所以工程里很多有序 map / set 更喜欢红黑树。

经典 5 条性质

  1. 每个节点非红即黑。
  2. 根节点是黑色。
  3. 所有空叶子(NIL)视为黑色。
  4. 红节点的孩子必须是黑色。
  5. 任一节点到其所有叶子的路径,黑节点数量相同。

真正要抓住的是后两条:

  • 不允许红红相连
  • 每条路径黑高一致

这两件事一起约束了树高不会太离谱。

你可以这样理解“为什么它平衡”

红节点可以看成“临时粘在黑节点上的附属节点”。
如果把红节点向上收缩,红黑树就能类比成一棵多路平衡树。

这就是为什么红黑树虽然不是严格平衡,但仍然能保证:

高度 = O(log n)

旋转本身并不难,难的是“何时旋”

红黑树操作真正难的地方,不是左旋右旋,而是:

  • 什么时候变色
  • 什么时候先旋再变色
  • 什么时候问题会向上冒

所以学习红黑树时,建议先把旋转当成基础工具,再用一个更顺手的版本理解插入。

用左倾红黑树(LLRB)来理解更直观

很多资料会直接上标准 CLRS 红黑树修复分类,细节非常多。
从做题和理解角度,更推荐先看左倾红黑树(Left-Leaning Red-Black Tree):

  • 红链接默认左倾
  • 插入后局部修正
  • 本质仍然是红黑树

这样代码会短很多,也更适合 Go 演示。

Go 代码:节点定义

const (
    red   = true
    black = false
)
 
type RBNode struct {
    key         int
    color       bool
    left, right *RBNode
}

Go 代码:基础辅助函数

func isRed(node *RBNode) bool {
    return node != nil && node.color == red
}
 
func rotateLeft(h *RBNode) *RBNode {
    x := h.right
    h.right = x.left
    x.left = h
    x.color = h.color
    h.color = red
    return x
}
 
func rotateRight(h *RBNode) *RBNode {
    x := h.left
    h.left = x.right
    x.right = h
    x.color = h.color
    h.color = red
    return x
}
 
func flipColors(h *RBNode) {
    h.color = !h.color
    if h.left != nil {
        h.left.color = !h.left.color
    }
    if h.right != nil {
        h.right.color = !h.right.color
    }
}

这三个动作分别在干什么

  • rotateLeft:把右侧红链接转成左侧红链接
  • rotateRight:处理连续左红链接
  • flipColors:把一个临时 4-节点向上拆分

也就是说,修复逻辑其实是在不断维持局部结构合法。

Go 代码:插入

type RedBlackTree struct {
    root *RBNode
}
 
func (t *RedBlackTree) Insert(key int) {
    t.root = insert(t.root, key)
    t.root.color = black
}
 
func insert(h *RBNode, key int) *RBNode {
    if h == nil {
        return &RBNode{
            key:   key,
            color: red,
        }
    }
 
    if key < h.key {
        h.left = insert(h.left, key)
    } else if key > h.key {
        h.right = insert(h.right, key)
    }
 
    if isRed(h.right) && !isRed(h.left) {
        h = rotateLeft(h)
    }
    if isRed(h.left) && isRed(h.left.left) {
        h = rotateRight(h)
    }
    if isRed(h.left) && isRed(h.right) {
        flipColors(h)
    }
 
    return h
}

这段插入逻辑可以直接背成 3 条规则

从下往上回溯时:

  1. 右红左黑,左旋
  2. 左红且左左红,右旋
  3. 左右都红,变色

这就是 LLRB 风格最值钱的地方:
把原本复杂的插入修复压缩成几个稳定局部规则。

Go 代码:查找

func (t *RedBlackTree) Search(key int) bool {
    cur := t.root
    for cur != nil {
        if key == cur.key {
            return true
        }
        if key < cur.key {
            cur = cur.left
        } else {
            cur = cur.right
        }
    }
    return false
}

为什么标准库爱用红黑树

因为它在工程上是很均衡的选择:

  • 查找足够快
  • 插入删除旋转次数通常较少
  • 最坏复杂度稳定
  • 不像 AVL 那样为了更矮的树付出更高更新代价

所以它特别适合“增删查都很多”的有序字典场景。

红黑树和 AVL 怎么选

红黑树更像

  • 更新友好
  • 近似平衡
  • 工程取舍更强

AVL 更像

  • 查找更极致
  • 平衡更严格
  • 更新时旋转更积极

这也是为什么 平衡二叉树 常被当作“理论上更规整”的代表,而红黑树更常落地到标准库实现。

复杂度

操作时间复杂度
查找O(log n)
插入O(log n)
删除O(log n)

易错点

学红黑树最容易卡住的地方

  • 不要只背性质,不理解它们为什么控制树高。
  • 左旋右旋本身不难,真正难的是修复触发条件。
  • 红黑树不是严格平衡树,它只是把最坏高度压在 O(log n)。
  • 如果你是第一次系统学习,建议先从 LLRB 版本建立直觉。

经典题目 / 思考题

  • 手写一棵支持插入和查找的红黑树
  • 证明红黑树高度是 O(log n)
  • 比较红黑树、AVL 树、跳表的更新代价
  • 理解 map/set 为什么更偏爱红黑树

相关主题


返回:数据结构 | 算法学习导航