C++红黑树的深入解析:从理论到实践

红黑树作为一种自平衡的二叉搜索树,是计算机科学中的经典数据结构之一,广泛应用于各种需要高效查找、插入和删除操作的场景中,比如STL中的 mapset。虽然它的基本原理并不复杂,但在实现过程中,需要处理许多细节,比如颜色的调整、旋转操作等。本文将对红黑树的结构、插入、删除等操作进行详细的剖析,以帮助大家更好地理解和实现这一高效的数据结构。

一、红黑树的基本概念与规则

红黑树是一种满足特定性质的二叉搜索树。与普通的二叉搜索树不同,红黑树的每个结点都附加了一个颜色属性,且通过一些规则保证了树的平衡性。红黑树的四条基本规则如下:

  1. 每个节点的颜色要么是红色,要么是黑色。
  2. 根节点是黑色的。
  3. 如果一个节点是红色的,那么它的两个子节点必须是黑色的。 这就意味着红色节点不能有连续的红色节点。
  4. 从任意一个节点到其所有叶子节点的路径上,必须包含相同数量的黑色节点。 这条规则保证了树的平衡性,防止树的某些路径过长。

红黑树的这些规则通过"颜色约束"来间接实现自平衡,因此每次进行插入、删除操作时,都需要确保这些规则被满足。

以上均为红黑树示例。

二、红黑树的高度与效率

红黑树的高度是它的关键特性之一。红黑树的自平衡机制确保了它的高度不会太大,这对于树的查找、插入和删除操作的效率至关重要。理论上,红黑树的高度 h 与黑色高度 bh(从根到叶子节点的路径上黑色节点的数量)之间存在如下关系:

  • 最短路径(只有黑色节点)长度为 bh。
  • 最长路径(交替的红色和黑色节点)长度为 2 * bh。

因此,红黑树的最大高度为 2 * bh,而最小高度为 bh。因此,红黑树的高度 h 满足 h≤2×bhh \leq 2 \times bhh≤2×bh,并且由于红黑树的黑色高度是相同的,所以可以得出红黑树的高度是 O(log N),其中 N 是树中结点的数量。

由于红黑树的高度被控制在对数级别,它能够保证查找、插入、删除操作在最坏情况下的时间复杂度均为 O(log N)。

三、红黑树的结构

红黑树的基本节点结构包括以下几个部分:

cpp 复制代码
enum Colour {
    RED,
    BLACK
};

template<class K, class V>
struct RBTreeNode {
    pair<K, V> _kv;         // 存储键值对
    RBTreeNode<K, V>* _left;  // 左子树
    RBTreeNode<K, V>* _right; // 右子树
    RBTreeNode<K, V>* _parent;  // 父节点
    Colour _col;  // 颜色属性

    RBTreeNode(const pair<K, V>& kv)
        : _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED) {}
};

每个节点不仅包含键值对 (_kv),还包含指向左子树、右子树和父节点的指针。此外,节点的颜色 (_col) 用于维护红黑树的平衡。

四、红黑树的插入操作

红黑树的插入操作需要遵循以下步骤:

  1. 按照二叉搜索树的规则插入新节点。这是插入操作的第一步,我们首先将新节点插入到树的适当位置。
  2. 将新节点的颜色设置为红色。新插入的节点默认是红色的,这有助于避免违反红黑树的黑色节点数平衡。
  3. 调整颜色和结构 。插入新节点后,可能会破坏红黑树的平衡。具体的修复操作包括:
    • 情况1:变色操作。如果父节点和叔叔节点都是红色,则将父节点和叔叔节点都变为黑色,祖父节点变为红色,并继续往上处理。
    • 情况2:旋转和变色。如果父节点是红色,且叔叔节点是黑色或不存在,则需要进行旋转操作(单旋或双旋),并相应地调整颜色。

我们来看一段插入代码的实现:

