C++--红黑树

红黑树

  • 红黑树
    • [1. 红黑树的介绍](#1. 红黑树的介绍)
      • [1.1 红黑树的概念](#1.1 红黑树的概念)
      • [1.2 红黑树的规则](#1.2 红黑树的规则)
      • [1.3 红黑树的效率](#1.3 红黑树的效率)
    • [2. 红黑树的实现](#2. 红黑树的实现)
      • [2.1 红黑树的结构](#2.1 红黑树的结构)
      • [2.2 红黑树的插入](#2.2 红黑树的插入)
      • [2.3 红黑树的调整](#2.3 红黑树的调整)
        • [2.3.1 叔叔存在且为红(变色)](#2.3.1 叔叔存在且为红(变色))
        • [2.3.2 叔叔不存在或存在且为黑(单旋/双旋 + 变色)](#2.3.2 叔叔不存在或存在且为黑(单旋/双旋 + 变色))
        • [2.3.3 完整代码](#2.3.3 完整代码)
        • [2.3.4 总结](#2.3.4 总结)
    • [3. 红黑树的删除](#3. 红黑树的删除)
    • [4. 红黑树的验证](#4. 红黑树的验证)
    • [5. 红黑树与 AVL 树的比较](#5. 红黑树与 AVL 树的比较)

红黑树

1. 红黑树的介绍

1.1 红黑树的概念

红黑树是一种平衡二叉搜索树,但和 AVL 树使用高度来控制平衡不同,红黑树在每个结点上增加一个存储位来表示结点的颜色,可以是Red或Black,然后通过对任何一条从根到叶子的路径上各个结点着色方式的限制来达到接近平衡。

红黑树通过对每个节点颜色的限制,从而使得整棵树的最长路径不超过最短路径的两倍 (注意是整棵树,对子树没有要求),这样红黑树就可以达到接近平衡。

注意:因为红黑树只要求整棵树中最长路径是最短路径的两倍,所以红黑树是近似平衡的,即子树中某一路径可能比另一条路径长两倍不止;但 AVL 是通过高度控制平衡的,且 AVL 能达到的平衡已经是二叉树中能够达到的最好平衡了,所以 AVL 树是绝对平衡的。

1.2 红黑树的规则

  1. 每个结点不是红色就是黑色。
  2. 根节点是黑色的。
  3. 如果一个节点是红色的,则它的两个孩子节点是黑色的 (不允许出现连续的红色节点)。
  4. 对于每个结点,从该结点到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点(每条路径都包含相同数量的黑色节点)。
  5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)。

思考:为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍?

  • 由规则4可知,从根到 NULL 结点的每条路径都有相同数量的黑色结点,所以极端场景下,最短路径就是全是黑色结点的路径,假设最短路径长度为 bh(black height)。

  • 由规则2和规则3可知,任意一条路径不会有连续的红色结点,所以极端场景下,最长的路径就是一黑一红间隔组成,那么最长路径的长度为 2 * bh。

  • 综合红黑树的4点规则而言,理论上的全黑最短路径和一黑一红的最长路径并不是在每棵红黑树都存在的。假设任意一条从根到 NULL 结点路径的长度为 x,那么最后红黑树的高度就满足 bh <= h <= 2 * bh。

1.3 红黑树的效率

假设 N 是红黑树中结点数量,h 是最短路径的长度,那么红黑树中的结点数量 N 满足 2 h − 1 ≤ N < 2 2 h − 1 2^h - 1 \leq N < 2^{2h} - 1 2h−1≤N<22h−1 ,由此推出红黑树高度 h 满足 h ≈ log ⁡ N h \approx \log N h≈logN,也就是意味着红黑树增删查改最坏也就是走最长路径为 2 ⋅ log ⁡ N 2 \cdot \log N 2⋅logN,那么时间复杂度还是 O ( log ⁡ N ) O(\log N) O(logN)。

红黑树的表达相对 AVL 树要抽象一些,AVL 树通过高度差直观地控制了平衡。红黑树通过 4 条规则的颜色约束,间接的实现了近似平衡。它们效率都是同一档次,但是相对而言,插入相同数量的结点,红黑树的旋转次数是更少的,因为它对平衡的控制没那么严格。

2. 红黑树的实现

2.1 红黑树的结构

cpp 复制代码
// 枚举值表示颜色
enum Colour

{
	RED,
	BLACK
};

// 这⾥默认按key/value结构实现 
template<class K, class V>
struct RBTreeNode
{
	// 这里更新控制平衡也要加⼊parent指针 
	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)
	{}
};

template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
	//...
    
private:
	Node* _root = nullptr;
};

2.2 红黑树的插入

红黑树插入的前面部分很简单,就是一般二叉搜索树的插入逻辑,需要注意的是如果插入的节点是根节点,则我们需要将该节点的颜色改为红色,因为新插入节点默认是被初始化为黑色的;

红黑树插入的关键点在于检测插入后红黑树的性质是否被破坏,其实可以分为两种情况:

  1. 父节点的颜色为黑色,此时没有违反红黑树任何性质,不需要调整;
  2. 父节点的颜色为红色,此时出现了连续红色节点,需要进行调整。
cpp 复制代码
bool Insert(const std::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 (kv.first < cur->_kv.first) 
        {
            parent = cur;
            cur = cur->_left;
        }
        else if (kv.first > cur->_kv.first) 
        {
            parent = cur;
            cur = cur->_right;
        }
        else 
        {
            return false;  //不允许重复节点
        }
    }

    //走到空开始插入,注意这里还需要修改父节点的指向
    //新增节点的颜色为默认被初始化为红色,所以这里不需要再显式赋值
    cur = new Node(kv);
	cur->_col = RED;

    if (kv.first < parent->_kv.first)
        parent->_left = cur;
    else
        parent->_right = newNode;
    cur->_parent = parent;  //修改父节点指向

    //如果父节点颜色为红色,则需要进行调整
    while(parent && parent->_col == RED)
    {
        Node* grandfather = parent->_parent;
    	//红黑树的调整
        //...
    }
}

而红黑树的调整又分为两种情况:(约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点

  1. 如果父节点是红色,此时我们只需要进行变色;
  2. 如果父节点是黑色或者父节点是白色,此时我们需要进行旋转+变色。

下面来看具体探讨这两种情况。

2.3 红黑树的调整

2.3.1 叔叔存在且为红(变色)

**操作:**如果c为红,p为红,g为黑,u存在且为红,则将p和u变黑,g变红。在把g当新的c,继续往上更新。

**分析:**因为p和u都是红色,g是黑色,把p和u变黑,左边子树路径各增加一个黑色节点,g再变红,相当于保持g所在树的黑色节点的数量不变,同时解决了c和p连续红色节点的问题,需要继续往上更新。

**补充:**情况1只变色,不旋转。所以无论c是p的左还是右,p是g的左还是右,都是上面的变色处理方式。

图1:具体情况

跟AVL树类似,图1展示了一种具体情况,但是实际上需要处理这样情况的有很多种情况。

  • 图2将以上类似的处理进行了抽象表达,d/e/f代表每条路径拥有hb个黑色节点的子树,a/b代表每条路径经过hb-1个黑色节点的根为红的子树,hb>=0。

  • 图3/图4/图5,分别展示了 h b = 0 / h b = 1 / h b = 2 hb=0/hb=1/hb=2 hb=0/hb=1/hb=2的具体情况组合分析,当hb等于2时,这里组合情况占百亿种,这些例子是帮助我们理解,不论情况多少种,多少复杂,处理方式一样的,变色再继续往上处理即可,所以只需要看抽象图即可。

图2:抽象情况

图3:hb = 0

图3:h = 1

图4:h = 2

代码实现:

cpp 复制代码
//	g
// p u
//父节点在组父节点左边
if (parent == grandfather->_left)
{
	Node* uncle = grandfather->_right;
	if (uncle && uncle->_col == RED)
	{
		// u存在且为红 ->变⾊再继续往上处理 
		parent->_col = uncle->_col = BLACK;
		grandfather->_col = RED;
		cur = grandfather;
		parent = cur->_parent;
	}
	else
	{
		//第二种调整情况
		//...
	}
}

//	g
// u p
//父节点在组父节点右边
else
{
	Node* uncle = grandfather->_left;
	// u存在且为红 ->变⾊再继续往上处理 
	if (uncle && uncle->_col == RED)
	{
		parent->_col = uncle->_col = BLACK;
		grandfather->_col = RED;
		// 继续往上处理 
		cur = grandfather;
		parent = cur->_parent;
	}
	else
	{
		//第二种调整情况
		//...
	}
}
2.3.2 叔叔不存在或存在且为黑(单旋/双旋 + 变色)

单旋 + 变色:

**分析:**c为红,p为红,g为黑,u不存在或存在且为黑,若u不存在,则c一定是新增结点,u存在且为黑此时g的左右子树的黑色节点个数并不平衡,则c一定不是新增,c一定有子树用来保持g的左右子树的黑色节点个数的平衡,c之前是黑色,且是c的子树中插入,符合情况1,变色将c从黑色变成红色,更新上来的。

**操作:**p必须变黑,才能解决连续红色节点的问题,u不存在或者是黑色的,这里单纯的变色无法解决问题,需要旋转+变色。

​ g

​ p u

c

如果p是g的左,c是p的左,那么以g为根转成右单旋,再把p变黑,g变红即可。p变成新树的新根,这样子树黑色节点的数量不变,且有连续的红色节点了,且不需要往上更新,因为p的父亲是黑色还是红色或者空都不违反规则。
​ g

​ u p

​ c

如果p是g的右,c是p的右,那么以g为根转成左单旋,再把p变黑,g变红即可。p变成新树的新根,这样子树黑色节点的数量不变,且有连续的红色节点了,且不需要往上更新,因为p的父亲是黑色还是红色或者空都不违反规则。

双旋 + 变色:

**分析:**c为红,p为红,g为黑,u不存在或存在且为黑,若u不存在,则c一定是新增结点,u存在且为黑此时g的左右子树的黑色节点个数并不平衡,则c一定不是新增,c一定有子树用来保持g的左右子树的黑色节点个数的平衡,c之前是黑色,且是c的子树中插入,符合情况1,变色将c从黑色变成红色,更新上来的。

**操作:**p必须变黑,才能解决连续红色节点的问题,u不存在或者是黑色的,这里单纯的变色解决问题,需要旋转+变色。

​ g

​ p u

​ c

如果p是g的左,c是p的右,那么先以p为旋转点进行左单旋,再以g为旋转点进行右单旋,再把c变黑,g变红即可。c变成新树的根,这样子树黑色节点的数量不变,没有连续的红色节点了,且不需要往上更新,因为c的父亲是黑色还不是红色或者空都会不违规则。
​ g

​ u p

​ c

如果p是g的右,c是p的左,那么先以p为旋转点进行右单旋,再以g为旋转点进行左单旋,再把c变黑,g变红即可。c变成新树的根,这样子树黑色节点的数量不变,没有连续的红色节点了,且不需要往上更新,因为c的父亲是黑色还是红色或者空都不违规则。


代码实现:

cpp 复制代码
// 父节点在组父节点左边
// u存在且为黑或不存在 -> 旋转+变⾊ 
if (cur == parent->_left)
{
	//  g
	// p u
	//c
	//单旋 
	RotateR(grandfather);

	parent->_col = BLACK;
	grandfather->_col = RED;
}
else
{
	//  g
	// p u
	//  c
	//双旋 
	RotateL(parent);
	RotateR(grandfather);

	cur->_col = BLACK;
	grandfather->_col = RED;
}

// 父节点在组父节点右边
// u存在且为黑或不存在 -> 旋转+变⾊ 
if (cur == parent->_right)
{
    //  g
	// u p
	//    c
	// 单旋
	RotateL(grandfather);

	parent->_col = BLACK;
	grandfather->_col = RED;
}
else
{
	//  g
	// u p
	//  c
	// 双旋
	RotateR(parent);
	RotateL(grandfather);

	cur->_col = BLACK;
	grandfather->_col = RED;
}
2.3.3 完整代码
cpp 复制代码
bool Insert(const std::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 (kv.first < cur->_kv.first) 
        {
            parent = cur;
            cur = cur->_left;
        }
        else if (kv.first > cur->_kv.first) 
        {
            parent = cur;
            cur = cur->_right;
        }
        else 
        {
            return false;  // 不允许重复节点
        }
    }

    // 走到空开始插入,注意这里还需要修改父节点的指向
    // 新增节点的颜色为默认被初始化为红色,所以这里不需要再显式赋值
    cur = new Node(kv);
	cur->_col = RED;

    if (kv.first < parent->_kv.first)
        parent->_left = cur;
    else
        parent->_right = newNode;
    cur->_parent = parent;  // 修改父节点指向

    // 如果父节点颜色为红色,则需要进行调整
    while(parent && parent->_col == RED)
    {
        Node* grandfather = parent->_parent;
    	// 红黑树的调整
		//	g
		// p u
		// 父节点在组父节点左边
		if (parent == grandfather->_left)
		{
			Node* uncle = grandfather->_right;
            // 第一种调整情况:叔叔存在且为红
			if (uncle && uncle->_col == RED)
			{
				// u存在且为红 ->变⾊再继续往上处理 
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;
				cur = grandfather;
				parent = cur->_parent;
			}
            // 第二种调整情况:叔叔不存在或叔叔存在且为黑
            else
			{
				// u存在且为黑或不存在 -> 旋转+变⾊ 
				if (cur == parent->_left)
				{
					//   g
					//  p u
					// c
					// 单旋 
					RotateR(grandfather);

					parent->_col = BLACK;
					grandfather->_col = RED;
				}
				else
				{
					//  g
					// p u
					//  c
					// 双旋 
					RotateL(parent);
					RotateR(grandfather);

					cur->_col = BLACK;
					grandfather->_col = RED;
				}
                
                break;
			}
		}
        //	g
		// u p
		// 父节点在组父节点右边
		else
		{
			Node* uncle = grandfather->_left;
			// 第一种调整情况:叔叔存在且为红
			if (uncle && uncle->_col == RED)
			{
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;
                
				// 继续往上处理 
				cur = grandfather;
				parent = cur->_parent;
			}
            // 第二种调整情况:叔叔不存在或叔叔存在且为黑
			else
			{
				// u存在且为黑或不存在 -> 旋转+变⾊ 
				if (cur == parent->_right)
				{
                    //  g
					// u p
					//    c
					// 单旋
					RotateL(grandfather);

					parent->_col = BLACK;
					grandfather->_col = RED;
				}
				else
				{
					//  g
					// u p
					//  c
					// 双旋
					RotateR(parent);
					RotateL(grandfather);

					cur->_col = BLACK;
					grandfather->_col = RED;
				}
                break;
			}
		}
    }
    
    _root->_col = BLACK;
    
 	return true;
}
2.3.4 总结

红黑树节点的调整其实很简单,共计有两种情况:(注意新增节点一定要初始化为红色)

  1. 父节点的颜色为黑色,此时没有违反红黑树的任何性质,不需要调整。
  2. 父节点的颜色为红色,此时出现了连续红色节点,需要进行调整;调整又分为两种情况:
    1. 若叔叔节点存在且为红色,则将父节点和叔叔节点变黑,祖父节点变红,再继续向上调整。
    2. 叔叔节点不存在或叔叔节点为黑色,则需要进行旋转+变色。旋转根据父节点和子节点的具体位置不同进行:
      • 左单旋、右单旋、左右双旋和右左双旋。
      • 变色统一将旋转后的子树的根节点置黑、根节点的左右节点置红即可
      • 调整完成后不需要继续往上调整。

简而言之,红黑树如何调整取决于叔叔节点。

3. 红黑树的删除

和 AVL 树一样,红黑树的删除作为了解内容即可,如果对其特别感兴趣的同学可以参考一下《算法导论》或者《STL源码剖析》,也可以看看下面这位大佬的博客:红黑树 - Never - 博客园 (cnblogs.com)

4. 红黑树的验证

红黑树的验证和 AVL 树一样,验证一共分为两部分:

  1. 验证是否为二叉搜索树;
  2. 验证其是否满足红黑树的性质;

验证二叉搜索树很简单,向树中插入数据,然后实现一个 InOrder 函数看遍历得到的数据是否有序即可。

cpp 复制代码
//中序遍历
void InOrder() 
{
    return _inOrder(_root);
}

void _inOrder(Node* root) 
{
    if (root == nullptr)
        return;

    _inOrder(root->_left);
    std::cout << root->_kv.first >> root->_kv.second >> std::endl;
    _inOrder(root->_right);
}

验证平衡则比较麻烦,因为需要分别验证红黑树的几个性质:根节点为黑色,不允许出现连续的红色,每条路径黑色节点的数量是红色节点数量的两倍。同时,在验证的过程中可以顺便将不符合要求的地方的节点的key值打印出来,方便发生错误时进行调试。

cpp 复制代码
bool Check(Node* root, int blackCount, int baseCount) 
{
    //如果走到空,说明这条路径结束了,此时看count与baseCount是否相等
    if (root == nullptr) 
    {
        if (blackCount != baseCount) 
        {
            std::cout << "此路径黑色节点数量有误,节点key值:" << root->_kv.first << std::endl;
            return false;
        }
        return true;
    }

    //检查是否出现连续红色节点
    if (root->_col == RED) 
    {
        if (root->_parent->_col == RED) 
        {  //红色节点一定有父亲,因为根节点为黑色
            std::cout << "出现连续红色节点,节点key值:" << root->_kv.first << std::endl;
        }
    }

    if (root->_col == BLACK)
        blackCount++;

    return Check(root->_left, blackCount, baseCount) && Check(root->_right, blackCount, baseCount);
}

//验证红黑树的性质
bool IsBanlance() 
{
    if (_root == nullptr)
        return true;

    //检查根节点
    if (_root->_col == RED) 
    {
        std::cout << "根节点为红" << std::endl;
        return false;
    }

    //以最左路径黑色节点数量为基准,同其他路径的黑色节点数量相比较,看是否相等
    int baseCount = 0;
    Node* cur = _root;
    while (cur) 
    {
        if (cur->_col == BLACK)
            baseCount++;
        cur = cur->_left;
    }

    //检查红黑树的其他性质
    return Check(_root, 0, baseCount);
}

5. 红黑树与 AVL 树的比较

红黑树和 AVL 树都是高效的平衡二叉树,增删改查的时间复杂度都是 O(logN),但由于红黑树不追求绝对平衡,只需要保证最长路径不超过最短路径的2倍,所以在某些极端情况下红黑树的查询效率相较于 AVL 树要低一点点;例如当树左子树全部为黑色节点,右子树全部为一黑一红交替时,红黑树的高度差不多是 AVL 树的两倍,但是此时红黑树的查询效率仍然属于 O(logN) 这个量级;

不过也正是由于红黑树不要求左右子树绝对平衡,所以红黑树相较于 AVL 树在插入和删除时的旋转次数要少一些,所以在经常进行增删的结构中红黑树的性能比 AVL 树更优,而且红黑树实现比较简单,所以在实际业务中一般都使用红黑树,而不使用 AVL 树

相关推荐
cdg==吃蛋糕8 分钟前
selenium 使用方法
开发语言·python
爱掉发的小李1 小时前
前端开发中的输出问题
开发语言·前端·javascript
zyx没烦恼1 小时前
五种IO模型
开发语言·c++
Q_Q5110082852 小时前
python的婚纱影楼管理系统
开发语言·spring boot·python·django·flask·node.js·php
EutoCool2 小时前
Qt窗口:菜单栏
开发语言·c++·嵌入式硬件·qt·前端框架
nightunderblackcat2 小时前
新手向:使用Python将多种图像格式统一转换为JPG
开发语言·python
我爱Jack3 小时前
深入解析 LinkedList
java·开发语言
engchina3 小时前
Python PDF处理库深度对比:PyMuPDF、pypdfium2、pdfplumber、pdfminer的关系与区别
开发语言·python·pdf
拓端研究室3 小时前
专题:2025供应链数智化与效率提升报告|附100+份报告PDF、原数据表汇总下载
开发语言·php
一百天成为python专家3 小时前
python库之jieba 库
开发语言·人工智能·python·深度学习·机器学习·pycharm·python3.11