C++——红黑树

红黑树

注:在学习红黑树之前,建议先对AVL树具备一定的了解


1. 红黑树的性质

和AVL树一样,红黑树也是一棵自平衡的搜索二叉树

如上图,就是一棵经典的红黑树,以下是他的性质:

  1. 红黑树同样是一棵搜索二叉树,其满足搜索二叉树的所有性质:
  • 每个节点的左右子树都是一棵搜索二叉树

  • 左孩子的键值比父亲的小;右孩子的键值比父亲的大

  1. 红黑树的每个节点的左右子树也都是红黑树

  2. 红黑树的每个节点都有一个颜色,且要么是红色要么是黑色空节点被视作黑色节点

  3. 红黑树的根节点的颜色必须是黑色

  4. 红黑树每条路径的黑色节点个数必须相等

  5. 红黑树的红色节点不能连续出现

性质5和性质6,我么可以推导出红黑树的平衡特性:

红黑树的最长路径的长度不会超过最短路径的两倍

2. 了解操作

2.1 红黑树节点类的定义:

cpp 复制代码
//定义一个枚举类,用于表示颜色
enum Color
{
	RED,
	BLACK
};

//默认该红黑树的存储模型为key_value型
template<class K, class V>
struct RBTreeNode
{
	typedef RBTreeNode<K, V> Node;

	Node* _parent = nullptr;
	Node* _left = nullptr;
	Node* _right = nullptr;
	pair<K, V> _kv;
	Color _color = RED;	//节点默默的颜色为红色

	RBTreeNode(const pair<K, V> kv)
		: _kv(kv)
	{}
};

2.2 插入节点

2.1.1 找到插入位置

既然红黑树也是一棵搜索二叉树,那么在插入新节点之前,首先肯定也要找到插入位置:

cpp 复制代码
bool insert(const pair<K, V> kv)
{
    //如果原来是一棵空树
    if (_root == nullptr)
    {
        _root = new Node(kv);
        _root->_color = BLACK;
        return true;
    }

    //找到插入位置
    Node* cur = _root;
    Node* parent = nullptr;
    while (cur)
    {
        parent = cur;

        if (cur->_kv.first > kv.first)
            cur = cur->_left;
        else if (cur->_kv.first < kv.first)
            cur = cur->_right;
        else
            return false;
    }

    cur = new Node(kv);
    cur->_parent = parent;
    if (cur->_kv.first > parent->_kv.first)
        parent->_right = cur;
    else
        parent->_left = cur;

    //........................
}

2.1.2 新节点颜色

红黑树的每个节点的颜色要么是红色,要么是黑色,那么我们新插入的节点应该是什么颜色?红色,还是黑色,还是红色黑色都可能呢?

我么可以进行假设:

假设新节点的颜色是黑色,如下图:

可以看到,如论黑色节点插入到哪个位置,红黑树的条件都会被打破

  • 这是因为:插入之前就是一棵红黑树,那么每条路径的黑色节点个数就一定相同
  • 此时在任意位置插入一个黑色节点,就肯定会使一条路径的黑色节点个数多余其他路径,从而破坏红黑树的条件

可见,如果新节点的颜色是黑色,我们是很难在插入后将其还原到满足红黑树的条件的

假设新节点的颜色是红色,如下图:

从图中可以看到:

  • 如果父节点的颜色是黑色,那么插入一个红色节点并不会破坏红黑树的条件
  • 如果父节点的颜色是红色,那么就会出现"出现连续红色节点"的情况,需要做出调整

总结:

  • 虽然插入黑色节点和红色节点都可能会导致红黑树的条件被破坏,但是相较于插入黑色节点必定会破坏红黑树的条件,插入红色节点只会在其父节点也是红色时才会破坏红黑树的条件
  • 因此,插入红色节点代价更小 ,即新节点的颜色默认为红色

接下来,我们就来分析父节点为红色或黑色时的不同插入情况

注意:

红黑树的形态无穷无尽,博主不可能将每种红黑树的插入情况都列举出来

但是,这些所有的情况都可以被分为父节点为黑色和父节点为红色这两类

博主在后面的分析会以一棵具体的红黑树来做示例,讲述节点的插入和后续颜色的调整。至于其他形态的红黑树,只要插入时的情况被归类为某一特定的情况,都可以用相同的方法处理,大家学会处理方法即可

2.1.3 父节点为黑色

插入新节点之前该树就是一棵红黑树,如果新节点的父节点的颜色是黑色,那么插入一个红色节点并不会影响到红黑树的条件

  • 红色节点没有连续出现
  • 每条路径的黑色节点个数仍然相等

