手写红黑树全流程学习总结

在C++数据结构学习中,红黑树作为一种自平衡二叉搜索树,兼具二叉搜索树的高效查找特性与平衡树的稳定性能,是难度较高、应用广泛的经典数据结构。本次我从零开始,手写实现了红黑树的完整功能,包括节点结构定义、旋转操作、插入操作、合法性检查、工具函数(大小、高度、中序遍历、查找)及析构函数,全程经历了理论理解、代码编写、BUG排查、优化完善的全流程,既有知识的沉淀,也有实战的积累。本文将围绕手写红黑树的全流程,结合我实际编写的代码逻辑、遇到的问题及解决思路,从基础理论、代码实现细节、问题排查过程、学习经验总结四个维度展开,全面复盘学习过程,梳理知识点与易错点,为后续数据结构的深入学习奠定基础,全文约5000字,真实还原手写红黑树的学习轨迹与实战感悟。

一、红黑树基础理论铺垫

在动手编写红黑树代码前,必须先扎实掌握红黑树的核心定义、性质与设计思想,这是后续代码实现的前提。红黑树的本质是一种自平衡二叉搜索树,它通过在每个节点上增加一个"颜色"属性(红色或黑色),并遵循特定的规则,确保树的高度始终保持在O(logn)级别,从而避免二叉搜索树在极端情况下退化为链表,保证查找、插入、删除等操作的时间复杂度稳定在O(logn)。

1.1 红黑树的核心性质

红黑树的所有操作都围绕其5条核心性质展开,这也是我们编写代码、检查树合法性的根本依据,结合我手写的代码,具体性质如下(贴合代码中enum Color{RED, BLACK}的定义):

  1. 每个节点要么是红色,要么是黑色(代码中通过Color类型的_color成员变量实现,默认插入节点为红色,这是因为插入红色节点更易避免破坏红黑树性质,减少调整次数);
  2. 根节点必须是黑色(代码中insert函数里,当树为空时,将新节点作为根节点,并强制设置为黑色,即_root->_color = BLACK);
  3. 所有叶子节点(NIL节点,代码中未单独定义,以nullptr表示)都是黑色;
  4. 如果一个节点是红色,则它的两个子节点必须是黑色(即不允许出现连续的红色节点,这是代码中Check函数的核心检查点之一);
  5. 从任意一个节点到其所有叶子节点的路径上,黑色节点的数量都相等(称为"黑高"相等,代码中IsBanlance函数通过计算最左路径的黑节点数量作为基准,校验所有路径的黑高是否一致)。

这5条性质相互约束,确保了红黑树的平衡性。其中,性质4和性质5是最容易被破坏的,也是插入操作中需要重点调整的内容------插入新节点(默认红色)后,可能会出现"连续红色节点"的问题,此时需要通过"变色"或"旋转"操作,恢复红黑树的所有性质。

1.2 红黑树与二叉搜索树、AVL树的区别

在学习红黑树之前,我已掌握二叉搜索树(BST)和AVL树的实现,明确三者的区别,能更好地理解红黑树的设计优势:

  1. 与二叉搜索树相比:二叉搜索树在插入有序数据时,会退化为单链表,查找时间复杂度退化为O(n);而红黑树通过自平衡机制,始终保持树的高度为O(logn),确保所有操作的高效性。例如,当插入1、2、3、4、5这样的有序数据时,二叉搜索树会变成一条直线,而红黑树会通过旋转和变色,维持平衡结构。
  2. 与AVL树相比:AVL树是严格平衡树,要求左右子树的高度差不超过1,插入和删除操作中需要频繁旋转,调整成本较高;而红黑树是"近似平衡"树,不追求严格的高度差,只通过颜色约束维持平衡,旋转操作的频率更低,在插入、删除操作频繁的场景下,性能优于AVL树。红黑树的黑高平衡机制,既保证了树的高度不会过高,又降低了调整成本,是实际应用中(如STL中的map、set)的首选平衡树。

1.3 红黑树插入操作的核心思路

红黑树的插入操作是整个实现过程的核心,也是最复杂的部分,其流程分为两步:第一步,按照二叉搜索树的插入规则插入新节点,并将新节点颜色设为红色;第二步,检查插入后是否破坏红黑树性质,若破坏则通过"变色"或"旋转+变色"的方式恢复性质。

