从零开始手写STL库–红黑树的实现
Gihub链接:miniSTL
一、红黑树是什么?
红黑树为一种自平衡的二叉查找树。
其产生的原因是,传统的平衡二叉查找树在面对递增或递减数据时,操作效率会急剧下降,频繁地旋转会消耗很多时间。
具体地,红黑树在平衡二叉树的节点中附加了一个额外的属性:颜色
,非红即黑
其具有的性质为:
黑高度相等代表的意思就是这个树的叶子节点之间的层高差<2,也就达到了近似平衡
红黑树自然是由Node构成,其专属Node一般包含:
二、红黑树要包含什么函数
红黑树的插入操作一般包含三个步骤:插入新节点,初始化为红色; 检查红黑树性质; 调整红黑树
(实际上插入的新节点必须是红色,进一步地以此为基础,调整树形来满足父子颜色不同、叶子&根为黑色的性质)
其中也需要两个平衡二叉树的固有操作:左旋和右旋
基础成员部分
首先定义好Node部分,并且适当地写好构造函数,虽然不写也能过
enum class Color { RED, BLACK };
template <typename Key, typename Value>
class myRedBlackTree{
class Node
{
public:
Key key;
Value value;
Color color;
Node * left;
Node * right;
Node * parent;
Node(const Key & k, const Value & v, Color c, Node * p = nullptr)
: key(k), value(v), color(c), left(nullptr), right(nullptr), parent(p) {}
Node()
: color(Color::BLACK), left(nullptr), right(nullptr), parent(nullptr) {}
};
};
有了节点,就可以定义整个树的元素了,包括头节点、长度和哨兵节点
哨兵节点是真正意义上的尾节点,所有的叶子节点都要指向该节点
从根节点沿着任意路径到哨兵节点,路径上的黑色节点数总数是相同的,这样才是红黑树
private:
Node * root;
size_t size;
Node * Nil;
基础函数部分
这些函数依然是在private下的,不能暴露给用户使用
插入功能
首先是左右旋函数,这里建议看一下红黑树左右旋,动图很清晰明了
void rightRotate(Node * node)
{
Node *l_son = node->left;
node->left = l_son->right;
if(l_son->right) l_son->right->parent = node;
l_son->parent = node->parent;
if(!node->parent) root = l_son;
else if(node == node->parent->left) node->parent->left = l_son;
else node->parent->right = l_son;
l_son->right = node;
node->parent = l_son;
}
右旋函数包括以下步骤:
1、记录左子节点
2、把左子节点的右节点接到node上,双向的处理
3、再用左子节点代替node的位置,与node的parent双向连接
4、最后处理左子节点和node的双向连接
看着动图写,逻辑很清楚,左旋的逻辑基本一样
void leftRotate(Node *node)
{
Node *r_son = node->right;
node->right = r_son->left;
if (r_son->left) r_son->left->parent = node;
r_son->parent = node->parent;
if (!node->parent) root = r_son;
else if (node == node->parent->left) node->parent->left = r_son;
else node->parent->right = r_son;
r_son->left = node;
node->parent = r_son;
}
当目标节点的父节点存在且父节点的颜色是红色时,需要修复(因为目标节点初始化为红色,冲突了)
所以在private中还存在一个修复函数,这里需要分类处理:
明确几个定义:
总共涉及四个节点:父节点、叔节点、爷节点、子节点,关系是:爷->[父->(子),叔]
情况一:父、叔都是红色
处理方式:父、叔都设为黑色,爷设为红色,并将爷设为新的“子”,迭代处理
情况二:父=红,叔=黑,父为左子树&子为右子树
处理方式:将父设为新的“子”,再左旋;新“子”的父设为黑,新“子”的爷设为红;再对新“子”的爷右旋
情况三:父=红,叔=黑,父为右子树&子为左子树
处理方式:将父设为新的“子”,再右旋;新“子”的父设为黑,新“子”的爷设为红;再对新“子”的爷左旋
情况二和三的操作是对称的
void insertFixup(Node *target)
{
while (target->parent && target->parent->color == Color::RED) // father is red
{
if (target->parent == target->parent->parent->left) // father is left of grandfather
{
Node *uncle = target->parent->parent->right;
if (uncle && uncle->color == Color::RED) // father & uncle is red
{
target->parent->color = Color::BLACK;
uncle->color = Color::BLACK;
target->parent->parent->color = Color::RED;
target = target->parent->parent;
}
else // uncle is black or NULL
{
if (target == target->parent->right)
{
target = target->parent;
leftRotate(target);
}
target->parent->color = Color::BLACK;
target->parent->parent->color = Color::RED;
rightRotate(target->parent->parent);
}
}
else // father is right of grandfather
{
Node *uncle = target->parent->parent->left;
if (uncle && uncle->color == Color::RED)
{
target->parent->color = Color::BLACK;
uncle->color = Color::BLACK;
target->parent->parent->color = Color::RED;
target = target->parent->parent;
}
else
{
if (target == target->parent->left)
{
target = target->parent;
rightRotate(target);
}
target->parent->color = Color::BLACK;
target->parent->parent->color = Color::RED;
leftRotate(target->parent->parent);
}
}
}
root->color = Color::BLACK;
}
接着就可以考虑插入节点的处理了,正常地比较大小,顺序地找到最合适的位置,插入后处理父子节点关系
然后就可以进行树的检查了,是否还满足红黑树性质,也就是调用修复函数
void insertNode(const Key &key, const Value &value)
{
Node *newNode = new Node(key, value, Color::RED);
Node *parent = nullptr;
Node *cmpNode = root;
while (cmpNode)
{
parent = cmpNode;
if (newNode->key < cmpNode->key) cmpNode = cmpNode->left; // smaller -> left
else if (newNode->key > cmpNode->key) cmpNode = cmpNode->right; // bigger -> right
else // already existed
{
delete newNode;
return;
}
}
size++;
newNode->parent = parent; // align the parent to son
if (!parent) root = newNode;
else if (newNode->key < parent->key) parent->left = newNode;
else parent->right = newNode;
insertFixup(newNode); // check RedBlackTree
}
删除功能
删除节点要考虑的事情就非常多了
先定义一个简单的查找函数,用来搜索到要删除的节点:
Node *lookUp(Key key)
{
Node *cmpNode = root;
while (cmpNode)
{
if (key < cmpNode->key) cmpNode = cmpNode->left;
else if (key > cmpNode->key) cmpNode = cmpNode->right;
else return cmpNode;
}
return cmpNode;
}
以及一个替换函数,用新节点替换旧节点:
void replaceNode(Node *targetNode, Node *newNode)
{
if (!targetNode->parent) root = newNode;
else if (targetNode == targetNode->parent->left) targetNode->parent->left = newNode;
else targetNode->parent->right = newNode;
if (newNode) newNode->parent = targetNode->parent;
}
额外的定义一个查找左叶子节点函数,功能是返回某个子树下最小值所在的节点:
Node *findMinimumNode(Node *node)
{
while (node->left) node = node->left;
return node;
}
为了方便操作,对节点的颜色处理也定义好函数:
Color getColor(Node *node)
{
if (node == nullptr) return Color::BLACK;
return node->color;
}
void setColor(Node *node, Color color)
{
if (node == nullptr) return;
node->color = color;
}
接下来就可以考虑如何从删除节点后的树中恢复红黑树性质了,即针对删除功能的修复函数
首先判断是否为空,如果当前节点为哨兵节点,且父节点为空,那么明显满足红黑树,返回即可
接着从节点开始遍历,一直遍历到根节点,检查父子节点性质
明确几个定义:
总共涉及三个节点:父节点、兄节点、子节点,关系是:父->(子,兄)
情况一:兄节点为红色
处理方式:兄节点改为黑色,父节点改为红色,并且以父节点左旋(相当于交换父兄节点)
情况二:兄节点为黑色,兄节点的子节点全为黑色
处理方式:兄节点改为红色,并且递归点设置为父节点
情况三:兄节点为黑,其左子为红右子为黑
处理方式:子节点全改黑,并将兄改红,右旋
情况四:兄节点为黑,其左子为黑右子为红
处理方式:子节点全改黑,父节点改黑,并左选父节点
void removeFixup(Node *node)
{
if (node == Nil && node->parent == nullptr) return;
while (node != root)
{
if (node == node->parent->left) // son is left
{
Node *sibling = node->parent->right; // bro is right
if (getColor(sibling) == Color::RED) // bro is red
{
setColor(sibling, Color::BLACK);
setColor(node->parent, Color::RED);
leftRotate(node->parent);
sibling = node->parent->right;
}
if (getColor(sibling->left) == Color::BLACK &&
getColor(sibling->right) == Color::BLACK) // bro and nephew are black
{
setColor(sibling, Color::RED);
node = node->parent;
if (node->color == Color::RED)
{
node->color = Color::BLACK;
node = root;
}
}
else
{
if (getColor(sibling->right) == Color::BLACK)
{
setColor(sibling->left, Color::BLACK);
setColor(sibling, Color::RED);
rightRotate(sibling);
sibling = node->parent->right;
}
setColor(sibling, getColor(node->parent));
setColor(node->parent, Color::BLACK);
setColor(sibling->right, Color::BLACK);
leftRotate(node->parent);
node = root;
}
}
else
{
Node *sibling = node->parent->left;
if (getColor(sibling) == Color::RED)
{
setColor(sibling, Color::BLACK);
setColor(node->parent, Color::RED);
rightRotate(node->parent);
sibling = node->parent->left;
}
if (getColor(sibling->right) == Color::BLACK &&
getColor(sibling->left) == Color::BLACK)
{
setColor(sibling, Color::RED);
node = node->parent;
if (node->color == Color::RED)
{
node->color = Color::BLACK;
node = root;
}
}
else
{
if (getColor(sibling->left) == Color::BLACK)
{
setColor(sibling->right, Color::BLACK);
setColor(sibling, Color::RED);
leftRotate(sibling);
sibling = node->parent->left;
}
setColor(sibling, getColor(node->parent));
setColor(node->parent, Color::BLACK);
setColor(sibling->left, Color::BLACK);
rightRotate(node->parent);
node = root;
}
}
}
setColor(node, Color::BLACK);
}
有这些前置函数后,就可以进行删除节点操作了
首先定义几个需要用到的变量
Node *rep = del; // rep(替代节点)初始指向要删除的节点
Node *child = nullptr; // 要删除节点的孩子节点
Node *parentRP; // 替代节点的父节点
Color origCol = rep->color; // 保存要删除节点的原始颜色
分情况讨论:
若该节点无左或右孩子,那么用存在的孩子代替它即可
if (!del->left)
{
rep = del->right; // 替代节点更换为右子
parentRP = del->parent; // 替代父节点更新
origCol = getColor(rep); // 替代颜色更新
replaceNode(del, rep); // 替代操作,del节点从原树中脱离
}
else if (!del->right)
{
rep = del->left;
parentRP = del->parent;
origCol = getColor(rep);
replaceNode(del, rep);
}
否则说明该节点左右子都存在,这里取删除节点的直接后继节点作为替代节点,将删除节点替代掉即可
注意分类操作,替代节点与删除节点是否为父子关系会影响替代过程的写法,只要实现了替代就行
后续再进行一些判断即可,调用删除恢复函数来保持红黑树性质
void deleteNode(Node *del)
{
Node *rep = del;
Node *child = nullptr;
Node *parentRP;
Color origCol = rep->color;
if (!del->left)
{
rep = del->right;
parentRP = del->parent;
origCol = getColor(rep);
replaceNode(del, rep);
}
else if (!del->right)
{
rep = del->left;
parentRP = del->parent;
origCol = getColor(rep);
replaceNode(del, rep);
}
else
{
rep = findMinimumNode(del->right);
origCol = rep->color;
if (rep != del->right)
{
parentRP = rep->parent;
child = rep->right;
parentRP->left = child;
if (child != nullptr) child->parent = parentRP;
del->left->parent = rep;
del->right->parent = rep;
rep->left = del->left;
rep->right = del->right;
if (del->parent != nullptr)
{
if (del == del->parent->left)
{
del->parent->left = rep;
rep->parent = del->parent;
}
else
{
del->parent->right = rep;
rep->parent = del->parent;
}
}
else
{
root = rep;
root->parent = nullptr;
}
}
else
{
child = rep->right;
rep->left = del->left;
del->left->parent = rep;
if (del->parent != nullptr)
{
if (del == del->parent->left)
{
del->parent->left = rep;
rep->parent = del->parent;
}
else
{
del->parent->right = rep;
rep->parent = del->parent;
}
}
else
{
root = rep;
root->parent = nullptr;
}
parentRP = rep;
}
}
if (rep != nullptr) rep->color = del->color;
else origCol = del->color;
if (origCol == Color::BLACK)
{
if (child != nullptr)
removeFixup(child);
else
{
Nil->parent = parentRP;
if (parentRP != nullptr)
{
if (parentRP->left == nullptr) parentRP->left = Nil;
else parentRP->right = Nil;
}
removeFixup(Nil);
dieConnectNil();
}
}
delete del;
}
void dieConnectNil()
{
if (Nil == nullptr) return;
if (Nil->parent != nullptr)
{
if (Nil == Nil->parent->left) Nil->parent->left = nullptr;
else Nil->parent->right = nullptr;
}
}
最后再将这些函数包装一下,输出给用户使用即可
public:
myRedBlackTree() : root(nullptr), size(0), Nil(new Node()) {
Nil->color = Color::BLACK;
}
void insert( const Key & key, const Value & value) { insertNode(key, value); }
void remove( const Key & key)
{
Node * node = lookUp(key);
if(node)
{
deleteNode(node);
size --;
}
}
Value *at(const Key & key)
{
auto ans = lookUp(key);
if(ans) return &ans->value;
return nullptr;
}
int getSize() { return size; }
bool empty() { return size == 0;}
void clear() { deleteNode(root); size = 0;}
~myRedBlackTree()
{
deleteTree(root);
}
private:
void deleteTree(Node * node)
{
if(node)
{
deleteTree(node->left);
deleteTree(node->right);
delete node;
}
}
完整代码参考miniSTL
总结
红黑树的构建实际上是非常深入的一个工作,理论上讲应该按照:二叉搜索树-二叉平衡搜索树-红黑树 这样的顺序进行学习
而且内容多,知识杂,在408考试里也属于是边缘且难度极大的内容了,一篇博客属实无法全部讲解清楚
注意红黑树的五条性质即可。