因此,如果新节点的父节点为黑色,插入流程结束

2.1.4 父节点为红色

如果父节点的颜色为红色,那么就要分叔叔节点为红色 以及叔叔节点不存在/叔叔节点为黑色这两种情况来讨论

注意:

以下涉及关于旋转的操作均已在AVL树中提及,下面只会讲述大概的步骤,并提供代码,不会做详细讲解

2.1.4.1 叔叔节点为红色

此时,为了使红色节点不连续出现,并且保持每条路径的黑色节点个数相等,我们需要对部分节点的颜色进行调整:

爷爷节点的颜色改为红色

叔叔节点和父节点的颜色改为黑色

调节过后,如果爷爷节点的父亲仍为红色,则要继续向上调节

需要注意,上图中的爷爷节点就是整棵树的根节点,而规定红黑树的根节点是黑色 的,因此调整完过后需要注意将根节点改为黑色

2.1.4.2 叔叔节点不存在/叔叔节点为黑色

在向上调整的过程中,我们可能会遇到叔叔节点不存在或者叔叔节点为黑色的情况:

此时,我们不能像叔叔节点为红色的情况一样,简单的将父节点变黑,爷爷节点变红来解决问题:

针对这种情况,我们需要根据parent节点相对grandparent的位置以及cur节点相对parent节点的位置,通过旋转来解决问题:

2.1.4.2.1 父节点parent为爷爷节点grandparent的左

情况一:curparent的左

如图:

此时,我们需要对grandparnet节点进行右单旋操作:

  • parent的右孩子链接给grandparent的左孩子

  • grandparent链接给parent的右孩子

最后,调节节点的颜色,使其满足红黑树的条件:

  • grandparent的颜色红
  • parnet的颜色变黑

对于叔叔节点不存在的情况,也执行相同的操作:

调整完毕

需要清楚的是

  • 调整后和插入节点前相比,该树每条路径的黑色节点的总个数并没有被改变 ,并且新的根为黑色

  • 如果该树是其他树的子树,尽管插入了一个节点,但是经过旋转调整过后并不会影响到其他节点,因此可以直接结束插入过程

情况二:curparent的右:

如图:

此时,我们需要对grandparent进行左右双旋操作:

  • 首先对parent进行左单旋
  • 再对grandparent进行右单旋

最后,调节节点的颜色,使其满足红黑树的条件:

  • cur的颜色变黑
  • grandparent的颜色变红

对于叔叔节点不存在的情况,也执行相同的操作:

调整完毕

同样,假设这棵树是一棵树的子树,经过旋转调节过后也不会影响到其他点节点,可以直接结束插入过程

2.1.4.2.2 父节点parent为爷爷节点grandparent的右

情况一:curparent的右

如图:

此时,我们需要对grandparnet节点进行左单旋操作:

  • parent的左孩子链接给grandparent的右孩子

  • grandparent链接给parent的左孩子

最后,调节节点的颜色,使其满足红黑树的条件:

  • grandparent的颜色红
  • parnet的颜色变黑

对于叔叔节点不存在的情况,也执行相同的操作:

调整完毕

同样,假设这棵树是一棵树的子树,经过旋转调节过后也不会影响到其他点节点,可以直接结束插入过程

情况二:curparent的左:

如图:

此时,我们需要对grandparent进行右左双旋操作:

  • 首先对parent进行右单旋
  • 再对grandparent进行左单旋

最后,调节节点的颜色,使其满足红黑树的条件:

  • cur的颜色变黑
  • grandparent的颜色变红

对于叔叔节点不存在的情况,也执行相同的操作:

调整完毕

同样,假设这棵树是一棵树的子树,经过旋转调节过后也不会影响到其他点节点,可以直接结束插入过程

2.1.4.3 旋转操作

左单旋:

旋转步骤为:

  • cur的左子树链接给parent的右子树
  • parent链接给cur的左子树

代码:

cpp 复制代码
void rotateL(Node* parent)
{
    Node* pparent = parent->_parent;
    Node* parentR = parent->_right;
    Node* parentRL = parentR->_left;

    parent->_right = parentRL;

    //考虑cur->_left为空的情况,防止空节点的访问
    if (parentRL)
        parentRL->_parent = parent;

    parentR->_left = parent;
    parentR->_parent = pparent;
    parent->_parent = parentR;

    //如果cur就是_root根节点,那么就要更新根节点
    if (pparent == nullptr)
        _root = parentR;
    else	//否则就要将新的根(即cur)链接给上级节点
    {
        if (parent == pparent->_left)
            pparent->_left = parentR;
        else
            pparent->_right = parentR;
    }
}

