[C++进阶] 21. 红黑树

一. 红黑树的概念

1)AVL树是二叉搜索树能达到的最佳平衡(左右子树高度差绝对值小于等于1),但也正是因为严格保持平衡,需要更多的旋转来实现,产生更多消耗。

红黑树则是一种近似平衡 的方案:红黑树是一棵二叉搜索树,它的每个节点增加一个变量来存储该节点的颜色(只能是红色或者黑色)。通过对每条路径上节点颜色的约束,红黑树可以确保没有一条路径会比其他路径(高度)长出二倍,因此是近似平衡的,既保证搜索效率不会差太多,也可以减少旋转消耗。

2)红黑树的规则

  1. 每个节点必须是红色或者黑色。

  2. 根节点必须是黑色。

  3. 任何一条路径上不可以有连续的红色节点(但可以有连续的黑)。

  4. 从任何一个节点开始,到它所有能到NULL的简单路径上,均包含相同数量的黑色节点。

注意数路径要数到NULL ,而不是数到叶子,否则可能落下一些路径。

3)路径最短和最长的情况

  1. 最短路径:全黑。

  2. 最长路径:一黑一红。每个黑节点都跟个红节点(每条路径上的黑节点个数是固定相同的,红节点还不能连续,最多每个黑节点下插一个红)。

  3. 最短的全黑路径和最长的一黑一红路径并不是在每一棵红黑树中都要出现的。所以假设最短路径上的黑节点数量是bh,那么任意一条从根节点开始到NULL结点路径的长度h总是:

4)红黑树的效率

  1. 假设N是红黑树中结点数量,h是最短路径的长度,那么节点数量在每条路径都是最短路径和每条路径都是最长路径的情况之间即 ,由此推出 ,也就是意味着红黑树增删查改最坏也就是走最长路径2*logN,那么时间复杂度还是O(logN)

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

二. 红黑树的实现(key_value结构)

1)基本结构

cpp 复制代码
#pragma once
// 实现一个Key-value结构的红黑树

enum Color
{
	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;
	Color _color;

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

template <class K, class V>
class RBtree
{
	typedef RBtreeNode<K, V> Node;

private:
	Node* _root = nullptr;

public:
	// ...
};

2)插入

  1. 直接按二叉搜索树规则进行插入,插入后观察是否符合红黑树的4条规则,如果不符合针对具体情况进行调整使其再次成为一棵红黑树。

先判断父亲的情况(红?黑?),再判断叔叔的情况(存在且为红?不存在 或者 存在且为黑?)。

2. 如果是空树插入,新增结点是黑色结点。

如果是非空树插入,新增结点必须红色 结点。因为非空树插入,新增黑色结点就破坏了每条路径上黑色节点一样多的规则,因为对每一条路径都有影响,所以这一点是很难维护的。
3. 非空树插入红色结点后,如果父亲结点是黑色 的,则没有违反任何规则,插入结束
4. 非空树插入红色结点后,如果父亲结点是红色 的,则违反不能出现连续红色的规则。

需要进一步分析处理:(c: cur, p: parent, g: grandfather, u: uncle)c是红色,p为红,g必为黑(没插入之前是红黑树p和g不可能都为红),这三个颜色都固定了。关键看u的情况 ,需要根据u分为以下几种情况和处理方式:

① u不存在

② u存在且为黑

③ u存在且为红

注意我们是在父亲为红导致与新增红节点冲突的情况下 讨论叔叔的可能,别看着看着想不明白为什么父亲一定是红,爷爷一定是黑。并且我们是为了解决cur与parent冲突问题
情况一: ③:直接变色
**情况二:**①和②的处理方式是相同的:旋转+变色。

5. 情况一--直接变色

① c为红,p为红,g为黑,u存在且为红:则将p和u变黑,g变红。再把g当做新的c,继续往上更新。

② 分析(结合图看):

因为p和u都是红色,g是黑色,把p和u变黑,左边子树路径各增加一个黑色结点,g再变红,相当于保持g所在子树的黑色结点的数量不变,同时解决了c和p连续红色结点的问题。

需要继续往上更新是因为,g变为红色:

  • 如果g的父亲还是红色,那么就还需要继续处理;
  • 如果g的父亲是黑色,则处理结束了;
  • 如果g就是整棵树的根,再把g变回黑色。

③ 抽象图

上图展示的是一种具体情况,但是实际中需要这样处理的情况有很多种。

将以上类似的处理进行抽象,d/e/f代表每条路径拥有hb(hb>=0)个黑色结点的子树,a/b代表每

条路径拥有hb-1个黑色结点的根为红的子树。

6. 情况二--单旋\双旋+变色

c为红,p为红,g为黑,u不存在或者u存在且为黑。

① 若u不存在,则c一定是新增结点,而不是下面新增更新上来的;

② 若u存在且为黑,则c一定不是新增。c之前是黑色的,是在c的子树中插入,发生情况1,变色后将c从黑色变成红色,更新上来的。