cpp 复制代码
bool Insert(const pair<K, V>& kv) {
    if (_root == nullptr) {
        _root = new Node(kv);
        _root->_col = BLACK;
        return true;
    }

    Node* parent = nullptr;
    Node* cur = _root;
    while (cur) {
        if (cur->_kv.first < kv.first) {
            parent = cur;
            cur = cur->_right;
        } else if (cur->_kv.first > kv.first) {
            parent = cur;
            cur = cur->_left;
        } else {
            return false; // 键值重复
        }
    }

    cur = new Node(kv);
    cur->_col = RED;
    if (parent->_kv.first < kv.first) {
        parent->_right = cur;
    } else {
        parent->_left = cur;
    }
    cur->_parent = parent;

    while (parent && parent->_col == RED) {
        Node* grandfather = parent->_parent;
        if (parent == grandfather->_left) {
            Node* uncle = grandfather->_right;
            if (uncle && uncle->_col == RED) {
                parent->_col = uncle->_col = BLACK;
                grandfather->_col = RED;
                cur = grandfather;
                parent = cur->_parent;
            } else {
                if (cur == parent->_left) {
                    RotateR(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                } else {
                    RotateL(parent);
                    RotateR(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                break;
            }
        } else {
            Node* uncle = grandfather->_left;
            if (uncle && uncle->_col == RED) {
                parent->_col = uncle->_col = BLACK;
                grandfather->_col = RED;
                cur = grandfather;
                parent = cur->_parent;
            } else {
                if (cur == parent->_right) {
                    RotateL(grandfather);
                    parent->_col = BLACK;
                    grandfather->_col = RED;
                } else {
                    RotateR(parent);
                    RotateL(grandfather);
                    cur->_col = BLACK;
                    grandfather->_col = RED;
                }
                break;
            }
        }
    }

    _root->_col = BLACK;
    return true;
}

这段代码展示了红黑树的插入操作,包括节点插入、颜色变更和旋转操作。关键的操作是通过循环和条件判断来确保红黑树的规则不被破坏。

五、红黑树的查找操作

红黑树的查找操作和普通的二叉搜索树是类似的,我们依然按照二叉搜索树的规则进行查找。由于红黑树的平衡性,查找操作的时间复杂度为 O(log N)。

cpp 复制代码
Node* Find(const K& key) {
    Node* cur = _root;
    while (cur) {
        if (cur->_kv.first < key) {
            cur = cur->_right;
        } else if (cur->_kv.first > key) {
            cur = cur->_left;
        } else {
            return cur;
        }
    }
    return nullptr;
}

上述代码展示了查找操作的实现,我们根据节点的键值逐步向左或向右子树移动,直到找到目标节点或树为空。

六、红黑树的删除操作

删除操作在红黑树中比插入操作更为复杂。删除一个节点后,可能会破坏红黑树的平衡,特别是当删除的节点是黑色时,需要特别注意。因此,删除操作需要执行旋转和变色操作来恢复平衡。尽管删除操作复杂,但其时间复杂度同样是 O(log N)。

由于删除操作较为复杂,本文不深入讨论其实现。如果有兴趣,可以参考《算法导论》或《STL源码剖析》中的相关章节。

七、红黑树的验证

为了验证红黑树的正确性,我们可以检查它是否满足四条基本规则。可以通过前序遍历树的每一条路径,检查节点颜色和黑色节点数量是否符合要求。

cpp 复制代码
bool Check(Node* root, int blackNum, const int refNum) {
    if (root == nullptr) {
        if (refNum != blackNum) {
            cout << "存在黑色结点数量不相等的路径" << endl;
            return false;
        }
        return true;
    }
    if (root->_col == RED && root->_parent->_col == RED) {
        cout << root->_kv.first << "存在连续的红色结点" << endl;
        return false;
    }
    if (root->_col == BLACK) {
        blackNum++;
    }
    return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum);
}

通过这个检查函数,我们能够验证树的结构是否符合红黑树的规则。

八、总结

红黑树是一种自平衡的二叉搜索树,通过简单的规则保证树的平衡性,确保了查找、插入和删除操作的时间复杂度始终为 O(log N)。虽然红黑树的插入、删除操作相对复杂,但它的高效性和稳定性使其成为许多应用中不可或缺的数据结构。希望通过本文的详细解析,大家能够对红黑树有更深入的了解。

!!预告,下一期详细讲红黑树的变换过程

相关推荐
圣保罗的大教堂2 小时前
《算法笔记》9.3小节——数据结构专题(2)->树的遍历 问题 A: 树查找
算法
eamon1003 小时前
麒麟V10 arm cpu aarch64 下编译 RocketMQ-Client-CPP 2.2.0
c++·rocketmq·java-rocketmq
laimaxgg3 小时前
Qt窗口控件之颜色对话框QColorDialog
开发语言·前端·c++·qt·命令模式·qt6.3
乌云暮年4 小时前
算法刷题整理合集(四)
java·开发语言·算法·dfs·bfs
梁山1号4 小时前
【QT】】qcustomplot的初步使用二
c++·单片机·qt
bryant_meng4 小时前
【C++】Virtual function and Polymorphism
c++·多态·抽象类·虚函数·纯虚函数
Luo_LA5 小时前
【排序算法对比】快速排序、归并排序、堆排序
java·算法·排序算法
AI技术控5 小时前
机器学习算法实战——天气数据分析(主页有源码)
算法·机器学习·数据分析
oioihoii5 小时前
C++20 中的同步输出流:`std::basic_osyncstream` 深入解析与应用实践
c++·算法·c++20
猪猪成5 小时前
【图论】FLOYD弗洛伊德算法-最短路径
学习·算法·图论