右单旋:

旋转步骤为:

  • cur的右子树链接给parent的左子树
  • parent链接给cur的右子树

代码:

cpp 复制代码
void rotateR(Node* parent)
{
    Node* pparent = parent->_parent;
    Node* parentL = parent->_left;
    Node* parentLR = parentL->_right;

    parent->_left = parentLR;

    //考虑cur->_right为空的情况
    if (parentLR)
        parentLR->_parent = parent;

    parentL->_right = parent;
    parent->_parent = parentL;
    parentL->_parent = pparent;

	//如果cur就是_root根节点,那么就要更新根节点
    if (pparent == nullptr)
        _root = parentL;
    else	//否则就要将新的根(即cur)链接给上级节点
    {
        if (parent == pparent->_left)
            pparent->_left = parentL;
        else
            pparent->_right = parentL;
    }
}

左右双旋:

旋转步骤为:

  • 先对cur为根的树进行左单旋
  • 再对parent为根的树进行右单旋

代码:

cpp 复制代码
void rotateLR(Node* parent)
{
    Node* parentL = parent->_left;	//即图中的cur节点
    Node* parentLR = parentL->_right;	//即图中的parentLR节点

    rotateL(parentL);	//先对cur为根的树进行左单旋
    rotateR(parent);	//再对parent为根的树进行右单旋
}

右左双旋:

操作步骤为:

  • 先对cur为根的树进行右单旋
  • 再对parent为根的树进行左单旋

代码:

cpp 复制代码
void rotateRL(Node* parent)
{
    Node* parentR = parent->_right;	//即为图中提到的cur节点
    Node* parentRL = parentR->_left;	//即为图中提到的parentRL节点

    rotateR(parentR);	//先对cur为根的树进行右单旋
    rotateL(parent);	//再对parent为根的树进行左单旋
}

2.1.5 插入节点代码

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

    //找到插入位置
    Node* cur = _root;
    Node* parent = nullptr;
    while (cur)
    {
        parent = cur;

        if (cur->_kv.first > kv.first)
            cur = cur->_left;
        else if (cur->_kv.first < kv.first)
            cur = cur->_right;
        else
            return false;
    }

    cur = new Node(kv);
    cur->_parent = parent;
    if (cur->_kv.first > parent->_kv.first)
        parent->_right = cur;
    else
        parent->_left = cur;

    //更新颜色,调节平衡
    while (parent && parent->_color != BLACK)
    {
        Node* grandparent = parent->_parent;
		
        //如果父节点为爷爷节点的左
        if (parent == grandparent->_left)
        {
            Node* uncle = grandparent->_right;
			
            //如果叔叔存在且叔叔节点为红色
            if (uncle && uncle->_color == RED)
            {
                parent->_color = BLACK;
                uncle->_color = BLACK;
                grandparent->_color = RED;
				
                //继续向上调整
                cur = grandparent;
                parent = grandparent->_parent;
            }
            else	//如果叔叔节点不存在或叔叔节点为黑色
            {
                //如果cur为父节点的左,就进行右单旋
                if (cur == parent->_left)
                {
                    rotateR(grandparent);

                    grandparent->_color = RED;
                    parent->_color = BLACK;
                }
                else	//如果cur为父节点的右,就进左右双旋
                {
                    rotateLR(grandparent);
                    cur->_color = BLACK;
                    grandparent->_color = RED;
                }

                break;	//旋转过后必定满足红黑树条件,直接结束插入
            }
        }
        else	//如果父节点为爷爷节点的右
        {
            Node* uncle = grandparent->_left;
			
            //如果叔叔存在且叔叔节点为红色
            if (uncle && uncle->_color == RED)
            {
                parent->_color = BLACK;
                uncle->_color = BLACK;
                grandparent->_color = RED;
				
                //继续向上调整
                cur = grandparent;
                parent = grandparent->_parent;
            }
            else	//如果叔叔节点不存在或叔叔节点为黑色
            {
                //如果cur为父节点的右,就进行左单旋
                if (cur == parent->_right)
                {
                    rotateL(grandparent);

                    grandparent->_color = RED;
                    parent->_color = BLACK;
                }
                else	//如果cur为父节点的左,就进右左双旋
                {
                    rotateRL(grandparent);
                    cur->_color = BLACK;
                    grandparent->_color = RED;
                }

                break;	//旋转过后必定满足红黑树条件,直接结束插入
            }
        }
    }

    _root->_color = BLACK;	//直接将根节点的颜色设置为黑色

    return true;
}

2.3 检查一棵树是否为红黑树