**解决办法:**p必须变黑,才能解决连续红色结点的问题,u不存在或者是黑色的,这里单纯的变色无法解决问题,需要旋转+变色(不存在和存在且为黑用相同的办法就可以处理)。

① 如果p是g的左,c是p的左,那么以g为旋转点进行右单旋,再把p变黑,g变红即可。

p变成这棵树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父亲是黑色还是红色或者空都不违反规则(黑节点可连续)。

② 如果p是g的右,c是p的右,那么以g为旋转点进行左单旋,再把p变黑,g变红即可。

p变成这棵树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父亲是黑色还是红色或者空都不违反规则。

③ 如果p是g的左,c是p的右,那么先以p为旋转点进行左单旋,再以g为旋转点进行右单旋,再把c变黑,g变红即可。

c变成这棵树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的父亲是黑色还是红色或者空都不违反规则。

④ 如果p是g的右,c是p的左,那么先以p为旋转点进行右单旋,再以g为旋转点进行左单旋,再把c变黑,g变红即可。

c变成这棵树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的父亲是黑色还是红色或者空都不违反规则。

7. 代码实现插入

cpp 复制代码
private:
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* pparent = parent->_parent;

		// 一定要记住每一个节点中都有一个_parent,不要忘记更新
		parent->_left = subLR;
		if (subLR) // h为0的情况
			subLR->_parent = parent;

		subL->_right = parent;
		parent->_parent = subL;

		// 旋转完这个局部子树,要看看pparent是否为空
		// 也就是原本的parent是不是整棵树的根,他还有没有父亲
		if (pparent == nullptr)
		{
			// 直接把新根给_root
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (parent == pparent->_left)
				pparent->_left = subL;
			else
				pparent->_right = subL;

			subL->_parent = pparent;
		}
	}

	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* pparent = parent->_parent;

		// 一定要记住每一个节点中都有一个_parent,不要忘记更新
		parent->_right = subRL;
		if (subRL) // h为0的情况
			subRL->_parent = parent;

		subR->_left = parent;
		parent->_parent = subR;

		// 旋转完这个局部子树,要看看pparent是否为空
		// 也就是原本的parent是不是整棵树的根,他还有没有父亲
		if (pparent == nullptr)
		{
			// 直接把新根给_root
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (parent == pparent->_left)
				pparent->_left = subR;
			else
				pparent->_right = subR;

			subR->_parent = pparent;
		}
	}

public:
	bool Insert(const std::pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_color = BLACK;
			return true;
		}

		Node* cur = _root;
		Node* parent = nullptr;
		// 找空位
		while (cur)
		{
			if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else return false;
		}

		cur = new Node(kv);
		cur->_color = RED;
		// 先直接插入
		if (parent->_kv.first > kv.first)
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
		else if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}

		// parent第一次确实是一定存在的,但是我们这是一个循环向上处理的逻辑
		// 当处理到根节点时,根节点没有父亲
		// 如果父亲存在且为红,根据叔叔决定处理方式,并向上处理
		while (parent && parent->_color == RED)
		{
			// 通过爷爷找叔叔
			Node* grandfather = parent->_parent;
			// 叔叔在右
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;

				// 如果叔叔存在且为红,直接变色并向上处理
				if (uncle && uncle->_color == RED)
				{
					// 变色
					uncle->_color = parent->_color = BLACK;
					grandfather->_color = RED;
					// 向上移动更新
					cur = grandfather; // !!颜色改变可能影响上级的是爷爷,成为新的cur
					parent = cur->_parent;
				}
				// 叔叔不存在 或者 存在且为黑,变色+旋转(单旋?双旋?)
				else
				{
					// 变色+右旋
					//if (parent == grandfather->_left && cur == parent->_left) // uncle在右,parent一定在左
					if (cur == parent->_left)
					{
						RotateR(grandfather);
						parent->_color = BLACK;
						grandfather->_color = RED;
					}
					// 变色+左右双旋
					if (cur == parent->_right)
					{
						RotateL(parent);
						RotateR(grandfather);
						cur->_color = BLACK;
						grandfather->_color = RED;
					}
					break; // 不再继续向上更新
				}
			}
			// 叔叔在左
			else
			{
				Node* uncle = grandfather->_left;

				// 叔叔存在且为红
				if (uncle && uncle->_color == RED)
				{
					// 变色
					uncle->_color = parent->_color = BLACK;
					grandfather->_color = RED;
					// 向上移动更新
					cur = grandfather; // !!颜色改变可能影响上级的是爷爷,成为新的cur
					parent = cur->_parent;
				}
				// 叔叔不存在 或者 存在且为黑,变色+旋转(单旋?双旋?)
				else
				{
					// 变色+左旋
					if (cur == parent->_right)
					{
						RotateL(grandfather);
						parent->_color = BLACK;
						grandfather->_color = RED;
					}
					// 变色+左右双旋
					if (cur == parent->_left)
					{
						RotateR(parent);
						RotateL(grandfather);
						cur->_color = BLACK;
						grandfather->_color = RED;
					}
					break;
				}
			}
		}
		// 如果没处理到根根还是黑的,但是可能处理根时将根变为红,需要变回来
		// 简单处理,无论是否处理到根,最后都将根置黑一次
		_root->_color = BLACK;

		// 如果父亲存在且为黑,直接插入结束,return true;(不止这一种情况return true;)
		return true;
	}

