前言
本文将与读者一同使用C++语言完成红黑树的编写。阅读前,希望你已经掌握了红黑树的基本原理,并对各种操作有印象。如果你不确定自己的知识储备是否足以继续阅读,欢迎阅读《红黑树详解》文章:
后文将针对代码设计与实现展开研究,不再对红黑树的性质与原理等进行过多的赘述。
直接查看源码
如果你不想跟着本文一起学习,而是想直接看代码,请到这里:
RedBlackTree - Github
设计
希望的效果
我们希望按照面向对象的思路,将红黑树封装到类内。从外部看,并不知道它内部是一个红黑树结构,只知道它具有键到数据的映射能力(注意,《红黑树详解》因专注探讨红黑树本身的性质,只考虑了键,忽略了数据。实际编写时不能这样简化)。 即:我们希望我们的红黑树类对外具有以下性质:
- 键的数据类型任意。
- 数据的数据类型任意。
- 拥有“设置”能力。
- 拥有“查找”能力。
- 拥有“删除”能力。
例如,我们希望设计一个从字符串到整数的映射,那么我们在创建对象的时候,希望能像下面这样创建:
RedBlackTree<std::string, int> obj;
之后,我们希望能对对象实现以下操作:
obj.setData("沈", 1);
obj.setData("陈", 2);
obj.setData("彭", 3);
int a = obj.getData("陈");
obj.setData("彭", 5);
a = obj.getData("彭");
a = obj.getData("李");
obj.removeKey("彭");
a = obj.getData("彭");
如上,这个类对外表现的能力刚好对应了红黑树的插入、查找、删除操作。但在调用者看来,他并不知道你的类内部是什么结构,他只是发现你做的这个类能够实现一定的映射功能,并且很好用。 接下来,我们将一起完成这个红黑树类的编写,并一同完成单元测试。
节点结构
因为红黑树是由节点组成的,我们先把节点定义好。 由于节点有两种颜色,我们补充定义一个枚举类来表示颜色:
enum class NodeColor {
RED, BLACK
};
节点本身应该存储以下信息:
- 节点键值
- 节点数据
- 节点颜色
- 父节点指针
- 左孩子指针
- 右孩子指针
因为我们希望节点键值和数据都是任意的,我们需要引入一个模板(注意,所谓“任意”,指的是我们可以用这个类创建适用于不同数据类型的对象。对于已经创建出来的对象,它的键和数据的类型不能变)。
template <
typename KeyType,
typename DataType
>
如此,节点的结构体可以这样定义:
struct Node {
KeyType key;
DataType data;
NodeColor color;
Node* father;
Node* leftChild;
Node* rightChild;
};
红黑树类
红黑树由节点组成,节点之间构成树形图。 如节点的定义一样,我们希望这个类适用于不同数据类型,因此要借助函数模板。
template <typename KeyType, typename DataType>
class RedBlackTree {}
只要知道根节点,就能遍历整棵树。 我们为红黑树类添加一个私有成员,用于存储根节点:
private:
Node* root = nullptr;
前面的分析已经提到,我们的红黑树需要对外展现出查找、插入、删除的能力。因此,我们设计三个函数:
public:
DataType& getData(const KeyType& key);
RedBlackTree<KeyType, DataType>& setData(const KeyType& key, const DataType& data);
RedBlackTree<KeyType, DataType>& removeKey(const KeyType& key);
为方便对象的使用,我们加入“清空”能力和“查询键是否存在”能力:
public:
bool hasKey(const KeyType& queryKey);
void clear();
你一定注意到,setData() 和removeKey() 的返回类型是一个红黑树对象的引用。对此,可能产生两个问题。
Q:为什么要这么设计?
A:我们将这两个函数的返回设为对象自身,以此提供连续操作的功能。如下:
RedBlackTree<std::string, int> obj;
obj.setData("a", 97)
.setData("c", 99)
.setData("d", 100)
.setData("f", 102);
obj.removeKey("c")
.removeKey("d");
Q:对于removeKey(...) 函数,如何告知调用者是否删除成功?
A:我们选择在无法找到删除目标时抛出异常,以此告知调用者。因此,调用者可以这样编写代码:
try {
obj.removeKey("x");
std::cout << "删除成功!" << std::endl;
}
catch (std::runtime_error& e) {
std::cout << "删除失败!" << std::endl;
std::cout << "原因:" << e.what() << std::endl;
}
当然,我们需要设计构造函数 和析构函数 。 为方便操作,我们再加入以下内部函数:
- 释放内存
- 左旋
- 右旋
- 修复“连续红色节点”问题
- 修复“节点失衡问题”
public:
RedBlackTree();
~RedBlackTree();
private:
void cleanup(Node* node);
void rotateLeft(Node* node);
void rotateRight(Node* node);
void fixContinuousRedNodeProblem(Node* node);
void fixUnbalancedChildrenProblem(Node* node);
接下来,我们将上面的功能一一实现即可。
功能实现
内存释放函数(cleanup)
回忆函数定义的样子:
void cleanup(Node* node);
过程与二叉树的内存释放方式没有区别。很简单。
if (node->leftChild != nullptr) {
cleanup(node->leftChild);
}
if (node->rightChild != nullptr) {
cleanup(node->rightChild);
}
delete node;
注意,我们没有对传入节点 node 做非空判断。由于这是内部函数,我们保证自己在调用这个函数时先对传入节点做非空判断即可。 事实上,我们只会往这个函数传入根节点。
析构函数和清空(clear)函数
函数定义如下:
~RedBlackTree();
void clear();
因为我们已经写好了内存释放函数,直接调用它就行。但别忘了对根节点做非空检查。
if (this->root != nullptr) {
this->cleanup(this->root);
this->root = nullptr;
}
左旋
函数定义如下:
void rotateLeft(Node* node);
在草稿纸上画好左旋操作涉及的所有节点,旋转前后的图形,想清楚涉及哪些指针的改变,然后即可编写代码。
Node* father = node->father;
Node* targetRoot = node->rightChild;
if (father == nullptr) {
this->root = targetRoot;
}
else {
if (node == father->leftChild) {
father->leftChild = targetRoot;
}
else {
father->rightChild = targetRoot;
}
}
targetRoot->father = father;
node->rightChild = targetRoot->leftChild;
if (node->rightChild != nullptr) {
node->rightChild->father = node;
}
targetRoot->leftChild = node;
node->father = targetRoot;
右旋
此部分与左旋高度相似,希望读者自行完成。也可通过文末链接直接查看完整源代码。
获取数据(getData)
定义如下:
DataType& getData(const KeyType& key);
我们先按照二叉查找树的方式尝试查找节点。如果找到了,就把节点的数据返回。否则,抛出异常。
Node* currentNode = root;
while (currentNode != nullptr) {
if (key == currentNode->key) {
return currentNode->data;
}
else if (key < currentNode->key) {
currentNode = currentNode->leftChild;
}
else {
currentNode = currentNode->rightChild;
}
}
throw std::runtime_error("could not find your key in the object.");
判断键是否存在(hasKey)
如果你看懂了获取数据(getData) 的过程,判断存在(hasKey) 对你来说应该非常简单。希望你能自行完成。
插入数据(setData)
函数定义:
RedBlackTree<KeyType, DataType>& setData(const KeyType& key, const DataType& data);
当我们找不到键时,要创建新节点,并检查新节点的加入是否破坏了红黑树的性质。 如果节点正在树上,更新值即可,不用做其他操作。
首先,我们要寻找键,并处理键已在树上的情况。这种情况十分简单。
Node* currentNode = root;
Node* currentFather = nullptr;
while (currentNode != nullptr) {
if (key == currentNode->key) {
currentNode->data = data;
return *this;
}
else {
currentFather = currentNode;
currentNode = (key < currentNode->key ? currentNode->leftChild : currentNode->rightChild);
}
}
之后,我们创建新节点,并尝试插入。注意,如果查找后的父节点也是空的,说明要插入的节点是红黑树的第一个节点,也就是根节点。此时,这个节点应该设为黑色。
currentNode = new Node;
currentNode->father = currentFather;
currentNode->leftChild = nullptr;
currentNode->rightChild = nullptr;
currentNode->key = key;
currentNode->data = data;
if (currentFather == nullptr) {
currentNode->color = NodeColor::BLACK;
this->root = currentNode;
return *this;
}
否则,我们要将节点设为红色,然后尝试进行“连续红色节点问题”的修复。 注意,我们有两种处理逻辑可以选择。
-
先检测是否存在问题,在存在问题时再调用函数。这时,我们认为“修复”函数的功能是修正已有问题。 -
我们可以直接调用“修复”函数,将检测过程写到“修复”函数内。这样,“修复”函数同时具备了检测和检测后修正的能力。
为让代码显得更加优雅,我们选择后者。
currentNode->color = NodeColor::RED;
if (key < currentFather->key) {
currentFather->leftChild = currentNode;
}
else {
currentFather->rightChild = currentNode;
}
this->fixContinuousRedNodeProblem(currentNode);
return *this;
删除键
这是整个红黑树最难的部分。我们还是先回顾函数定义:
RedBlackTree<KeyType, DataType>& removeKey(const KeyType& key);
首先,我们需要查找删除的键是不是在树上。如果不是,就抛出异常。
Node* currentNode = root;
while (currentNode != nullptr) {
if (key == currentNode->key) {
break;
}
else {
currentNode = (key < currentNode->key ? currentNode->leftChild : currentNode->rightChild);
}
}
if (currentNode == nullptr) {
throw std::runtime_error("could not find your key in the object.");
}
如果函数没有抛出异常,说明已经找到了要删除的节点。我们采用替代法,将要删除的目标移动到叶子部位。 特别注意,移动过程中,我们需要分多种情况分别处理。
- 该节点有一个孩子还是两个孩子?
- 该节点的替代者,是不是该节点的孩子?
对于第一个问题,如果节点有两个孩子,或者只有右孩子,寻找后继节点替代。否则,寻找前驱节点替代。 对于第二个问题,如果替代节点的父亲不是原节点,则删除时涉及替代节点的父亲及原节点的所有孩子的变动。但是,当替代节点正是原节点的亲孩子时,直接交换指针可能会导致出现自环。
如图所示(黄色表示原节点,红色表示替代节点):
此外,在修改其他节点信息的时候,对于可以为空的节点,必须先做非空检查。
while (currentNode->leftChild != nullptr || currentNode->rightChild != nullptr) {
if (currentNode->rightChild != nullptr) {
Node* replacementNode = currentNode->rightChild;
while (replacementNode->leftChild != nullptr) {
replacementNode = replacementNode->leftChild;
}
struct {
Node* leftChild;
Node* rightChild;
NodeColor color;
Node* father;
} currentNodeInfo = {
currentNode->leftChild, currentNode->rightChild,
currentNode->color, currentNode->father
}, replacementNodeInfo = {
replacementNode->leftChild, replacementNode->rightChild,
replacementNode->color, replacementNode->father
};
currentNode->color = replacementNodeInfo.color;
replacementNode->color = currentNodeInfo.color;
if (currentNodeInfo.father != nullptr) {
if (currentNodeInfo.father->leftChild == currentNode) {
currentNodeInfo.father->leftChild = replacementNode;
}
else {
currentNodeInfo.father->rightChild = replacementNode;
}
}
else {
this->root = replacementNode;
}
if (currentNodeInfo.leftChild != nullptr) {
currentNodeInfo.leftChild->father = replacementNode;
}
if (replacementNodeInfo.rightChild != nullptr) {
replacementNodeInfo.rightChild->father = currentNode;
}
currentNode->leftChild = nullptr;
currentNode->rightChild = replacementNodeInfo.rightChild;
if (replacementNodeInfo.father == currentNode) {
currentNode->father = replacementNode;
}
else {
currentNode->father = replacementNodeInfo.father;
}
replacementNode->leftChild = currentNodeInfo.leftChild;
replacementNode->father = currentNodeInfo.father;
if (currentNodeInfo.rightChild == replacementNode) {
replacementNode->rightChild = currentNode;
}
else {
replacementNode->rightChild = currentNodeInfo.rightChild;
}
if (currentNodeInfo.rightChild != replacementNode) {
currentNodeInfo.rightChild->father = replacementNode;
replacementNodeInfo.father->leftChild = currentNode;
}
}
else {
Node* replacementNode = currentNode->leftChild;
while (replacementNode->rightChild != nullptr) {
replacementNode = replacementNode->rightChild;
}
struct {
Node* leftChild;
Node* rightChild;
NodeColor color;
Node* father;
} currentNodeInfo = {
currentNode->leftChild, currentNode->rightChild,
currentNode->color, currentNode->father
}, replacementNodeInfo = {
replacementNode->leftChild, replacementNode->rightChild,
replacementNode->color, replacementNode->father
};
currentNode->color = replacementNodeInfo.color;
replacementNode->color = currentNodeInfo.color;
if (currentNodeInfo.father != nullptr) {
if (currentNodeInfo.father->leftChild == currentNode) {
currentNodeInfo.father->leftChild = replacementNode;
}
else {
currentNodeInfo.father->rightChild = replacementNode;
}
}
else {
this->root = replacementNode;
}
if (currentNodeInfo.rightChild != nullptr) {
currentNodeInfo.rightChild->father = replacementNode;
}
if (replacementNodeInfo.leftChild != nullptr) {
replacementNodeInfo.leftChild->father = currentNode;
}
currentNode->rightChild = nullptr;
currentNode->leftChild = replacementNodeInfo.leftChild;
if (replacementNodeInfo.father == currentNode) {
currentNode->father = replacementNode;
}
else {
currentNode->father = replacementNodeInfo.father;
}
replacementNode->rightChild = currentNodeInfo.rightChild;
replacementNode->father = currentNodeInfo.father;
if (currentNodeInfo.leftChild == replacementNode) {
replacementNode->leftChild = currentNode;
}
else {
replacementNode->leftChild = currentNodeInfo.leftChild;
}
if (currentNodeInfo.leftChild != replacementNode) {
currentNodeInfo.leftChild->father = replacementNode;
replacementNodeInfo.father->rightChild = currentNode;
}
}
}
如果删除目标是根,我们需要在删除节点的同时,将红黑树对象的根设为空。 如果删除目标是红色节点,则在删除并取消其父节点对它的绑定后,不用做其他操作。 如果删除目标是黑色节点,需要做分类处理。
if (currentNode == this->root) {
this->root = nullptr;
delete currentNode;
}
else if (currentNode->color == NodeColor::RED)
{
if (currentNode == currentNode->father->leftChild)
{
currentNode->father->leftChild = nullptr;
}
else {
currentNode->father->rightChild = nullptr;
}
delete currentNode;
}
else {
...
}
如果你仔细阅读了前面的所有代码,会发现,我们一直要对“左”和“右”做分别的处理。为方便这个过程,我们补充定义一个枚举类:
enum class ChildSide {
LEFT, RIGHT
};
之后,寻找兄弟节点,然后按照之前分析过的进行分类处理即可。
Node* sibling = (currentNode->father->leftChild != currentNode ?
currentNode->father->leftChild : currentNode->father->rightChild);
Node* currentFather = currentNode->father;
ChildSide siblingSideToFather =
(sibling == currentFather->leftChild ? ChildSide::LEFT : ChildSide::RIGHT);
if (currentFather->leftChild == currentNode) {
currentFather->leftChild = nullptr;
}
else {
currentFather->rightChild = nullptr;
}
delete currentNode;
之后,对不同情况分别做处理。情况非常多,在此先展示分类方式:
if (currentFather->color == NodeColor::RED) {
if (sibling->leftChild != nullptr && sibling->rightChild != nullptr) {...}
else if (siblingSideToFather == ChildSide::RIGHT && sibling->leftChild != nullptr) {...}
else if (siblingSideToFather == ChildSide::LEFT && sibling->rightChild != nullptr) {...}
else if (siblingSideToFather == ChildSide::RIGHT && sibling->rightChild != nullptr) {...}
else if (siblingSideToFather == ChildSide::LEFT && sibling->leftChild != nullptr) {...}
else {
...
}
}
else {
if (sibling->color == NodeColor::BLACK
&& sibling->leftChild != nullptr && sibling->rightChild != nullptr)
{
if (siblingSideToFather == ChildSide::RIGHT) {...}
else {...}
}
else if (sibling->color == NodeColor::BLACK &&
(sibling->leftChild != nullptr || sibling->rightChild != nullptr))
{
if (siblingSideToFather == ChildSide::RIGHT && sibling->rightChild != nullptr) {...}
else if (siblingSideToFather == ChildSide::RIGHT && sibling->leftChild != nullptr) {...}
else if (siblingSideToFather == ChildSide::LEFT && sibling->leftChild != nullptr) {...}
else {...}
}
else if (sibling->color == NodeColor::BLACK) {
if (currentFather->leftChild == nullptr) {...}
else {...}
}
else {
sibling->color = NodeColor::BLACK;
currentFather->color = NodeColor::RED;
if (siblingSideToFather == ChildSide::RIGHT) {
...
if (currentFather->rightChild != nullptr) {...}
}
else {
...
if (currentFather->leftChild != nullptr) {...}
}
}
}
篇幅有限,无法将所有情况的修复代码一一呈现。现挑选两个具有代表性的情况进行讲解。
父节点是红色,并且兄弟有两个孩子时:
if (sibling->leftChild != nullptr && sibling->rightChild != nullptr)
{
sibling->color = NodeColor::RED;
currentFather->color = NodeColor::BLACK;
if (siblingSideToFather == ChildSide::RIGHT) {
sibling->rightChild->color = NodeColor::BLACK;
this->rotateLeft(currentFather);
}
else {
sibling->leftChild->color = NodeColor::BLACK;
this->rotateRight(currentFather);
}
}
父节点是黑色,兄弟是黑色,但是兄弟没有孩子时:
if (currentFather->leftChild == nullptr) {
currentFather->rightChild->color = NodeColor::RED;
this->fixUnbalancedChildrenProblem(currentFather);
}
else {
currentFather->leftChild->color = NodeColor::RED;
this->fixUnbalancedChildrenProblem(currentFather);
}
修复“连续红色问题”
函数定义:
void fixContinuousRedNodeProblem(Node* node);
当一次修复后,局部根节点和其父节点同为红色时,该函数会被递归调用。对此,我们不能真的让它递归运行,不然可能出现栈溢出问题。 由于这个“递归”过程很简单,我们完全可以转换为循环运行。 循环中,要注意对红黑树树根的特殊处理。因为它是没有父亲的,同时也是“递归”出口。
Node* currentNode = node;
while (currentNode->color == NodeColor::RED) {
Node* currentFather = currentNode->father;
if (currentFather == nullptr) {
currentNode->color = NodeColor::BLACK;
break;
}
else if (currentFather->color == NodeColor::BLACK) {
break;
}
Node* currentGrandpa = currentFather->father;
Node* uncle =
(currentGrandpa->leftChild != currentFather ?
currentGrandpa->leftChild : currentGrandpa->rightChild);
if (uncle != nullptr && uncle->color == NodeColor::RED) {
uncle->color = NodeColor::BLACK;
currentFather->color = NodeColor::BLACK;
currentGrandpa->color = NodeColor::RED;
currentNode = currentGrandpa;
}
else {
if (currentFather == currentGrandpa->leftChild) {
if (currentNode == currentFather->leftChild) {
this->rotateRight(currentGrandpa);
currentFather->color = NodeColor::BLACK;
currentGrandpa->color = NodeColor::RED;
}
else {
this->rotateLeft(currentFather);
this->rotateRight(currentGrandpa);
currentGrandpa->color = NodeColor::RED;
currentNode->color = NodeColor::BLACK;
}
}
else {
if (currentNode == currentFather->rightChild) {
this->rotateLeft(currentGrandpa);
currentFather->color = NodeColor::BLACK;
currentGrandpa->color = NodeColor::RED;
}
else {
this->rotateRight(currentFather);
this->rotateLeft(currentGrandpa);
currentGrandpa->color = NodeColor::RED;
currentNode->color = NodeColor::BLACK;
}
}
return;
}
}
修复“失衡问题”
函数定义:
void fixUnbalancedChildrenProblem(Node* node);
该函数也涉及多种情况的分析讨论,同时也涉及“递归”问题。同样,我们使用循环来规避递归调用函数。 注意,对于修复成功的情况,需要在对应代码块内跳出循环(break)。
Node* currentNode = node;
while (currentNode != this->root) {
Node* currentFather = currentNode->father;
Node* sibling = (currentFather->leftChild != currentNode ?
currentFather->leftChild : currentFather->rightChild);
ChildSide siblingSideToFather =
(sibling == currentFather->leftChild ? ChildSide::LEFT : ChildSide::RIGHT);
if (currentFather->color == NodeColor::RED) {
if (siblingSideToFather == ChildSide::RIGHT
&& sibling->leftChild->color == NodeColor::BLACK) {...}
else if (siblingSideToFather == ChildSide::LEFT
&& sibling->rightChild->color == NodeColor::BLACK) {...}
else if (siblingSideToFather == ChildSide::RIGHT
&& sibling->rightChild->color == NodeColor::BLACK)
{
...
}
else if (siblingSideToFather == ChildSide::LEFT
&& sibling->leftChild->color == NodeColor::BLACK)
{
...
}
else {
...
}
}
else {
if (sibling->color == NodeColor::RED) {
...
}
else {
if (sibling->leftChild->color == NodeColor::BLACK
&& sibling->rightChild->color == NodeColor::BLACK) {...}
else if (siblingSideToFather == ChildSide::RIGHT
&& sibling->rightChild->color == NodeColor::RED) {...}
else if (siblingSideToFather == ChildSide::LEFT
&& sibling->leftChild->color == NodeColor::RED) {...}
else if (siblingSideToFather == ChildSide::RIGHT) {...}
else {...}
}
}
}
我们选其中一种情况看看:当父节点为黑色、兄弟也是黑色、兄弟的孩子都是黑色时:
sibling->color = NodeColor::RED;
currentNode = currentFather;
单元测试
我们编写简单的单元测试程序来检测红黑树类的运行是否正常。
#include <string>
#include "pch.h"
#include "CppUnitTest.h"
#include "../Red Black Tree/RedBlackTree.hpp"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace RedBlackTreeTest
{
TEST_CLASS(RedBlackTreeTest)
{
public:
TEST_METHOD(OrderedSetGetTest) {
RedBlackTree<int, int> tree;
for (int i = 0; i < 20; i++) {
tree.setData(i, i * i);
}
for (int i = 0; i < 20; i++) {
Assert::AreEqual(i * i, tree.getData(i));
}
}
TEST_METHOD(RandomSetGetTest) {
int inputList[20] = {
12, 13, 10, 3, 14, 15, 1, 2, 17, 0, 6, 7, 8, 4, 18, 5, 9, 11, 19, 16
};
RedBlackTree<int, int> tree;
for (int i = 0; i < 20; i++) {
tree.setData(inputList[i], inputList[i] * inputList[i]);
}
for (int i = 0; i < 20; i++) {
Assert::AreEqual(i * i, tree.getData(i));
}
for (int i = 20; i < 30; i++) {
try {
tree.getData(i);
Assert::IsFalse(true);
}
catch (...) {
Assert::IsTrue(true);
}
}
}
TEST_METHOD(HasKeyFunctionTest) {
RedBlackTree<int, int> tree;
for (int i = 0; i < 20; i++) {
tree.setData(i, i * i);
}
for (int i = 0; i < 10; i++) {
Assert::AreEqual(i * 5 < 20, tree.hasKey(i * 5));
}
}
TEST_METHOD(MissFetchTest) {
RedBlackTree<int, int> tree;
for (int i = 0; i < 20; i++) {
tree.setData(i, i * i);
}
for (int i = 0; i < 10; i++) {
try {
tree.getData(i * 5);
Assert::AreEqual(true, i * 5 < 20);
}
catch (...) {
Assert::AreEqual(false, i * 5 < 20);
}
}
}
TEST_METHOD(RemoveKeyTest) {
RedBlackTree<int, int> tree;
for (int i = 0; i < 20; i++) {
tree.setData(i, i * i);
}
for (int i = 0; i < 20; i += 2) {
tree.removeKey(i);
}
for (int i = 0; i < 20; i++) {
Assert::AreEqual(i % 2 != 0, tree.hasKey(i));
}
}
TEST_METHOD(MixedHashOperationTest) {
srand((unsigned int)time(0));
RedBlackTree<std::string, int> tree;
struct TestObj {
std::string key;
int value;
} testArray[] = {
{"沈", rand()},
{"家的", rand()},
{"公子", rand()},
{"最最最", rand()},
{"帅的", rand()},
{"曹安", rand()},
{"公路", rand()},
{"4800号", rand()},
{"四平路1239号", rand()},
{"zbhyyds", rand()},
{"zyfdltql", rand()},
{"larrytj", rand()},
{"黄渡", rand()},
{"安亭", rand()},
{"花桥", rand()},
{"昆山", rand()},
};
const int testArraySize = sizeof(testArray) / sizeof(TestObj) / 2 * 2;
for (int i = 0; i < testArraySize; i += 2) {
Assert::IsFalse(tree.hasKey(testArray[i].key));
tree.setData(testArray[i].key, testArray[i].value);
Assert::IsTrue(tree.hasKey(testArray[i].key));
}
for (int i = 0; i < testArraySize; i++) {
Assert::AreEqual(i % 2 == 0, tree.hasKey(testArray[i].key));
}
for (int i = testArraySize - 1; i >= 0; i--) {
Assert::AreEqual(i % 2 == 0, tree.hasKey(testArray[i].key));
try {
int value = tree.getData(testArray[i].key);
Assert::AreEqual(testArray[i].value, value);
}
catch (...) {
Assert::IsTrue(i % 2 != 0);
}
}
for (int i = 0; i < testArraySize; i += 2) {
tree.setData(testArray[i].key, testArray[i + 1].value);
tree.setData(testArray[i + 1].key, testArray[i + 1].value);
}
for (int i = 0; i < testArraySize; i += 2) {
Assert::IsTrue(tree.hasKey(testArray[i].key));
Assert::IsTrue(tree.hasKey(testArray[i + 1].key));
Assert::AreEqual(testArray[i + 1].value, tree.getData(testArray[i].key));
Assert::AreEqual(testArray[i + 1].value, tree.getData(testArray[i + 1].key));
}
for (int i = 0; i < testArraySize; i += 2) {
Assert::IsTrue(tree.hasKey(testArray[i].key));
tree.removeKey(testArray[i].key);
Assert::IsFalse(tree.hasKey(testArray[i].key));
for (int j = i + 2; j < testArraySize; j += 2) {
Assert::AreEqual(testArray[j + 1].value, tree.getData(testArray[j].key));
Assert::AreEqual(testArray[j + 1].value, tree.getData(testArray[j + 1].key));
}
}
for (int i = 0; i < testArraySize; i++) {
Assert::AreEqual(i % 2 != 0, tree.hasKey(testArray[i].key));
try {
tree.getData(testArray[i].key);
Assert::IsTrue(i % 2 != 0);
}
catch (...) {
Assert::IsTrue(i % 2 == 0);
}
}
}
};
}
这个单元测试显然无法完成对整个红黑树结构的测试,但是可以帮助发现一些隐藏的问题。通过测试不代表编写的代码正确,但不通过测试是可以说明代码编写有疏漏的。
后记
到这里,你已经大概了解如何使用C++完成红黑树结构的编写。如果你想阅读完整源码,请到文末链接处查看。 红黑树的一大复杂之处正在于其情况之多。实际编写易用的红黑树结构时,还需要考虑很多其他的问题。 我们在《红黑树详解》中,对红黑树做了很多简化,以此躲避这些外围的问题,这样才能静下心研究红黑树本身。然而在编写代码时,无法逃避这些。虽然每种情况的代码都很简单,但必须分类清晰。但凡有一点错误,查错过程都会是很艰辛的。 希望我的文章对你的学习帮助有启发作用,也感谢你的阅读。
源码文件获取
你已经完成了使用C++编写红黑树的学习。你可以到这里获取完整源码:
RedBlackTree - Github
|