为了验证我们的插入算法是否正确,我们需要写一个isbalance函数来进行检验:

我么可以根据红黑树的两个性质来实现这个函数;

  1. 红色节点不能重复出现

  2. 每条路径的黑色节点个数相同

对于第一个性质:

  • 容易想到的一个方法是:递归遍历这棵树,每遍历到一个节点,就检查该节点是否和它的左孩子或者右孩子同时为红色
  • 但是,上面的方法书写起来较为复杂。我们可以换一种思路:每遍历到一个节点,就检查它和它的父节点是否同时为红色

对于第二个性质:

  • 我们需要求出每条路径的黑色节点的个数
  • 而所谓路径,就是从根节点出发,到达空节点所经过的节点
  • 因此,我们可以利用一个参数来记录遍历到当前节点时黑色节点的总个数
  • 为了方便比较,我么可以预先求出一条路径的黑色节点总个数 作为基准值
  • 之后,当遍历到空节点时,就可以拿记录黑色节点个数的参数与基准值进行比较

实现代码;

cpp 复制代码
bool isBalance()
{
    if (_root && _root->_color != BLACK)
        return false;
	
    //以最左路径的黑色节点纵作为基准值
    int refBlackNum = 0;
    Node* cur = _root;
    while (cur)
    {
        if (cur->_color == BLACK)
            refBlackNum++;

        cur = cur->_left;
    }

    return cheak(_root, 0, refBlackNum);
}
bool cheak(Node* root, int curBlackNum, int refBlackNum)
{
    //如果遍历到空节点就要拿curBlackNum与基准值refBlackNum进行比较
    if (root == nullptr)
    {
        if (curBlackNum == refBlackNum)
            return true;
        else
        {
            cout << root->_kv.first << "改路径黑色节点的数量与最左路径黑色节点数量不同" << endl;
            return false;
        }
    }
	
    //如果当前节点和父节点同时为红色,则不满足红黑树条件
    if (root->_color == RED && root->_parent->_color == RED)
    {
        cout << root->_kv.first << "存在连续的红色节点" << endl;
        return false;
    }
	
    //如果当前节点为黑色,则当前路径的黑色节点个数加1
    if (root->_color == BLACK)
        curBlackNum++;

    return cheak(root->_left, curBlackNum, refBlackNum) && cheak(root->_right, curBlackNum, refBlackNum);
}

2.4 AVL树与红黑树

如今,我们已经学习了两颗自平衡的搜索二叉树:AVL树与红黑树

现在,我们来对二者进行一个简单的对比:

AVL树规定:每个节点的左右子树的高度差不大于1

红黑树的性质决定:最长路径的长度不大于最短路径的两倍

从上面的两个特性我们可以得出两个结论:

  1. AVL树的平衡条件比红黑树更加严格,因此AVL树也比红黑树更加平衡就查找效率来说,AVL树是要比红黑树略高的 。但是,由于二者都是平衡搜索二叉树,查找特定的元素的时间复杂度都是O(logN)
  2. 由于AVL树的平衡条件比红黑树要更加严格,因此维护其平衡也会更加复杂 ,同样是要删除或者插入一个元素,AVL树往往会比红黑树经历更多的旋转操作,这也就导致了在插入和删除元素的效率上,AVL树是不及红黑树的

可以做出总结:

  • 如果需要频繁的查找树中的元素,而插入和删除元素的操作相对较少,可以优先使用AVL树
  • 如果需要频繁的插入或删除元素,而查找元素的次数相对较少,可以优先使用红黑树

事实上,我们后面将学到的关联式容器mapset的底层结构就是红黑树

相关推荐
猷咪7 分钟前
C++基础
开发语言·c++
IT·小灰灰8 分钟前
30行PHP,利用硅基流动API,网页客服瞬间上线
开发语言·人工智能·aigc·php
快点好好学习吧10 分钟前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
秦老师Q10 分钟前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
烟锁池塘柳011 分钟前
解决Google Scholar “We‘re sorry... but your computer or network may be sending automated queries.”的问题
开发语言
是誰萆微了承諾11 分钟前
php 对接deepseek
android·开发语言·php
CSDN_RTKLIB14 分钟前
WideCharToMultiByte与T2A
c++
2601_9498683614 分钟前
Flutter for OpenHarmony 电子合同签署App实战 - 已签合同实现
java·开发语言·flutter
星火开发设计28 分钟前
类型别名 typedef:让复杂类型更简洁
开发语言·c++·学习·算法·函数·知识
蒹葭玉树39 分钟前
【C++上岸】C++常见面试题目--操作系统篇(第二十八期)
linux·c++·面试