C++ 实现红黑树结构

前言

本文将与读者一同使用C++语言完成红黑树的编写。阅读前,希望你已经掌握了红黑树的基本原理,并对各种操作有印象。如果你不确定自己的知识储备是否足以继续阅读,欢迎阅读《红黑树详解》文章:

后文将针对代码设计与实现展开研究,不再对红黑树的性质与原理等进行过多的赘述。

直接查看源码

如果你不想跟着本文一起学习,而是想直接看代码,请到这里:

RedBlackTree - Github

设计

希望的效果

我们希望按照面向对象的思路,将红黑树封装到类内。从外部看,并不知道它内部是一个红黑树结构,只知道它具有键到数据的映射能力(注意,《红黑树详解》因专注探讨红黑树本身的性质,只考虑了键,忽略了数据。实际编写时不能这样简化)。
即:我们希望我们的红黑树类对外具有以下性质:

  1. 键的数据类型任意。
  2. 数据的数据类型任意。
  3. 拥有“设置”能力。
  4. 拥有“查找”能力。
  5. 拥有“删除”能力。

例如,我们希望设计一个从字符串到整数的映射,那么我们在创建对象的时候,希望能像下面这样创建:

RedBlackTree<std::string, int> obj;

之后,我们希望能对对象实现以下操作:

// 设置三个从string到int的映射。
obj.setData("沈", 1);
obj.setData("陈", 2);
obj.setData("彭", 3);

int a = obj.getData("陈"); // 将“陈”对应的整数赋值给a. 此时,a应该等于2.

obj.setData("彭", 5); // 对已有数据,应该进行更新操作。

a = obj.getData("彭"); // 此时,a应该等于5. 
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);
    /**
	 * 修复“连续红色节点”问题。当出现当前节点和父节点同为红色时,需要进行修复。
	 * @param node “连续红色节点”中的子节点。
	 */
    void fixContinuousRedNodeProblem(Node* node);
    /**
	 * 修复“左右孩子不平衡”问题。当某边删除黑色节点后,可能产生此问题。
	 * @param 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; // 孩子有可能是 nullptr
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 { // key > currentNode->key
        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);
    }
}

// 如果是更新现有键值,则在上面的循环里会触发 return.

之后,我们创建新节点,并尝试插入。注意,如果查找后的父节点也是空的,说明要插入的节点是红黑树的第一个节点,也就是根节点。此时,这个节点应该设为黑色。

// 下面部分负责创建新的节点,完成新数据的存储。

/*
    此时,currentNode 指向 nullptr, currentFather 指向最后遍历到的节点,也可能是 nullptr.
    插入时,新节点设为红色,根据键值插入到最后一个节点的左或右。
*/

// 创建新节点。
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;
}

否则,我们要将节点设为红色,然后尝试进行“连续红色节点问题”的修复。
注意,我们有两种处理逻辑可以选择。

  1. 先检测是否存在问题,在存在问题时再调用函数。这时,我们认为“修复”函数的功能是修正已有问题。

  2. 我们可以直接调用“修复”函数,将检测过程写到“修复”函数内。这样,“修复”函数同时具备了检测和检测后修正的能力。

为让代码显得更加优雅,我们选择后者。

// 下面处理树是非空时的情况。
// 先将节点设为红色。
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."); // 找不到对应键。抛出异常。
}

如果函数没有抛出异常,说明已经找到了要删除的节点。我们采用替代法,将要删除的目标移动到叶子部位。
特别注意,移动过程中,我们需要分多种情况分别处理。

  1. 该节点有一个孩子还是两个孩子?
  2. 该节点的替代者,是不是该节点的孩子?

对于第一个问题,如果节点有两个孩子,或者只有右孩子,寻找后继节点替代。否则,寻找前驱节点替代。
对于第二个问题,如果替代节点的父亲不是原节点,则删除时涉及替代节点的父亲及原节点的所有孩子的变动。但是,当替代节点正是原节点的亲孩子时,直接交换指针可能会导致出现自环。

如图所示(黄色表示原节点,红色表示替代节点):


在这里插入图片描述


在这里插入图片描述


此外,在修改其他节点信息的时候,对于可以为空的节点,必须先做非空检查。

