c++ 红黑树学习及简单实现

1. 了解红黑树

1.1. 概念

红黑树,是一种二叉搜索树,但在每个节点增加一个存储位表示节点的颜色,可以是红色,或是黑色,通过对任何一条从根到叶子的路径上各个节点的着色方式进行限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡。
都是搜索二叉树,红黑树和AVL树有什么区别?
AVL 树:左右高度差不超过1(严格平衡)
红黑树:最长路径不超过最短路径的两倍(近似平衡)
这样看起来,似乎 AVL树更优秀,严格平衡
如果相同的数据,AVL 树可能是 18层,但是 红黑树有可能是 18 - 36 层
在查找方面 AVL 树可能会比红黑树优秀一点,但是只是一点。
因为18 层的数据,2^17 ==131072,18层数据就有 这么多节点,查找 18 次和 查找 35次,对计算机来说区别很小。
同时还要注意,AVL树的严格平衡,因为严格平衡,所以 AVL树为了保证平衡,需要不停的旋转,因此在插入删除上,效率会慢很多
但是红黑树并不是严格平衡,所以在调整上,不会进行那么多次的调整,因此插入和删除的时间就会少很多。

1.2. 性质

先看下面一棵树
在这里插入图片描述
红黑树性质:

  1. 每个节点不是红色就是黑色。
  2. 根节点是黑色的。
  3. 如果一个节点是红色的,则它的两个节点都是黑色的(不能出现连续的红色节点,可以出现的组合:黑+黑, 黑+红, 红+黑)。
  4. 对于每个节点,从该节点到其所有后代中,均包含数目相同的黑色节点(每条路径都包含相同数量的黑色节点)。
  5. 每个叶子节点(NIL节点)都是黑色的。

为什么满足上面的性质,红黑树就能保证:最长路径中节点个数不会超过最短路径节点个数的两倍?
最短路径:全黑
最长路径:一黑一红间隔
在这里插入图片描述
假设最短上有 N 个节点
其他路径上的节点数量都在 [N, 2N] 之间
每条路径上黑色节点数量相同,最长的情况下是一红一黑间隔,同一条路径下,红节点数量和黑节点数量相同,也就是2
N
NIL 节点(叶子节点的空节点)
在这里插入图片描述
比如这棵树,这里看起来有 4 条理解,其实有 8 条
NIL 节点也要被算入路径内
路径是从跟节点走到空,才算路径。
在这里插入图片描述
这棵树也算红黑树,这棵树有 7 条路径。
在这里插入图片描述
这棵树,我们看起来好像是一棵红黑树,但是当我们把所有的 NIL 节点全部标出来
在这里插入图片描述

这样就能清楚的看到,标记位置的右子树只含有一个黑色节点,但是左子树的每条路径含有 两个黑色节点,所以 这棵树并不是红黑树

2. 模拟实现

2.1. 节点

enum Colour
{
	RED,
	BLACK
};
template<class K, class V>
struct RBTreeNode
{
	RBTreeNode* _left;
	RBTreeNode* _right;
	RBTreeNode* _parent;
	pair<K, V> _kv;
	Colour _col;
	
	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
	{}
};

这里的节点和 AVL 树类似,不过 红黑树 是用颜色来控制的,所以这里我们添加了 _col 来控制。
不够在这里的 构造函数,我们还不能确定 _col 初始化成什么样子,我们后面再看。

2.2. insert

首先 红黑树 还是搜索二叉树,所以插入的基本逻辑还是那套,我们先实现这部分,后面主要分析调整的地方。

template<class k, class V>
class RBTree
{
public:
	typedef RBTreeNode<K, V> Node;
	RBTree()
		:_root(nullptr)
	{}
	bool insert(const pair<K, V>* kv)
	{
		if(_root == nullptr)
		{
			_root = new Node;
			return true;
		}
		Node* cur = _root;
		Node* parent = nullptr;
		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(cur);
		if(parent->_kv.first < kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
		return true;
	}
private:
	Node* _root;
};

二叉树的插入,这段没什么好说的,接下来看调整。
如果是插入节点,最开始应该插入什么颜色的节点最好?
我们都分析一下
在这里插入图片描述
如果我们在上面这棵树上插入节点,插入黑色节点后,我们发现,这条路径上的黑色节点数量发生变化,所以这里必须调整。
如果插入的是红色节点,每条路径上的黑色节点数量并未发生变化,这里可以不需要调整。
所以我们最好把 插入的节点初始化为 红色节点
在这里插入图片描述
在这里插入图片描述
但这不代表插入红色节点完全不需要修改,插入红色节点后,主要影响的是父节点。
这里插入分为 3 类型的情况:

  1. 如果新增的节点是 黑色节点,会影响所有父亲节点下面的路径。
  2. 如果新增的节点是 红色节点,父亲节点是 黑色节点,不用影响。
  3. 如果新增的节点是 红色节点, 父亲节点是 红色节点, 需要进行调整。

我们需要处理的情况是第 3 种。
在这里插入图片描述
如果出现这种情况,我们首先考虑把父亲变色。
如果仅靠变色不能解决问题,就需要通过 旋转 + 变色。
在这里插入图片描述

第一步,7 变 黑,因为 7 的父节点一定是 黑色,所以还要处理 父节点(7原本是红色,红色节点的父亲只能是黑色,7 所在的路径上多了一个黑色节点,所以还是要从 7 的父亲节点入手处理)。
第二步,7 的 叔叔节点 5 变 黑,但是又会导致 6 的两条路径上分别多出来一个 黑色节点,所以我们还需要把 6 变 红,从而使每条路径黑色节点数量想同。
这里调整自己路径后,还要通过自己的根去调整另一条路径,我们这里默认的情况是 叔叔节点 存在 且 和 7 都是红色。
那么叔叔不存在的情况,或者叔叔颜色原本为黑呢?
叔叔 不存在时:
在这里插入图片描述
这两棵树,如果是前面学过 AVL 树的人,应该能一眼看出来,这里是 右左双旋 和 右右左单旋。
在这里插入图片描述
旋转 + 变色 处理。
当然,这是基于 叔叔节点不存在的情况,叔叔节点存在且为黑的情况还要讨论,现在我们慢慢分析怎么用代码实现这些操作

2.2.1. 调整

我们先把需要调整的情况主要分为三种
在这里插入图片描述先确定节点
插入的节点: cur
插入节点的父节点: parent - p
插入节点的父节点的父节点: grandfather - g
叔叔节点: uncle - u
a,b,c,d,e 子树(可能为空,也可能是一整颗树)

  1. cur 为红,p 为红,g 为黑, u存在且为红
  2. cur 为红,p为红, g 为黑, u不存在
  3. cur 为红,p为红, g 为黑, u存在且为黑

首先是 第一种 情况:=
cur 为红,p 为红,g 为黑, u存在且为红
解决方法:将 p ,u 改为黑色, g 改为红色, 然后把 g 当做 cur,继续向上处理
在这里插入图片描述
但是简简单单变个色就行了吗?
这里还是有特殊情况需要处理的

  1. 如果 g 是 根节点
  2. 如果 g 的父节点是 红色
    在这里插入图片描述

第一种情况很简单,直接把 _root 变黑色就行了。
毕竟不能出现连续的红色节点,但是可以出现连续的黑色节点,所以这样变完全没有问题
那第二种呢,修改后,g 变成了 红色,但是如果 g 是一棵树的子树,同时 g 的父亲节点是 红色呢?
我们的子树是没有问题了,所以我们把 原树的 g 当做新的 cur 向上调整。
在这里插入图片描述
如果 修改后的树 的父亲节点是黑色,那么此时直接退出即可(原本g为根节点的树每条路径是 一个 黑色节点,修改后每条路径仍是一个黑色节点,对 g 以上的根节点来说,每天路径上的黑色节点数量没有发生改变,所以不需要向上调整)
这里我们像 AVL 树那样分析分析,是不是 只需要改变这 4 个节点才能完成操作?
在这里插入图片描述
还是这棵树,此时 a,b,c,d,e 的情况有点复杂
如果 a/b/c/d/e 都是 空,cur 就是新增节点。
如果 a/b 不是空节点,但是想要触发这个调整,必须在 a,b任意位置插入节点就会破坏规则
至于 c,d,e 可能是下面的任意一种情况
在这里插入图片描述
c,d,e 是每条路径上含有一个黑色节点的红黑树。
a,b 作为之前新插入的节点。
在 a,b 下任意位置插入节点都会破坏规则。
所以这里可能存在的树 (4x4x4)x4 = 256种
不管再怎么复杂,对应的子树都是经过调整后的 红黑树,或者说下面调整玩完成后向上调整到这个位置,所以我们只需要关注这里的 cur 就行。

第二种情况:
cur 为红,p为红, g 为黑, u不存在
在这里插入图片描述
如果遇到单纯变色不能解决问题的情况,这里就需要先旋转后变色。
解决方法:左左右单旋,旋转后,g变红, p变黑
这里一般没有什么特殊情况,最后根节点也处理成黑色,不会和父节点冲突
第三种情况:
cur 为红,p为红, g 为黑, u为黑
在这里插入图片描述
这种情况会出现吗,这里 u 可能是上一次调整后的结构,g 可能是根节点,所以会出现这种情况。
这里的处理方法,还是先进行旋转。
但是还是要看 cur 的位置
p 为 g 的左孩子,cur 为 p 的左孩子,进行右单旋
p 为 g 的右孩子,cur 为 p 的右孩子,进行左单旋
p 为 g 的左孩子,cur 为 p 的右孩子,进行左右双旋
p 为 g 的右孩子,cur 为 p 的左孩子,进行右左双旋
在这里插入图片描述

这里默认 a,b,c 都是含有一个黑色节点的红黑树
图不好画,大概按这样的过程理解
旋转完成后,还要变色,主要的变色逻辑就是上面展示的。
单旋:p 变为 黑,g 变为 红色
双旋:g 变为红, cur 变为黑
旋转的大概逻辑了解以后,下面开始实现

while(parent && parent->_parent && parent->_col == RED)
{
	Node* grandfather = parent->_parent;
	if(parent == grandfather->_left)
	{
		Node* uncle = grandfather->_right;
		if(uncle && uncle->_col == RED)
		{
			uncle->_col = parent->_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)
		{
			uncle->_col = parent->_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;
			}
		}
	}
	_root->_col = BLACK;
}