插入新节点后,破坏的性质主要是"连续红色节点"(性质4),此时需要根据"叔叔节点"(父节点的兄弟节点)的颜色,分为两种情况处理:

  1. 叔叔节点为红色:此时只需通过"变色"操作即可恢复平衡------将父节点、叔叔节点设为黑色,祖父节点设为红色,然后将当前节点指向祖父节点,继续向上检查,直到根节点(根节点最终强制设为黑色);
  2. 叔叔节点为黑色或不存在:此时需要通过"旋转"+"变色"操作恢复平衡,具体分为四种子情况(根据父节点在祖父节点的左右侧、当前节点在父节点的左右侧划分):
    (1)父节点在祖父节点左侧,当前节点在父节点左侧(LL型):对祖父节点进行右单旋,然后将父节点设为黑色,祖父节点设为红色;
    (2)父节点在祖父节点左侧,当前节点在父节点右侧(LR型):先对父节点进行左单旋,再对祖父节点进行右单旋,最后将当前节点设为黑色,祖父节点设为红色;
    (3)父节点在祖父节点右侧,当前节点在父节点右侧(RR型):对祖父节点进行左单旋,然后将父节点设为黑色,祖父节点设为红色;
    (4)父节点在祖父节点右侧,当前节点在父节点左侧(RL型):先对父节点进行右单旋,再对祖父节点进行左单旋,最后将当前节点设为黑色,祖父节点设为红色。

这一核心思路,直接决定了后续插入函数、旋转函数的代码实现逻辑,也是我在编写代码时重点攻克的难点。

二、红黑树代码实现全细节(贴合本人手写代码)

结合我手写的红黑树代码,从节点结构定义、核心操作(旋转、插入)、合法性检查、工具函数、析构函数五个方面,详细拆解代码实现细节,还原每一步的编写思路与注意事项,确保每一行代码都有对应的理论支撑,贴合我的学习轨迹。

2.1 节点结构定义

红黑树的节点需要包含左孩子、右孩子、父节点、键值对、颜色五个核心成员,结合模板编程(支持任意类型的键值对),我定义的节点结构如下,完全贴合手写代码:

cpp 复制代码
template<class K, class V>
struct RBTreeNode
{
    RBTreeNode(pair<K,V> kv)
        :_left(nullptr)
        , _right(nullptr)
        ,_parent(nullptr)
        ,_kv(kv)
        ,_color(RED)//默认给成红色,减少调整次数
    {
    }

    RBTreeNode<K, V>* _left;    // 左孩子
    RBTreeNode<K, V>* _right;   // 右孩子
    RBTreeNode<K, V>* _parent;  // 父节点(旋转、调整时必须用到)
    pair<K, V> _kv;             // 键值对(存储数据)
    Color _color;               // 节点颜色(RED/BLACK)
};

编写节点结构时,需要注意以下3个细节(结合我遇到的小问题):

  1. 父节点指针必须存在:红黑树的调整操作(变色、旋转)需要频繁访问父节点、祖父节点,若缺少父节点指针,无法实现后续的平衡调整,这是我最开始忽略的点,后续在编写旋转函数时才补充完善;
  2. 构造函数初始化:必须初始化所有成员变量,尤其是父节点、左右孩子设为nullptr,颜色默认设为红色------这是因为插入红色节点,只会破坏"连续红色节点"这一种性质,而插入黑色节点会破坏"黑高相等"的性质,调整成本更高;
  3. 模板参数的使用:采用模板<class K, class V>,支持任意类型的键值对(如int、double等),贴合STL中map的实现逻辑,后续测试时也能灵活传入不同类型的数据。

注意:最初我曾在节点结构中声明了析构函数~RBTreeNode(),但未实现,导致链接时出现隐患,后续在完善析构函数时,删除了该声明(无需单独实现节点析构,由红黑树类的析构函数统一销毁节点)。

2.2 红黑树类的整体框架

红黑树类采用模板编程,包含根节点指针、核心成员函数(旋转、插入、检查、工具函数)和私有辅助函数(递归实现大小、高度、中序遍历),整体框架贴合我的手写代码,如下所示:

