在C++数据结构学习中,红黑树作为一种自平衡二叉搜索树,兼具二叉搜索树的高效查找特性与平衡树的稳定性能,是难度较高、应用广泛的经典数据结构。本次我从零开始,手写实现了红黑树的完整功能,包括节点结构定义、旋转操作、插入操作、合法性检查、工具函数(大小、高度、中序遍历、查找)及析构函数,全程经历了理论理解、代码编写、BUG排查、优化完善的全流程,既有知识的沉淀,也有实战的积累。本文将围绕手写红黑树的全流程,结合我实际编写的代码逻辑、遇到的问题及解决思路,从基础理论、代码实现细节、问题排查过程、学习经验总结四个维度展开,全面复盘学习过程,梳理知识点与易错点,为后续数据结构的深入学习奠定基础,全文约5000字,真实还原手写红黑树的学习轨迹与实战感悟。
一、红黑树基础理论铺垫
在动手编写红黑树代码前,必须先扎实掌握红黑树的核心定义、性质与设计思想,这是后续代码实现的前提。红黑树的本质是一种自平衡二叉搜索树,它通过在每个节点上增加一个"颜色"属性(红色或黑色),并遵循特定的规则,确保树的高度始终保持在O(logn)级别,从而避免二叉搜索树在极端情况下退化为链表,保证查找、插入、删除等操作的时间复杂度稳定在O(logn)。
1.1 红黑树的核心性质
红黑树的所有操作都围绕其5条核心性质展开,这也是我们编写代码、检查树合法性的根本依据,结合我手写的代码,具体性质如下(贴合代码中enum Color{RED, BLACK}的定义):
- 每个节点要么是红色,要么是黑色(代码中通过Color类型的_color成员变量实现,默认插入节点为红色,这是因为插入红色节点更易避免破坏红黑树性质,减少调整次数);
- 根节点必须是黑色(代码中insert函数里,当树为空时,将新节点作为根节点,并强制设置为黑色,即_root->_color = BLACK);
- 所有叶子节点(NIL节点,代码中未单独定义,以nullptr表示)都是黑色;
- 如果一个节点是红色,则它的两个子节点必须是黑色(即不允许出现连续的红色节点,这是代码中Check函数的核心检查点之一);
- 从任意一个节点到其所有叶子节点的路径上,黑色节点的数量都相等(称为"黑高"相等,代码中IsBanlance函数通过计算最左路径的黑节点数量作为基准,校验所有路径的黑高是否一致)。
这5条性质相互约束,确保了红黑树的平衡性。其中,性质4和性质5是最容易被破坏的,也是插入操作中需要重点调整的内容------插入新节点(默认红色)后,可能会出现"连续红色节点"的问题,此时需要通过"变色"或"旋转"操作,恢复红黑树的所有性质。
1.2 红黑树与二叉搜索树、AVL树的区别
在学习红黑树之前,我已掌握二叉搜索树(BST)和AVL树的实现,明确三者的区别,能更好地理解红黑树的设计优势:
- 与二叉搜索树相比:二叉搜索树在插入有序数据时,会退化为单链表,查找时间复杂度退化为O(n);而红黑树通过自平衡机制,始终保持树的高度为O(logn),确保所有操作的高效性。例如,当插入1、2、3、4、5这样的有序数据时,二叉搜索树会变成一条直线,而红黑树会通过旋转和变色,维持平衡结构。
- 与AVL树相比:AVL树是严格平衡树,要求左右子树的高度差不超过1,插入和删除操作中需要频繁旋转,调整成本较高;而红黑树是"近似平衡"树,不追求严格的高度差,只通过颜色约束维持平衡,旋转操作的频率更低,在插入、删除操作频繁的场景下,性能优于AVL树。红黑树的黑高平衡机制,既保证了树的高度不会过高,又降低了调整成本,是实际应用中(如STL中的map、set)的首选平衡树。
1.3 红黑树插入操作的核心思路
红黑树的插入操作是整个实现过程的核心,也是最复杂的部分,其流程分为两步:第一步,按照二叉搜索树的插入规则插入新节点,并将新节点颜色设为红色;第二步,检查插入后是否破坏红黑树性质,若破坏则通过"变色"或"旋转+变色"的方式恢复性质。
插入新节点后,破坏的性质主要是"连续红色节点"(性质4),此时需要根据"叔叔节点"(父节点的兄弟节点)的颜色,分为两种情况处理:
- 叔叔节点为红色:此时只需通过"变色"操作即可恢复平衡------将父节点、叔叔节点设为黑色,祖父节点设为红色,然后将当前节点指向祖父节点,继续向上检查,直到根节点(根节点最终强制设为黑色);
- 叔叔节点为黑色或不存在:此时需要通过"旋转"+"变色"操作恢复平衡,具体分为四种子情况(根据父节点在祖父节点的左右侧、当前节点在父节点的左右侧划分):
(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个细节(结合我遇到的小问题):
- 父节点指针必须存在:红黑树的调整操作(变色、旋转)需要频繁访问父节点、祖父节点,若缺少父节点指针,无法实现后续的平衡调整,这是我最开始忽略的点,后续在编写旋转函数时才补充完善;
- 构造函数初始化:必须初始化所有成员变量,尤其是父节点、左右孩子设为nullptr,颜色默认设为红色------这是因为插入红色节点,只会破坏"连续红色节点"这一种性质,而插入黑色节点会破坏"黑高相等"的性质,调整成本更高;
- 模板参数的使用:采用模板<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个关键细节(我曾在这里出错导致旋转后节点断裂):
- 处理subLR节点:若subLR不为空,必须更新其_parent指针为cur,否则会导致subLR节点"失联",出现空指针崩溃;
- 更新根节点:若旋转的是根节点(cur == _root),旋转后subL成为新的根节点,必须更新_root指针,否则根节点依然指向cur,导致树结构错误;
- 关联父节点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型),核心是"先将当前节点与父节点旋转为同一侧,再对祖父节点进行单旋"。
- 左右双旋(RotateLR):适用场景为父节点在祖父节点左侧,当前节点在父节点右侧(LR型),实现逻辑是"先对父节点进行左单旋,再对祖父节点进行右单旋"。
我的手写代码实现(修复了最初的递归死循环问题):
cpp
// 左右双旋:先左旋,再右旋(LR 型)
void RotateLR(Node* cur)
{
RotateL(cur->_left); // 先对cur的左孩子(父节点)进行左单旋
RotateR(cur); // 再对cur(祖父节点)进行右单旋
}
注意:最初我编写时,误将第二行写成RotateLR(subLR),导致函数无限递归,出现栈溢出警告(C4717),后续排查后发现错误,改为正确的RotateR(cur),解决了递归死循环问题。
- 右左双旋(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;
}
插入操作的实现,我曾遇到多个错误,后续逐步排查修复,核心注意事项(贴合我的踩坑经历)如下:
- 空树处理:必须将根节点设为黑色,这是红黑树的核心性质,最初我曾遗漏这一步,导致根节点为红色,检查时报错;
- 重复键值处理:当插入的键值已存在时,必须释放新创建的节点(delete newnode),否则会导致内存泄漏,这是我最初忽略的点,后续排查内存问题时发现;
- 父节点指针更新:新节点插入后,必须设置cur->_parent = parent,否则后续调整时,无法访问父节点,出现空指针崩溃;
- 调整循环的终止条件:循环条件为"parent && parent->_color == RED",当父节点为黑色时,无需调整,直接退出循环;
- 旋转后的break:旋转+变色后,当前子树已恢复平衡,无需继续向上检查,必须加break,否则会导致无限循环,这是我最初的致命错误之一;
- 根节点强制变黑:无论调整过程中根节点是否变为红色,最终都要强制设置_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; // 释放当前节点
}
实现析构函数时,核心注意事项:必须采用后序遍历,先销毁左右子树,再销毁当前节点,避免出现"先销毁当前节点,导致无法访问其左右子树"的问题,从而造成内存泄漏。最初我曾采用前序遍历,导致部分节点无法被销毁,后续修改为后序遍历,解决了内存泄漏问题。