左旋和右旋还是 AVL 树那套,但是要注意,这里我们在 insert 里修改节点颜色,所以左旋右旋里不需要修改颜色,同时 也不需要我们去单独写函数实现 左右双旋和右左双旋来修改颜色。

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

        parent->_right = subRL;
        subR->_left = parent;

        Node* parentParent = parent->_parent;

        parent->_parent = subR;

        if (subRL)
        {
            subRL->_parent = parent;
        }

        if (_root == parent)
        {
            _root = subR;
            subR->_parent = nullptr;
        }
        else
        {
            if (parentParent->_left == parent)
            {
                parentParent->_left = subR;
            }
            else
            {
                parentParent->_right = subR;
            }
            subR->_parent = parentParent;
        }
    }

    void RotateR(Node* parent)
    {
        Node* subL = parent->_left;
        Node* subLR = subL->_right;

        parent->_left = subLR;
        if (subLR)
        {
            subLR->_parent = parent;
        }

        Node* parentParent = parent->_parent;

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

        if (parent == _root)
        {
            _root = subL;
            subL->_parent = nullptr;
        }
        else
        {
            if (parentParent->_left == parent)
            {
                parentParent->_left = subL;
            }
            else
            {
                parentParent->_right = subL;
            }
            subL->_parent = parentParent;
        }
    }

2.2.2. 插入全代码

	bool insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}
		Node* cur = _root;
		Node* parent = nullptr;
		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;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		while (parent && parent->_parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)
				{
					uncle->_col = parent->_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)
				{
					uncle->_col = parent->_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;
	}

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

        parent->_right = subRL;
        subR->_left = parent;

        Node* parentParent = parent->_parent;

        parent->_parent = subR;

        if (subRL)
        {
            subRL->_parent = parent;
        }

        if (_root == parent)
        {
            _root = subR;
            subR->_parent = nullptr;
        }
        else
        {
            if (parentParent->_left == parent)
            {
                parentParent->_left = subR;
            }
            else
            {
                parentParent->_right = subR;
            }
            subR->_parent = parentParent;
        }
    }

    void RotateR(Node* parent)
    {
        Node* subL = parent->_left;
        Node* subLR = subL->_right;

        parent->_left = subLR;
        if (subLR)
        {
            subLR->_parent = parent;
        }

        Node* parentParent = parent->_parent;

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

        if (parent == _root)
        {
            _root = subL;
            subL->_parent = nullptr;
        }
        else
        {
            if (parentParent->_left == parent)
            {
                parentParent->_left = subL;
            }
            else
            {
                parentParent->_right = subL;
            }
            subL->_parent = parentParent;
        }
    }

2.3. check

现在插入的大概逻辑是没问题了,但是和 AVL 树的问题一样,怎么样证明我们的树是 红黑树?
树是不是红黑树,我们需要根据红黑树的性质来判断
在这里插入图片描述
第 1 点,第 2 点,第 5 点都很好检查。
第 3 点相对来说就麻烦了
如果是红色节点,检查子节点是不是都是黑的,但是也不好检查,红色节点的孩子节点可能是 两个,可能是 一个,也可能不存在,所以我们可以考虑不向下查找,向上查找

bool Check(Node* root)
{
	if(root == nullptr)
	{
		return true;
	}
	if(root->_col == RED)
	{
		
	}
	return Check(root->_left) && Check(root->_right);
}
bool IsBalance(Node* root)
{
	if(root->_col ==BLACK)
	{
		return true;
	}
	if(root->_col == RED)
	{
		return false;
	}
	return Check(root);
}