cpp 复制代码
template<class K, class V>
class RBTree
{
    typedef RBTreeNode<K, V> Node;  // 类型重定义,简化代码
public:
    RBTree()
        :_root(nullptr)
    {
    }
    ~RBTree();  // 析构函数,后续完善

    // 核心操作:旋转、插入
    void RotateR(Node* cur);    // 右单旋
    void RotateL(Node* cur);    // 左单旋
    void RotateLR(Node* cur);   // 左右双旋(LR型)
    void RotateRL(Node* cur);   // 右左双旋(RL型)
    bool insert(pair<K, V> kv); // 插入操作

    // 合法性检查
    bool Check(Node* cur, int BlackNum, int BaseCount);
    bool IsBanlance();

    // 工具函数
    int size();         // 节点总数
    int height();       // 树的高度
    void inorder();     // 中序遍历(有序输出)
    Node* find(const K& key);   // 查找指定key的节点

private:
    // 私有辅助函数(递归实现)
    int _size(Node* cur);
    int _height(Node* cur);
    void _inorder(Node* cur);
    void destroy(Node* cur);    // 析构辅助函数,后序销毁节点

    Node* _root;        // 根节点指针
};

类的框架设计遵循"封装"原则,将核心操作(如旋转、插入)设为公有成员,供外部调用;将递归辅助函数设为私有成员,隐藏实现细节,避免外部误调用。同时,通过typedef简化节点类型的书写,提升代码可读性。

2.3 核心旋转操作实现(最基础、最关键)

旋转操作是红黑树实现平衡的核心手段,分为左单旋(RotateL)、右单旋(RotateR),以及基于两种单旋的双旋(RotateLR、RotateRL)。旋转的核心目的是调整节点的位置关系,不改变二叉搜索树的有序性,同时修复红黑树的性质。以下结合我的手写代码,详细拆解每一种旋转的实现细节。

2.3.1 右单旋(RotateR)

右单旋的适用场景:父节点在祖父节点左侧,当前节点在父节点左侧(LL型),需要通过右单旋调整平衡。旋转的核心是"以当前节点(祖父节点)的左孩子为旋转轴,将当前节点右旋,使其成为左孩子的右孩子,左孩子成为新的父节点"。

我的手写代码实现(带详细注释,贴合逻辑):

cpp 复制代码
// 右单旋:以 cur 为旋转轴进行右旋
void RotateR(Node* cur) 
{
    // 定义三个关键节点:cur的左孩子、左孩子的右孩子、cur的父节点
    Node* subL = cur->_left;
    Node* subLR = subL->_right;
    Node* pparent = cur->_parent;

    // 1. 调整cur与subLR的关系:将subLR作为cur的左孩子
    cur->_left = subLR;
    // 若subLR不为空,更新其父亲为cur
    if(subLR)
        subLR->_parent = cur;

    // 2. 调整subL与cur的关系:将cur作为subL的右孩子
    subL->_right = cur;
    cur->_parent = subL;

    // 3. 调整subL与pparent的关系:更新pparent的孩子指针
    subL->_parent = pparent;
    // 若cur是根节点,旋转后subL成为新根
    if (_root == cur)
    {
        _root = subL;
    }
    else
    {
        // 若cur是pparent的左孩子,将pparent的左孩子改为subL;否则改为右孩子
        if (pparent->_left == cur)
            pparent->_left = subL;
        else
            pparent->_right = subL;
    }
}

实现右单旋时,必须注意3个关键细节(我曾在这里出错导致旋转后节点断裂):

  1. 处理subLR节点:若subLR不为空,必须更新其_parent指针为cur,否则会导致subLR节点"失联",出现空指针崩溃;
  2. 更新根节点:若旋转的是根节点(cur == _root),旋转后subL成为新的根节点,必须更新_root指针,否则根节点依然指向cur,导致树结构错误;
  3. 关联父节点pparent:必须判断cur是pparent的左孩子还是右孩子,才能正确更新pparent的孩子指针,否则会导致pparent与subL的关联断裂,树结构混乱。

2.3.2 左单旋(RotateL)

左单旋的适用场景:父节点在祖父节点右侧,当前节点在父节点右侧(RR型),与右单旋对称,核心是"以当前节点(祖父节点)的右孩子为旋转轴,将当前节点左旋,使其成为右孩子的左孩子,右孩子成为新的父节点"。

