【C++进阶】红黑树的实现

目录

本节学习目标

[1 红黑树概念](#1 红黑树概念)

[1.1 红黑树四条硬性规则](#1.1 红黑树四条硬性规则)

[1.2 为什么最长路径 ≤ 2 × 最短路径](#1.2 为什么最长路径 ≤ 2 × 最短路径)

[1.3 AVL 树 vs 红黑树](#1.3 AVL 树 vs 红黑树)

[2 红黑树结点定义](#2 红黑树结点定义)

[2.1 旋转函数说明](#2.1 旋转函数说明)

[3 红黑树插入完整逻辑](#3 红黑树插入完整逻辑)

插入步骤总览

[场景 1:叔叔 u 存在,并且 u 是红色(只变色,不旋转)](#场景 1:叔叔 u 存在,并且 u 是红色(只变色,不旋转))

[场景 2:叔叔 u 不存在 /u 存在但是 u 是黑色,单旋 + 变色](#场景 2:叔叔 u 不存在 /u 存在但是 u 是黑色,单旋 + 变色)

[场景 3:叔叔 u 不存在 /u 存在但是 u 是黑色,双旋 + 变色](#场景 3:叔叔 u 不存在 /u 存在但是 u 是黑色,双旋 + 变色)

[完整 Insert 插入源码](#完整 Insert 插入源码)

[4 红黑树查找](#4 红黑树查找)

[5 红黑树校验函数(调试必写)](#5 红黑树校验函数(调试必写))

[6 关于红黑树删除](#6 关于红黑树删除)

📔本篇核心考点总结

本节学习目标

  1. 掌握红黑树 4 条核心规则,理解 NIL 外部叶子结点概念
  2. 理解红黑树 "最长路径不超过最短路径 2 倍" 的原理,对比 AVL 树
  3. 掌握红黑树结点结构:_kv键值对、左右孩子、_parent父指针、颜色枚举
  4. 掌握红黑树插入完整流程:BST 插入、新增结点默认红色;分三大 case 处理:叔叔红 (仅变色)、叔叔黑 / 不存在单旋变色、叔叔黑 / 不存在双旋变色
  5. 看懂红黑树的左单旋、右单旋(和 AVL 旋转逻辑一致,没有平衡因子)
  6. 手写红黑树校验函数:校验根颜色、禁止连续红结点、所有路径黑色结点数目相等
  7. 对比 AVL 树,理解红黑树优势:旋转次数更少,STLmap/set底层就是红黑树;了解删除复杂,课件不实现删除

1 红黑树概念

红黑树是一棵二叉搜索树,每个结点多一个颜色标记,红色或者黑色。通过颜色约束间接实现近似平衡。

性质保证:任意一条从根到 NIL 空叶子结点的路径,最长路径长度不会超过最短路径的 2 倍。

1.1 红黑树四条硬性规则

  1. 每个结点只能是红色 或者 黑色。
  2. 根结点必须是黑色。
  3. 不能出现连续的红色结点:如果一个结点是红色,那么它的两个直接孩子必须是黑色。(父红则子不能红)
  4. 对于任意结点,该结点到达所有 NIL(空外部叶子结点)的全部路径上,黑色结点的数量完全相等。

补充 NIL 外部结点说明: 《算法导论》中提到所有 NIL 叶子(外部空结点)是黑色。课件实现中代码不专门创建 NIL 结点,把nullptr等价看作黑色 NIL 结点。NIL 只是逻辑概念,代码不实例化对象。

1.2 为什么最长路径 ≤ 2 × 最短路径

设黑色高度 bh:从某结点向下走到 NIL,路径上面黑色结点数量。

  • 最短路径:全部都是黑色结点,路径长度 = bh。
  • 根据规则 3,不能连续红色结点;极端最长路径:红黑交替,一红一黑 ,路径长度 = 2*bh。

所以:bh ≤ h ≤ 2*bh。 结点数量 N,时间复杂度:增删查 \(O(logN)\),最坏 2logN。

1.3 AVL 树 vs 红黑树

表格

AVL 树 红黑树
平衡约束 严格平衡,左右子树高度差绝对值≤1 颜色约束近似平衡,最长路径≤2 倍最短路径
平衡控制 平衡因子 bf,记录高度差 结点红、黑颜色标记
旋转次数 插入删除触发旋转频繁 旋转少,大部分情况只做变色
查找性能 理论略快 略低,同一复杂度级别 \(O(logN)\)
工程使用 极少手写,标准库不使用 STL map/set底层实现,工业界主流

AVL 追求严格高度平衡;红黑树牺牲一点点查找性能,换取更少旋转,综合工程表现更好。

2 红黑树结点定义

cpp 复制代码
//结点颜色枚举
enum Colour
{
    RED,
    BLACK
};

template<class K, class V>
struct RBTreeNode
{
    pair<K,V> _kv;
    RBTreeNode<K,V>* _left;
    RBTreeNode<K,V>* _right;
    RBTreeNode<K,V>* _parent; //父指针,向上回溯处理颜色
    Colour _col;

    RBTreeNode(const pair<K,V>& kv)
        :_kv(kv)
        ,_left(nullptr)
        ,_right(nullptr)
        ,_parent(nullptr)
        ,_col(RED) //新结点默认红色!
    {}
};

template<class K,class V>
class RBTree
{
    typedef RBTreeNode<K,V> Node;
private:
    Node* _root = nullptr;
public:
    //接口省略
};

✨重点:新插入结点默认设置为 RED 红色。 如果新增结点设置黑色,直接破坏规则 4(每条路径黑色数量相等),修复代价巨大;新增红色,只会可能违反规则 3(连续红)。

2.1 旋转函数说明

红黑树的左单旋RotateL、右单旋RotateR和 AVL 树旋转代码完全一样!红黑树没有平衡因子,旋转之后不用维护 bf,只需要修改颜色。 旋转核心要点:

  1. 修改父子孩子指针
  2. 更新所有结点的_parent父指针
  3. 如果旋转结点是根,更新_root。
cpp 复制代码
//右单旋
void RotateR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;

    parent->_left = subLR;
    if(subLR != nullptr)
        subLR->_parent = parent;

    Node* ppNode = parent->_parent;
    subL->_right = parent;
    parent->_parent = subL;

    if(ppNode == nullptr)
    {
        _root = subL;
        subL->_parent = nullptr;
    }
    else
    {
        if(parent == ppNode->_left)
            ppNode->_left = subL;
        else
            ppNode->_right = subL;
        subL->_parent = ppNode;
    }
}

//左单旋
void RotateL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;

    parent->_right = subRL;
    if(subRL != nullptr)
        subRL->_parent = parent;

    Node* ppNode = parent->_parent;
    subR->_left = parent;
    parent->_parent = subR;

    if(ppNode == nullptr)
    {
        _root = subR;
        subR->_parent = nullptr;
    }
    else
    {
        if(parent == ppNode->_left)
            ppNode->_left = subR;
        else
            ppNode->_right = subR;
        subR->_parent = ppNode;
    }
}

3 红黑树插入完整逻辑

插入步骤总览

  1. 按照普通 BST 二叉搜索树规则查找位置插入新结点;key 重复返回 false 插入失败。
  2. 空树:直接 new 结点,根强制设置为 BLACK 黑色,结束。
  3. 非空树:新结点cur默认红色。
  4. 如果父结点parent是黑色:没有违反任何红黑规则,直接插入结束。
  5. 如果父结点parent是红色:违反规则 3(连续红色)。 此时祖父grandfather(g)一定是黑色。接下来看叔叔结点 uncle (u),分三大场景处理。

符号约定:

  • cur(c):当前结点(新增结点,或者向上迭代上来的结点)
  • parent(p):cur 的父亲
  • grandfather(g):cur 的祖父,p 的父亲,p 为红,g 必然黑色
  • uncle(u):p 的兄弟,cur 的叔叔结点

场景 1:叔叔 u 存在,并且 u 是红色(只变色,不旋转)

条件:p红,g黑,u红 处理逻辑:

  1. parent (p) 变黑;uncle (u) 变黑;
  2. grandfather (g) 变红;
  3. 把g赋值给cur,继续向上循环迭代处理;
  4. 循环结束之后,最后强制把根结点置为黑色。

原理:p、u 变黑,两条子路径黑色计数 + 1;g 变红抵消增加的黑色计数,子树黑色高度不变。但是 g 变红,g 和 g 的父结点有可能形成连续红色,必须继续向上处理。

该场景:不管 p 是 g 的左还是右,cur 是 p 左还是右,逻辑完全一样,只变色,不旋转。

场景 2:叔叔 u 不存在 /u 存在但是 u 是黑色,单旋 + 变色

两种子情况:

  1. LL:p 是 g 左孩子,cur 是 p 左孩子 → 右单旋 RotateR (g)
    • 旋转完成,p 变黑;g 变红;处理完毕,break 不再向上循环。
  2. RR:p 是 g 右孩子,cur 是 p 右孩子 → 左单旋 RotateL (g)
    • 旋转完成,p 变黑;g 变红;处理完毕,break。

旋转之后子树黑色高度不变,上层不会出现连续红色,不需要继续向上。

场景 3:叔叔 u 不存在 /u 存在但是 u 是黑色,双旋 + 变色

  1. LR:p 是 g 左孩子,cur 是 p 右孩子:
    • 先对 p 做左单旋RotateL(p);再对 g 做右单旋RotateR(g);
    • cur 变黑;g 变红;break 结束。
  2. RL:p 是 g 右孩子,cur 是 p 左孩子:
    • 先对 p 做右单旋RotateR(p);再对 g 做左单旋RotateL(g);
    • cur 变黑;g 变红;break 结束。

双旋完成后,cur 成为这棵局部子树根,黑色;g 变红,黑色高度不变,停止向上迭代。

完整 Insert 插入源码

cpp 复制代码
bool Insert(const pair<K, V>& kv)
{
    if (_root == nullptr)
    {
        _root = new Node(kv);
        _root->_col = BLACK; //根必须黑色
        return true;
    }

    //BST查找插入位置
    Node* parent = nullptr;
    Node* cur = _root;
    while(cur != nullptr)
    {
        if(cur->_kv.first < kv.first)
        {
            parent = cur;
            cur = cur->_right;
        }
        else if(cur->_kv.first > kv.first)
        {
            parent = cur;
            cur = cur->_left;
        }
        else
        {
            //key重复
            return false;
        }
    }
    //创建新结点,默认RED红色
    cur = new Node(kv);
    cur->_col = RED;
    if(parent->_kv.first < kv.first)
    {
        parent->_right = cur;
    }
    else
    {
        parent->_left = cur;
    }
    cur->_parent = parent;

    //向上处理颜色冲突:父结点为红色才进入循环
    while(parent != nullptr && parent->_col == RED)
    {
        Node* grandfather = parent->_parent;
        //父是红,祖父一定是黑
        if(parent == grandfather->_left)
        {
            //父亲是祖父的左孩子,叔叔是祖父的右孩子
            Node* uncle = grandfather->_right;
            if(uncle != nullptr && uncle->_col == RED)
            {
                //【场景1:叔叔红色,只变色】
                parent->_col = BLACK;
                uncle->_col = BLACK;
                grandfather->_col = RED;

                //g作为新cur继续向上
                cur = grandfather;
                parent = cur->_parent;
            }
            else
            {
                //【场景2/3:叔叔为空或者黑色,旋转+变色】
                if(cur == parent->_left)
                {
                    //LL 右单旋
                    RotateR(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                }
                else
                {
                    //LR 双旋:先左后右
                    RotateL(parent);
                    RotateR(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                break; //旋转处理完毕,退出循环
            }
        }
        else
        {
            //父亲是祖父的右孩子,叔叔是祖父的左孩子
            Node* uncle = grandfather->_left;
            if(uncle != nullptr && uncle->_col == RED)
            {
                //场景1:叔叔红色,变色
                parent->_col = BLACK;
                uncle->_col = BLACK;
                grandfather->_col = RED;

                cur = grandfather;
                parent = cur->_parent;
            }
            else
            {
                //叔叔空/黑色,旋转变色
                if(cur == parent->_right)
                {
                    //RR左单旋
                    RotateL(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                }
                else
                {
                    //RL双旋,先右后左
                    RotateR(parent);
                    RotateL(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                break;
            }
        }
    }
    //循环结束,强制根结点黑色
    _root->_col = BLACK;
    return true;
}

4 红黑树查找

复用二叉搜索树查找逻辑,时间复杂度 \(O(logN)\)

cpp 复制代码
Node* Find(const K& key)
{
    Node* cur = _root;
    while(cur != nullptr)
    {
        if(cur->_kv.first < key)
        {
            cur = cur->_right;
        }
        else if(cur->_kv.first > key)
        {
            cur = cur->_left;
        }
        else
        {
            return cur;
        }
    }
    return nullptr;
}

5 红黑树校验函数(调试必写)

❗错误思路:只判断最长路径 ≤2 倍最短路径,不能作为红黑树校验标准。满足路径长度条件,结点颜色依然可以违反 4 条规则,后续插入会直接出错。 必须逐条校验四条规则:

  1. 根结点必须黑色。
  2. 不能存在连续红色结点(当前结点红色,父结点不能红色)。
  3. 每一条从根到 nullptr (NIL) 路径,黑色结点数目完全相等。

递归校验代码:

cpp 复制代码
//内部递归:blackNum 当前路径累计黑色数量;refNum 参考路径黑色计数
bool _Check(Node* root, int blackNum, const int refNum)
{
    if(root == nullptr)
    {
        //走到NIL空结点,一条路径结束
        if(blackNum != refNum)
        {
            cout << "路径黑色结点数目不一致!" << endl;
            return false;
        }
        return true;
    }

    //规则:不能连续红色,当前结点红,父亲不能红
    if(root->_col == RED && root->_parent->_col == RED)
    {
        cout << "key:" << root->_kv.first << " 存在连续红色结点!" << endl;
        return false;
    }

    //黑色结点计数+1
    if(root->_col == BLACK)
    {
        blackNum++;
    }

    //递归校验左右子树
    return _Check(root->_left, blackNum, refNum)
        && _Check(root->_right, blackNum, refNum);
}

//对外接口
bool IsBalance()
{
    if(_root == nullptr)
        return true;

    //规则:根结点必须黑色
    if(_root->_col == RED)
    {
        cout << "根结点是红色,违反红黑树规则" << endl;
        return false;
    }

    //求参考黑色计数:一直向左走到NIL,统计黑色结点数量作为基准refNum
    int refNum = 0;
    Node* cur = _root;
    while(cur != nullptr)
    {
        if(cur->_col == BLACK)
            refNum++;
        cur = cur->_left;
    }

    return _Check(_root, 0, refNum);
}

6 关于红黑树删除

本章节不实现删除操作 。删除逻辑复杂,分为大量 case,需要处理结点删除后的颜色修复;工程参考《算法导论》、《STL 源码剖析》阅读源码。笔试面试重点考察红黑树规则、插入三种 case、AVL 对比,删除一般不手写。

📔本篇核心考点总结

  1. 红黑树 4 条核心规则 ①结点红或黑;②根必须黑色;③不能连续红色结点;④任意结点到全部 NIL 空叶子路径黑色结点数量相等。
  2. 最长路径不超过最短路径 2 倍;时间复杂度\(O(logN)\);对比 AVL,红黑树旋转更少,STL map/set 底层采用红黑树。
  3. 新增结点默认红色,新增黑色极易破坏黑色计数规则。
  4. 插入冲突处理三大 case:
    • case1:叔叔 uncle 为红色:仅变色,向上迭代,不旋转;
    • case2:叔叔为空 / 黑色,LL/RR:单旋 + 变色,结束;
    • case3:叔叔为空 / 黑色,LR/RL:双旋 + 变色,结束;
    • 循环结束强制根结点置为黑色。
  5. 红黑树旋转和 AVL 旋转代码一样,没有平衡因子,旋转后只修改结点颜色。
  6. 校验函数:①根黑色;②禁止连续红结点;③全部路径黑色结点数目相等;不能只靠路径长度倍数校验。

面试简答

Q:红黑树和 AVL 树区别?

A:AVL 依靠平衡因子严格高度平衡;红黑树依靠颜色做近似平衡;AVL 查找略快,插入删除旋转多;红黑树旋转次数少,综合性能更好,STL map set 底层红黑树。

Q:红黑树新结点为什么默认是红色?

A:新增黑色直接破坏规则 4,所有路径黑色计数被打乱,修复成本高;新增红色只会可能触发连续红色,修复逻辑简单。

Q:红黑树插入叔叔结点红色的时候做什么?

A:父亲、叔叔变黑,祖父变红,把祖父当做 cur 继续向上处理,不需要旋转。

相关推荐
纪念 2291 小时前
C++算法(一)
开发语言·c++·算法
程序猿乐锅1 小时前
【黑马点评 | 第九篇】秒杀优化-异步实现
java·spring boot·redis·后端·spring·中间件·mybatis
All for pursuit.1 小时前
【二叉树-10】114.二叉树展开为链表
数据结构·c++·算法·leetcode·链表
多弗朗皮卡丘1 小时前
C++string类
c++
FYKJ_20103 小时前
springboot助农产品销售商城05829-计算机课程设计、毕业设计
java·spring boot·python·mysql·spark·django
Yunovian3 小时前
AI时代,针对模型与应用,浅谈一下各编程语言
开发语言·c++·人工智能·python·ai·rust·ai编程
周杰伦fans4 小时前
构建真正智能的CAD AI绘图代理
开发语言·人工智能·c#
user_admin_god5 小时前
第 10 篇:拼上下文与生成——预算、边界与防幻觉
java·人工智能·spring boot·语言模型
用户3126874877205 小时前
GC 算法与垃圾收集器演进:从 Serial 到 ZGC
java