3)查找

和二叉搜索树查找的逻辑相同,效率为O(logN)。

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;
	}

4)验证&&统计

cpp 复制代码
private:
int _Height(Node* root)
{
	if (root == nullptr)
		return 0;

	int leftHeight = _Height(root->_left);
	int rightHeight = _Height(root->_right);

	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

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

	_InOrder(root->_left);
	std::cout << root->_kv.first << " ";
	_InOrder(root->_right);
}

// 统计节点个数
int _Size(Node* root)
{
	return root == nullptr ? 0 : _Size(root->_left) + _Size(root->_right) + 1;
}

public:
bool Check(Node* root, int blackNum, const int refNum)
{
	if (root == nullptr)
	{
		// 前序遍历走到空时,意味着一条路径走完了
		if (blackNum != refNum)
		{
			std::cout << "存在黑色结点的数量不相等的路径" << std::endl;
			return false;
		}

		return true;
	}

	// 检查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲就方便多了
	if (root->_color == RED && root->_parent->_color == RED)
	{
		std::cout << root->_kv.first << "存在连续的红色结点" << std::endl;
		return false;
	}

	if (root->_color == BLACK)
	{
		++blackNum;
	}

	return Check(root->_left, blackNum, refNum) && Check(root->_right, blackNum, refNum);
}

bool IsRBTree()
{
	if (_root == nullptr)
		return true;

	if (_root->_color == RED)
		return false;

	// 参考值,最左路径有几个黑节点,拿去和每条路径做对比
	// 只要有一条路径不一样那就不是红黑树
	int refNum = 0;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_color == BLACK)
		{
			++refNum;
		}
		cur = cur->_left;
	}

	return Check(_root, 0, refNum);
}

void InOrder()
{
	_InOrder(_root);
	std::cout << std::endl;
}

int Height()
{
	return _Height(_root);
}

int Size()
{
	return _Size(_root);
}

测试代码

cpp 复制代码
#include <iostream>
#include <vector>

#include "RBtree.h"

void TestRBtree1()
{
    RBtree<int, int> t;
    // 常规的测试用例
    //int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
    // 特殊的带有双旋场景的测试用例
    int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14,3,5,66,33,543,54,2,435,321,32,43,4324,534 };
    for (auto e : a)
    {
        t.Insert({ e, e });
    }

    t.InOrder();
    std::cout << t.IsRBTree() << std::endl;
}

// 插入一堆随机值,测试平衡,顺便测试一下高度和性能等
void TestRBtree2()
{
    const int N = 10000000;
    std::vector<int> v;
    v.reserve(N);
    srand(time(0));

    for (size_t i = 0; i < N; i++)
    {
        v.push_back(rand() + i);
    }

    size_t begin2 = clock();
    RBtree<int, int> t;
    for (auto e : v)
    {
        t.Insert(std::make_pair(e, e));
    }
    size_t end2 = clock();

    std::cout << "Insert:" << end2 - begin2 << std::endl;
    std::cout << t.IsRBTree() << std::endl;

    std::cout << "Height:" << t.Height() << std::endl;
    std::cout << "Size:" << t.Size() << std::endl;

    size_t begin1 = clock();
    // 确定在的值
    /*for (auto e : v)
    {
            t.Find(e);
    }*/

    // 随机值
    for (size_t i = 0; i < N; i++)
    {
        t.Find((rand() + i));
    }

    size_t end1 = clock();

    std::cout << "Find:" << end1 - begin1 << std::endl;
}


int main()
{
    TestRBtree1();
    TestRBtree2();

	return 0;
}
相关推荐
像素猎人2 小时前
蓝桥杯OJ716【限定第一步和最后一步爬台阶的经典例题】【动态规划】
c++·算法·动态规划
Q741_1472 小时前
每日一题 力扣 3474. 字典序最小的生成字符串 贪心 字符串 C++ 题解
c++·算法·leetcode·贪心
小此方2 小时前
Re:从零开始的 C++ STL篇(九)AVL树太“较真”,红黑树更“现实”:一文讲透工程中的平衡之道
开发语言·数据结构·c++·算法·stl
进击的荆棘2 小时前
C++起始之路——二叉搜索树
数据结构·c++·stl
少司府2 小时前
C++基础入门:类和对象(上)
c语言·开发语言·c++·类和对象·访问限定符
REDcker2 小时前
C++ new、堆分配与 brk / mmap
linux·c++·操作系统·c·内存
阿阿阿阿里郎2 小时前
C++面向对象--类、模板
c++
William_wL_2 小时前
【C++】list的使用
c++
Elnaij3 小时前
从C++开始的编程生活(25)——C++11标准Ⅱ
开发语言·c++