我的手写代码实现(与右单旋对称,贴合逻辑):

cpp 复制代码
// 左单旋:以 cur 为旋转轴进行左旋(RR 型)
void RotateL(Node* cur) 
{
    // 定义三个关键节点:cur的右孩子、右孩子的左孩子、cur的父节点
    Node* subR = cur->_right;
    Node* subRL = subR->_left;
    Node* pparent = cur->_parent;
    
    // 1. 调整cur与subRL的关系:将subRL作为cur的右孩子
    cur->_right = subRL;
    // 若subRL不为空,更新其父亲为cur
    if (subRL)
        subRL->_parent = cur;

    // 2. 调整subR与cur的关系:将cur作为subR的左孩子
    subR->_left = cur;
    cur->_parent = subR;
    
    // 3. 调整subR与pparent的关系:更新pparent的孩子指针
    subR->_parent = pparent;
    // 若cur是根节点,旋转后subR成为新根
    if (_root == cur)
    {
        _root = subR;
    }
    else
    {
        if (pparent->_left == cur)
            pparent->_left = subR;
        else
            pparent->_right = subR;
    }
}

左单旋的实现细节与右单旋完全对称,核心是"保持节点的父子关系正确",避免出现节点断裂或指针异常。我在编写时,曾因遗漏subRL的_parent更新,导致后续查找节点时出现空指针崩溃,后续排查后才发现该问题。

2.3.3 双旋操作(RotateLR、RotateRL)

双旋是基于单旋的组合操作,适用于"父节点与当前节点不在同一侧"的情况(LR型和RL型),核心是"先将当前节点与父节点旋转为同一侧,再对祖父节点进行单旋"。

  1. 左右双旋(RotateLR):适用场景为父节点在祖父节点左侧,当前节点在父节点右侧(LR型),实现逻辑是"先对父节点进行左单旋,再对祖父节点进行右单旋"。

我的手写代码实现(修复了最初的递归死循环问题):

cpp 复制代码
// 左右双旋:先左旋,再右旋(LR 型)
void RotateLR(Node* cur)
{
    RotateL(cur->_left);  // 先对cur的左孩子(父节点)进行左单旋
    RotateR(cur);         // 再对cur(祖父节点)进行右单旋
}

注意:最初我编写时,误将第二行写成RotateLR(subLR),导致函数无限递归,出现栈溢出警告(C4717),后续排查后发现错误,改为正确的RotateR(cur),解决了递归死循环问题。

  1. 右左双旋(RotateRL):适用场景为父节点在祖父节点右侧,当前节点在父节点左侧(RL型),实现逻辑是"先对父节点进行右单旋,再对祖父节点进行左单旋",与左右双旋对称。

我的手写代码实现:

cpp 复制代码
// 右左双旋:先右旋,再左旋(RL 型)
void RotateRL(Node* cur) 
{
    RotateR(cur->_right);  // 先对cur的右孩子(父节点)进行右单旋
    RotateL(cur);          // 再对cur(祖父节点)进行左单旋
}

双旋的核心是"先调整子树,再调整整体",确保旋转后节点的有序性不变,同时修复红黑树的平衡性质。编写时,必须注意旋转的顺序,顺序错误会导致树结构混乱,无法实现平衡调整。

2.4 插入操作实现(核心难点)

插入操作是红黑树实现的核心难点,分为"二叉搜索树插入"和"红黑树性质修复"两个阶段,全程贴合红黑树的5条性质,结合旋转和变色操作,确保插入后树的平衡性。以下是我的手写代码实现,带详细注释,拆解每一步逻辑:

cpp 复制代码
bool insert(pair<K, V> kv)
{
    // 1. 创建新节点(默认红色)
    Node* newnode = new Node(kv);
    
    // 2. 空树处理:新节点作为根节点,设为黑色
    if (_root == nullptr)
    {
        _root = newnode;
        _root->_color = BLACK;
        return true;
    }

    // 3. 按照二叉搜索树规则,找到插入位置
    Node* cur = _root;
    Node* parent = nullptr;
    while (cur)
    {
        if (kv.first > cur->_kv.first)  // 插入值大于当前节点,走右子树
        {
            parent = cur;
            cur = cur->_right;
        }
        else if (kv.first < cur->_kv.first)  // 插入值小于当前节点,走左子树
        {
            parent = cur;
            cur = cur->_left;
        }
        else  // 键值已存在,不插入,释放新节点(避免内存泄漏)
        {
            delete newnode;
            return false;
        }
    }

    // 4. 将新节点链接到父节点
    cur = newnode;
    if (cur->_kv.first < parent->_kv.first)
        parent->_left = cur;
    else
        parent->_right = cur;
    cur->_parent = parent;  // 更新新节点的父节点

    // 5. 修复红黑树性质(解决连续红色节点问题)
    // 只要父节点是红色,就需要调整(根节点是黑色,父节点红色说明不是根)
    while (parent && parent->_color == RED)
    {
        Node* grandpa = parent->_parent;  // 祖父节点(必存在,因为父节点是红色,不是根)
        Node* uncle = nullptr;            // 叔叔节点(父节点的兄弟节点)

        // 情况1:父节点在祖父节点的左侧
        if (grandpa->_left == parent)	
        {
            uncle = grandpa->_right;  // 叔叔节点是祖父节点的右孩子

            // 子情况1.1:叔叔节点存在且为红色(只变色,不旋转)
            if (uncle && uncle->_color == RED)
            {
                grandpa->_color = RED;    // 祖父节点设为红色
                parent->_color = BLACK;   // 父节点设为黑色
                uncle->_color = BLACK;    // 叔叔节点设为黑色

                // 向上迭代,继续检查祖父节点的父节点
                cur = grandpa;
                parent = cur->_parent;
            }
            // 子情况1.2:叔叔节点不存在或为黑色(旋转+变色)
            else if (uncle == nullptr || uncle->_color == BLACK)
            {
                // 子子情况1.2.1:当前节点在父节点左侧(LL型),右单旋
                if (parent->_left == cur)
                {
                    RotateR(grandpa);
                }
                // 子子情况1.2.2:当前节点在父节点右侧(LR型),左右双旋
                else if (parent->_right == cur)
                {
                    RotateLR(grandpa);
                }
                // 旋转后变色:父节点设为黑色,祖父节点设为红色
                parent->_color = BLACK;
                grandpa->_color = RED;
                break;  // 旋转后当前子树已平衡,无需继续向上检查
            }
        }
        // 情况2:父节点在祖父节点的右侧(与情况1对称)
        else if (grandpa->_right == parent)
        {
            uncle = grandpa->_left;  // 叔叔节点是祖父节点的左孩子

            // 子情况2.1:叔叔节点存在且为红色(只变色)
            if (uncle && uncle->_color == RED)
            {
                grandpa->_color = RED;
                parent->_color = BLACK;
                uncle->_color = BLACK;

                cur = grandpa;
                parent = cur->_parent;
            }
            // 子情况2.2:叔叔节点不存在或为黑色(旋转+变色)
            else if (uncle == nullptr || uncle->_color == BLACK)
            {
                // 子子情况2.2.1:当前节点在父节点右侧(RR型),左单旋
                if (parent->_right == cur)
                {
                    RotateL(grandpa);
                }
                // 子子情况2.2.2:当前节点在父节点左侧(RL型),右左双旋
                else if (parent->_left == cur)
                {
                    RotateRL(grandpa);
                }
                // 旋转后变色
                parent->_color = BLACK;
                grandpa->_color = RED;
                break;
            }
        }	
    }
    // 强制根节点为黑色(防止调整过程中根节点变为红色)
    _root->_color = BLACK;
    return true;
}

插入操作的实现,我曾遇到多个错误,后续逐步排查修复,核心注意事项(贴合我的踩坑经历)如下:

  1. 空树处理:必须将根节点设为黑色,这是红黑树的核心性质,最初我曾遗漏这一步,导致根节点为红色,检查时报错;
  2. 重复键值处理:当插入的键值已存在时,必须释放新创建的节点(delete newnode),否则会导致内存泄漏,这是我最初忽略的点,后续排查内存问题时发现;
  3. 父节点指针更新:新节点插入后,必须设置cur->_parent = parent,否则后续调整时,无法访问父节点,出现空指针崩溃;
  4. 调整循环的终止条件:循环条件为"parent && parent->_color == RED",当父节点为黑色时,无需调整,直接退出循环;
  5. 旋转后的break:旋转+变色后,当前子树已恢复平衡,无需继续向上检查,必须加break,否则会导致无限循环,这是我最初的致命错误之一;
  6. 根节点强制变黑:无论调整过程中根节点是否变为红色,最终都要强制设置_root->_color = BLACK,确保符合红黑树性质。