// 只要有至少一个孩子,就要继续寻找替代节点。
while (currentNode->leftChild != nullptr || currentNode->rightChild != nullptr) {
    if (currentNode->rightChild != nullptr) {
        // 当前节点有右孩子。
        // 使用后继节点代替原删除节点。
        // 注意:后继节点的左孩子一定是 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;
        }

        // 编辑 currentNode 左孩子的信息。
        if (currentNodeInfo.leftChild != nullptr) {
            currentNodeInfo.leftChild->father = replacementNode;
        }

        // 编辑 replacementNode 右孩子的信息(如果有)。
        if (replacementNodeInfo.rightChild != nullptr) {
            replacementNodeInfo.rightChild->father = currentNode;
        }
        
        currentNode->leftChild = nullptr; // 替换节点的左孩子一定是 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;
        }
    } // if (当前节点有右孩子)
    else { // 当前节点有左孩子,但没有右孩子。
        // 使用前驱节点代替原删除节点。
        // 注意:前驱节点的右孩子一定是 nullptr.
        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;
        }

        // 编辑 currentNode 右孩子的信息。
        if (currentNodeInfo.rightChild != nullptr) {
            currentNodeInfo.rightChild->father = replacementNode;
        }

        // 编辑 replacementNode 左孩子的信息(如果有)。
        if (replacementNodeInfo.leftChild != nullptr) {
            replacementNodeInfo.leftChild->father = currentNode;
        }

        currentNode->rightChild = nullptr; // 替换节点的右孩子一定是 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;
        }
    } // 当前节点有左孩子,但没有右孩子。
} // while (currentNode->leftChild != nullptr || currentNode->rightChild != nullptr)

// 至此,删除目标没有子树。

如果删除目标是根,我们需要在删除节点的同时,将红黑树对象的根设为空。
如果删除目标是红色节点,则在删除并取消其父节点对它的绑定后,不用做其他操作。
如果删除目标是黑色节点,需要做分类处理。

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 { // sibling 没有孩子。
        ...
    }

} // currentFather->color == NodeColor::RED
else { // currentFather->color == NodeColor::BLACK
    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)
{
    /*
        X: 被删除; R: Red; B: Black
          R
         / \
        X   B
           / \
          R   R   或其对称形态。
    */
    // 兄弟的左右孩子都是红色。
    // 注意:如果这个黑色的叔叔有孩子,那么孩子一定是红色的。
    // 操作:对兄弟做旋转,再对父节点做旋转。父节点设为黑色,兄弟设为红色。
    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);
    }
}

父节点是黑色,兄弟是黑色,但是兄弟没有孩子时:

// 兄弟是黑色,但是没有孩子。
/*
      B          B
     / \   ->   / \
    X   B      X   R
*/
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 是根节点。
        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;
            }
        } // if (currentFather == currentGrandpa->leftChild)
        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;
            }
        } // if (currentFather != currentGrandpa->leftChild)

        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 {...}
        } // 兄弟是黑色的。
    } // 父节点为黑色。
}

我们选其中一种情况看看:当父节点为黑色、兄弟也是黑色、兄弟的孩子都是黑色时:

/*
      B
     / \
    X   B
       / \
      B   B

    及其对称形态。
*/
sibling->color = NodeColor::RED;
currentNode = currentFather;

单元测试

我们编写简单的单元测试程序来检测红黑树类的运行是否正常。

/**
 * Red Black Tree Module Test
 * by Flower Black
 * 2022.1
 * at Yushan County, Shangrao, Jiangxi
 */

#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); // Junit 风格的 assertFalse.
				}
				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()}, // 0
				{"家的", rand()}, // 1
				{"公子", rand()}, // 2
				{"最最最", rand()}, // 3
				{"帅的", rand()}, // 4
				{"曹安", rand()}, // 5
				{"公路", rand()}, // 6
				{"4800号", rand()}, // 7
				{"四平路1239号", rand()}, // 8
				{"zbhyyds", rand()}, // 9
				{"zyfdltql", rand()}, // 10
				{"larrytj", rand()}, // 11
				{"黄渡", rand()}, // 12
				{"安亭", rand()}, // 13
				{"花桥", rand()}, // 14
				{"昆山", rand()}, // 15
			};

			const int testArraySize = sizeof(testArray) / sizeof(TestObj) / 2 * 2;
			// 上方 (X * 2 / 2) 的作用:保证 testArraySize 是偶数。

			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

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值