IsBalance 检查根节点是不是黑色,Check 检查每个红色节点是不是有两个黑色子节点,如果直接检查红色节点的子节点情况,有点麻烦,所以我们反向检查

if(root->_col == RED && root->_parent->_col == RED)
{
	cout << "有连续的红色节点" << endl;
	return false;
}

这里的判断条件可以这样写,当前节点为红色,且这个节点的父节点是红色,就返回 false;
红色节点一定有父亲,且红色节点的父亲一定是黑色。
接下来处理,每条路径上的黑色节点数量相同,该怎么操作?
初步想法,拿栈可以检查,黑色节点入栈,当需要出栈时检测一下数量即可。
除了这个方法,我们还有其他方法
isBalance 传入 blacknum ,当遇见黑色节点++,直到遇见空节点时输出 blacknum 。

bool Check(Node* root, int blacknum)
{
	if(root == nullptr)
	{
		cout << blacknum << "  ";
	}
	if(root->_col == RED & root->_parent->_col == RED)
	{
		cout << "有连续的红色节点" << endl;
	}
	if(root->_col == BLACK)
	{
		++blacknum;
	}
	return Check(root->_left, blacknum) && Check(root->_right, blacknum);
}
bool IsBalance()
{
	return _IsBalance(_root);
}
private:
bool _IsBalance(Node* root)
{
	if(root->_col == RED)
	{
		return false;
	}
	int blacknum = 0;
	return Check(root, blacknum);
}

在这里插入图片描述
这里我们看见,所有路径的黑色节点数量都是 2
但是数据量太少了,我们需要和 AVL 树一样,传入大量数据来测试我们的 红黑树

void test_RBT ree2()
{
	const size_t N = 10000;
	vector<int> v1;
	RBTree<int, int> t1;
	srand(time(0));
	for(int i = 0; i < N; i++)
	{
		v1.push_back(rand());
	}
	for(auto : v1)
	{
		t1.insert(make_pair(e, e));
	}
	cout << t1.IsBalance() << endl;
}

在这里插入图片描述
当我们输出所有路径的黑色节点数量时,发现,这数据有点多,我们看不过来,不方便检查。
这里我们的思路是,除了传入 blacknum, 我们还需要传入一个判断的值

        bool Check(Node* root, int blacknum, const int refVal)
        {
                if (root == nullptr)
                {
                        if (blacknum != refVal)
                        {
                                cout << "存在黑色节点数量不相同的路径" << endl;
                                return false;
                        }
                        return true;
                }
                if (root->_col == RED && root->_parent->_col == RED)
                {
                        cout << "有连续的红色节点" << endl;
                        return false;
                }
                if (root->_col == BLACK)
                {
                        ++blacknum;
                }
                return Check(root->_left, blacknum) && Check(root->_right, blacknum);
        }
        bool IsBalance()
        {
                return _IsBalance(_root);
        }
private:
        bool _IsBalance(Node* root)
        {
                if (root->_col == RED)
                {
                        return false;
                }
                int blacknum = 0;
                int refVal = 0;
                Node* cur = root;
                while (cur)
                {
                        if (cur->_col = BLACK)
                        {
                                refVal++;
                        }
                        cur = cur->_left;
                }
                return Check(root, blacknum, refVal);
        }

在这里插入图片描述
这里我们看见,在debug 版本下,插入和检查的时间一共用了 309ms,非常快
在这里插入图片描述
release 版本下,用了 119ms
在这里插入图片描述
100w 的数据一共是 18 层
注:这里看起来和 AVL 树高度差不多,其实是因为 rand 是伪随机,给的数值在量很大的情况下,会出现重复,所以表面上我们插入了 100w 个数据,实际上数据早就饱和了

2.4. erase

删除节点
删除的情况比插入的情况多很多,和AVL树一样,我们还是简单说一下,不实现
在这里插入图片描述
如果我们要删除 红色节点
在这里插入图片描述
这没啥影响,那5条规则没有受到破坏
如果删除的黑色节点
在这里插入图片描述
删除 黑色节点,我们发现,这个节点所在的路径黑色节点减少了,这里仅靠变色无法解决
在这里插入图片描述
25变红,17变黑?但是这样会导致出现连续的红色节点。
最右边的两条路径不管怎么样都会含有一个黑色节点,但是左边这条路径不存在黑色节点,所以需要旋转解决,这里还好看一点,右边整体高,右右左单旋。
在这里插入图片描述
删除时,如果出现问题,和插入一样,优先考虑 变色,后考虑旋转,但是旋转一定要放在 变色前。
删除的情况比较多,这里就不细说了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值