2.5 合法性检查函数实现(验证红黑树正确性)

编写完插入和旋转操作后,需要实现合法性检查函数,验证红黑树是否满足5条核心性质,避免出现结构错误。我实现了两个函数:Check(递归检查每一条路径)和IsBanlance(入口函数,检查根节点、计算基准黑高),贴合我的手写代码。

2.5.1 Check函数(递归检查)

Check函数的核心功能:递归遍历整棵树,检查两条核心规则------① 无连续红色节点;② 所有路径的黑高相等。参数说明:cur(当前节点)、BlackNum(当前路径的黑节点数量)、BaseCount(基准黑高,由最左路径计算得出)。

cpp 复制代码
bool Check(Node* cur, int BlackNum, int BaseCount)
{
    // 走到空节点(叶子节点),检查当前路径黑高是否与基准一致
    if (cur == nullptr)
    {
        return BlackNum == BaseCount;
    }

    // 检查连续红色节点:当前节点为红色,且父节点也为红色(违规)
    if (cur->_color == RED) 
    {
        // 无需判断父节点是否为空:根节点为黑色,当前节点为红色,父节点必存在
        if (cur->_parent->_color == RED)
        { 
            cout << "出现连续红色节点,节点key值:" << cur->_kv.first << endl;
            return false;
        }
    }

    // 当前节点为黑色,黑节点数量+1
    if (cur->_color == BLACK)
        BlackNum++;

    // 递归检查左、右子树,必须都满足条件才返回true
    return Check(cur->_left, BlackNum, BaseCount)
        && Check(cur->_right, BlackNum, BaseCount);
}

编写Check函数时,我曾出现"_col与_color混用"的错误------最初误写为cur->_col == RED,而节点结构中成员变量是_color,导致编译报错,后续统一改为_color,解决了该问题。同时,无需判断cur->_parent是否为空,因为当前节点为红色时,根节点是黑色,所以当前节点不可能是根节点,父节点必存在。

2.5.2 IsBanlance函数(入口函数)

IsBanlance函数的核心功能:作为合法性检查的入口,检查根节点是否为黑色、计算基准黑高(最左路径的黑节点数量),并调用Check函数递归检查整棵树。

cpp 复制代码
bool IsBanlance()
{
    // 空树视为合法
    if (_root == nullptr)
        return true;

    // 检查根节点是否为黑色
    if (_root->_color == RED)
    {
        cout << "根为红" << endl;
        return false;
    }

    // 计算基准黑高:最左路径的黑节点数量
    Node* cur = _root;
    int BaseCount = 0;
    while (cur)
    {
        if (cur->_color == BLACK)
            BaseCount++;
        cur = cur->_left;  // 一直走左子树,获取最左路径的黑高
    }

    // 调用Check函数,从根节点开始检查,初始黑节点数量为0
    return Check(_root, 0, BaseCount);
}

注意:最初我编写while循环时,误将"cur = cur->_left"写在else分支中,导致循环死循环(当节点为红色时,cur不移动),后续修改为"无论节点颜色,都走左子树",解决了死循环问题。

2.6 工具函数实现(完善红黑树功能)

为了更全面地操作红黑树,我实现了4个工具函数:size(节点总数)、height(树的高度)、inorder(中序遍历)、find(查找节点),均采用递归或迭代实现,贴合我的手写代码,完善红黑树的功能。

2.6.1 size函数(节点总数)

功能:计算红黑树的节点总数,采用递归实现,核心逻辑是"当前节点数量 = 左子树节点数 + 右子树节点数 + 1(当前节点)"。

cpp 复制代码
// 公有接口,供外部调用
int size()
{
    return _size(_root);
}

// 私有辅助函数,递归实现
int _size(Node* cur)
{
    if (cur == nullptr)
        return 0;
    return _size(cur->_left) + _size(cur->_right) + 1;
}

2.6.2 height函数(树的高度)

功能:计算红黑树的高度,采用递归实现,核心逻辑是"树的高度 = 左子树高度与右子树高度的最大值 + 1(当前节点)"。

cpp 复制代码
// 公有接口
int height()
{
    return _height(_root);
}

// 私有辅助函数,递归实现
int _height(Node* cur)
{
    if (cur == nullptr)
        return 0;
    int leftH = _height(cur->_left);
    int rightH = _height(cur->_right);
    return leftH > rightH ? leftH + 1 : rightH + 1;
}

2.6.3 inorder函数(中序遍历)

功能:中序遍历红黑树,输出所有节点的键值对,验证红黑树的有序性(二叉搜索树的核心特性,中序遍历结果为升序)。

cpp 复制代码
// 公有接口,供外部调用
void inorder()
{
    _inorder(_root);
    cout << endl;
}

// 私有辅助函数,递归实现中序遍历
void _inorder(Node* cur)
{
    if (cur == nullptr)
        return;
    _inorder(cur->_left);  // 先遍历左子树
    cout << cur->_kv.first << ":" << cur->_kv.second << " ";  // 访问当前节点
    _inorder(cur->_right); // 再遍历右子树
}

2.6.4 find函数(查找节点)

功能:根据指定的key值,查找红黑树中的对应节点,找到返回节点指针,未找到返回nullptr,采用迭代实现(效率高于递归)。

cpp 复制代码
Node* find(const K& key)
{
    Node* cur = _root;
    while (cur)
    {
        if (key > cur->_kv.first)  // key大于当前节点,走右子树
        {
            cur = cur->_right;
        }
        else if (key < cur->_kv.first)  // key小于当前节点,走左子树
        {
            cur = cur->_left;
        }
        else  // 找到对应节点,返回指针
        {
            return cur;
        }
    }
    // 遍历结束未找到,返回nullptr
    return nullptr;
}

2.7 析构函数实现(避免内存泄漏)

红黑树的节点是通过new动态分配的,若不手动释放,会导致内存泄漏,因此需要实现析构函数,采用后序遍历的方式,从叶子节点到根节点,逐一销毁所有节点。

cpp 复制代码
// 析构函数,调用私有辅助函数destroy销毁节点
~RBTree()
{
    destroy(_root);
    _root = nullptr;  // 销毁后,根节点置空,避免野指针
}

// 私有辅助函数,后序遍历销毁节点
void destroy(Node* cur)
{
    if (cur == nullptr)
        return;
    // 后序遍历:先销毁左子树,再销毁右子树,最后销毁当前节点
    destroy(cur->_left);
    destroy(cur->_right);
    delete cur;  // 释放当前节点
}

实现析构函数时,核心注意事项:必须采用后序遍历,先销毁左右子树,再销毁当前节点,避免出现"先销毁当前节点,导致无法访问其左右子树"的问题,从而造成内存泄漏。最初我曾采用前序遍历,导致部分节点无法被销毁,后续修改为后序遍历,解决了内存泄漏问题。

相关推荐
名字不好奇2 小时前
大模型如何“理解“人类语言:从符号到语义的飞跃
算法
我命由我123452 小时前
前端开发概念 - 无障碍树
javascript·css·笔记·学习·html·html5·js
小雅痞2 小时前
[Java][Leetcode hard] 76. 最小覆盖子串
java·算法·leetcode
绿豆人2 小时前
Cache缓存项目学习4
windows·学习·缓存
小O的算法实验室2 小时前
2026年IEEE TBD,面向大规模优化的随机矩阵粒子群算法,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
哭泣方源炼蛊2 小时前
AtCoder Beginner Contest 456 E补题(分层图 + 有向环检测 )
c++·算法·深度优先·图论·拓扑学
平行侠2 小时前
022Miller-Rabin 概率素性检验 - 概率与数论的完美联姻
数据结构·算法
Bechamz2 小时前
大数据开发学习Day29
大数据·学习
IT大白鼠3 小时前
AIGC+教育:个性化学习、AI助教、内容生产,教育行业的变革路径
人工智能·学习·aigc