红黑树实现—规则&约束的平衡之道

目录

一、前言

二、红黑树

1、结构

2、规则

3、效率

4、结构实现

(1)结点

(2)插入

[<1> 变色](#<1> 变色)

[<2> 单旋+变色](#<2> 单旋+变色)

[<3> 双旋+变色](#<3> 双旋+变色)

(3)查找

(4)结点数

(5)高度

(6)中序

(7)Isbalance

(8)测试

三、结语


一、前言

在数据结构中,数据的增删查以二叉搜索树为经典代表,但由于二叉搜索树在极端情况下会退化为链式结构,查找效率退化为O(N),因此就需要对二叉搜索树的结构进行平衡,即平衡二叉搜索树,AVL树、红黑树就是经典的平衡二叉搜索树,AVL树在二叉搜索树的基础上对左右子树的高度进行了平衡,左右子树的高度差绝对值不超过1,从而控制了左右子树的平衡,查找效率为O(logN)。红黑树并没有像AVL树那样对高度有严格的控制,而是通过几条规则对结点进行约束,从而实现平衡,红黑树在结构上没有像AVL树那样严格趋近平衡,红黑树平衡程度介于二叉搜索树与AVL树之间,其查找效率也在O(logN)这个量级,本文将围绕红黑树展开介绍,以红黑树的结构为出发点,进一步实现红黑树,由浅入深,逐步领略红黑树规则与约束的平衡之美。

二、红黑树

1、结构

红黑树为平衡二叉搜索树,红黑树的结点增加了一个存储位来表示结点的颜色,可以是红色或黑色,红黑树通过对任何一条从根到叶子的路径上各个结点的颜色进行约束,确保没有一条路径会比其他路径长出2倍,因此红黑树是接近平衡的。

2、规则

红黑树的结构需满足以下几条规则:

1、结点不是红色就是黑色。

2、根结点为黑色。

3、如果一个结点为红色,则它的两个孩子结点只能为黑色,即任意一条路径不会有连续的红色结点。

4、对于任意一个结点,从该结点到其所有NIL结点的简单路径上,均包含相同数量的黑色结点,NIL结点指的是空结点,规定NIL结点的颜色为黑色。

如上图所示的红黑树,以根13为起点,共有11条到NIL结点的简单路径,每条路径黑色结点的数量相等,都为3(包含NIL),最长路径如13->17->25->27->NIL的长度不超过最短路径如13->8->1->NIL的2倍,NIL结点在实践中一般省略不表示。由红黑树的规则和约束条件可知,假设每条路径有X个黑色结点,则最短路径长度为X,即全黑路径,最长路径长度为2X,即一黑一红组合,全黑最短路径和一红一黑最长路径并不是在每个红黑树中都存在,一般情况下,假设任意一条从根到NIL结点路径的长度为H,则X=<H<=2X。

3、效率

N为红黑树的结点数量,h是最短路径的长度,可知N不小于高度为h的完全二叉树结点数,即N>=2^h-1,不大于高度为2h的完全二叉树结点数,即N<=2^2h-1,由此可知红黑树增删改查最坏情况也就是走最长路径2logN,可知红黑树增删改查的时间复杂度也为O(logN)这个量级,红黑树相比AVL树要抽象一些,AVL树通过高度差直观地控制了平衡,红黑树通过4条规则的颜色约束,间接的实现了近似平衡,它们的效率都为O(logN),但相比于AVL树而言,插入相同数量的结点,由于红黑树对平衡的控制没有AVL树那么严格,故红黑树的旋转次数比AVL树要少,因此实战中红黑树相比AVL树有更广泛的应用。

4、结构实现

(1)结点

结点实现按key_value结构实现,pair成员_kv用于key_value的存储,colour枚举值表示结点颜色

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

_left、_right为左右孩子指针,与AVL树类似,红黑树结点也需存储_parent指针用于完成结点的更新与平衡的控制,RBtreenode结点构造函数通过初始化列表将pair成员变量_kv初始化为kv,_left、_right、_parent初始化为nullptr,就完成了红黑树结点的构造。

(2)插入

有了结点结构,就可以实现红黑树的结构了,using Node=RBtreenode<K,V>,将结点重命名为Node,红黑树RBtree成员变量为Node*根结点指针_root,_root初始化为nullptr。首先来实现红黑树的插入操作,先按二叉搜索树的规则进行插入,插入后需判断是否符合红黑树的4条规则。

如果是空树插入结点,即_root==nullptr,则新增结点即为根结点,_root=new Node(kv),再将根结点染黑,即_root->_col=BLACK,return true就完成了空树的插入。

cpp 复制代码
template<class K,class V>
class RBtree
{
	using Node = RBtreenode<K, V>;
public:
	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;
    }
private:
	Node* _root = nullptr;
};

如果是非空树插入,则先按二叉搜索树的规则插入,cur初始化为_root,parent初始化为nullptr,用于标记cur的位置,便于后续指针的连接,通过while循环遍历搜索树,若cur->_kv.first<kv.first,则cur往右子树方向继续遍历,即parent=cur,cur=cur->_right,若cur->_kv.first>kv.first,则cur往左子树方向继续遍历,即parent=cur,cur=cur->_left,else则cur->_kv.first==kv.first,不支持相同key的插入,故插入失败,return false,遍历完成后,cur来到相应的位置,构造新插入结点,即cur=new Node(kv),需要注意的是非空树插入,新增结点必须为红色结点,若新增结点为黑色结点,就破坏了红黑树每条路径的黑色结点数量相等这一规则,显然这条规则是很难维护的,因此规定新增结点为红色结点,cur->_col=RED,接下来处理parent与cur的连接问题,若parent->_kv.first<kv.first,则cur应为parent的右孩子,即parent->_right=cur,反之parent->_left=cur,cur的_parent指针指向parent,即cur->_parent=parent,就完成了parent与cur的连接。

接下来还需讨论parent与cur结点颜色是否冲突的问题,由于新增结点cur为红色结点,如果parent为黑色结点,则二者不冲突,插入结束。

若parent为红色结点,如下图所示,为方便分析,将新增结点标识为c,c的父亲结点标识为p,p的父亲结点标识为g,p的兄弟结点标识为u。

则c结点插入后出现了连续的红色结点,违反了红黑树的规则,p结点与c结点红红颜色冲突,需要对结点颜色进行调整,进一步分析,c为红,p为红,则g必为黑,这3个结点的颜色都固定了,关键看u的颜色,分类讨论也就是围绕u结点的颜色展开分析。

<1> 变色

情况1:c为红,p为红,g为黑,u存在且为红。

分析:p和u都为红,g为黑,可以先把p和u染黑,这样左边子树路径各增加一个黑色结点,再把g变红,这样相当于保持g所在子树的黑色结点的数量不变,同时也解决了c和p连续红色结点的问题,需要注意的是把g变红,如果g的双亲结点还是红色,那么就需继续向上更新,把g当成新的c,采取相同方式继续向上处理,如果g的双亲结点是黑色,则处理结束,如果g是整棵树的根,则再把g染回黑色即可,情况1只需变色即可,不旋转,无论c是p的左还是右,p是g的左还是右,都采取该变色处理方式。

与AVL树类似,上面只展示了一种具体情况,可以对子树进行抽象,以此表示这种变色处理方式下的所有情况。

在子树a或b中插入新结点,向上处理更新,X结点由黑更新为红,与双亲结点6的颜色红红冲突,因此需继续向上处理,由于u结点也为红,故处理方式为将p、u变黑,g变红,同理也需观察g的双亲结点是否与g颜色冲突,若冲突,继续向上处理,若g的双亲结点为黑,则处理结束,若g为整棵树的根,则再将g染黑即可。

代码实现:

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
                {
                  //u为黑或不存在
                }
            }
		    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为黑或不存在       
                }
            }
         }
      }   
   }    

while循环用于结点的向上处理更新,当parent结点存在且parent结点颜色为红时,继续向上处理,当uncle结点存在且为红,不论parent为grandfather结点的左孩子还是右孩子,都采取相同的变色处理方式,parent->_col=uncle->_col=BLACK,grandfather->_col=RED,将parent和uncle结点颜色变黑,再将grandfather结点颜色变红,cur=grandfather,parent=cur->_parent,继续向上更新处理。

<2> 单旋+变色

c为红,p为红,g为黑,u不存在或者u存在且为黑,若u不存在,则c一定是新增结点,若c是由c的子树更新上来的,则说明插入更新之前c为黑,而u不存在,可知从根到c和u两条路径的黑色结点数量不相等,与红黑树的规则不相符,因此c为新增结点。同理可知若u存在且为黑,则c是由c的子树更新上来的。

先分析u不存在的情况:

当u不存在,c结点插入后,p必须变黑才能解决连续红色结点的问题,p变黑就破坏了红黑树每条路径黑色结点数量相等这一规则,这种情况下单纯的变色就无法解决问题,需要进行旋转+变色。

如上图,p是g的左,c插入在p的左,先以g为旋转点进行右单旋,右单旋后p变为新的根,再将p变黑,g变红,这样右单旋变色后以p为根的子树黑色结点的数量保持不变,且也没有连续的红色结点,不需要继续向上更新,p的双亲结点是黑色或红色或空都不违反红黑树规则,处理结束。

同理当p是g的右,c是p的右,也采取类似的处理方式,如下图所示:

先以g为旋转点进行左单旋,p变为新的根,再将p染黑,g染红即可,左单旋变色后以p为根的子树不违反红黑树的规则,处理结束。

当u存在且为黑时,则c是由c的子树向上更新过来的,即c的颜色由黑更新为红,当c为p的左,p为g的左时,处理方法与上面一致,可以对c的子树进行抽象表示,可概括该情况的所有场景,如下图

在结点3的子树中插入新结点,向上处理更新,结点3颜色由黑更新为红,与结点6颜色红红冲突,且u结点颜色为黑,处理方法与上面类似,以10为旋转点进行右单旋,d变为10的左子树,10变为6的右子树,6变为新的根,再将结点10染红,结点6染黑,这样右单旋变色后,以6为根的子树黑色结点的数量不变,同时也解决了连续的红色结点问题,也不需要继续向上更新,处理结束。

同理当c是p的右,p是g的右时,处理方法也类似,如下图:

先以g为旋转点进行左单旋,p1变为g的右子树,g变为p的左子树,p作为新的根,再将g染红,p染黑,左单旋变色后,以p为根的子树黑色结点的数量不变,同时也解决了连续的红色结点问题,也不需要继续向上更新,处理结束。

代码实现:

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;
					}
                    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;
					}
                    break;
                }
             }
         }
		_root->_col = BLACK;
		return true; 
	}

接着上面u存在且为红的代码思路,else则为u不存在或u的结点颜色为黑的情况,分情况进行讨论,当parent为grandfather的左孩子且cur为parent的左孩子,即parent==grandfather->_left,cur==parent->_left,则先以grandfather为旋转点进行右单旋,即RotateR(grandfather),再将parent结点染黑,grandfather结点染红,parent->_col=BLACK,grandfather->_col=RED,右单旋变色后不需要继续向上处理,break跳出循环,处理结束。同理当parent为grandfather的右孩子且cur为parent的右孩子时,即parent==grandfather->_right,cur==parent->_right,先以grandfather为旋转点进行左单旋,即RotateL(grandfather),再将parent结点变黑,grandfather结点变红,parent->_col=BLACK,grandfather->_col=RED,左单旋变色处理后,也无需继续向上处理,break跳出循环即可,由于向上处理不确定是否处理到根结点,为确保根结点颜色为黑,处理完成后将根结点染黑,即_root->_col=BLACK,return true插入成功。

右单旋实现:

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

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

		Node* pParent = parent->_parent;

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

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

			subL->_parent = pParent;
		}
	}

左单旋实现:

cpp 复制代码
    void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

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

右单旋和左单旋代码如上,旋转处理思路可参考博主的AVL树博客,文章对左右单旋及双旋的图解及处理思路都作了详细分析,这里就不再赘述了。

<3> 双旋+变色

双旋+变色还是建立在u不存在或u为黑色结点的基础上,与单旋变色情况类似,当u不存在时,c为新增结点,当u存在且为黑时,则c是由c的子树向上更新过来的。

先分析u不存在的情况:

如上图,p是g的左,c插入在p的右,那么对于以g为根的子树来说是左子树偏高,而对于以p为根的子树来说是右子树偏高,二者高度差不一致,这时仅采取单旋+变色就无法直接解决问题了,需要双旋+变色处理,先以p为旋转点进行左单旋,再以g为旋转点进行右单旋,左右双旋后c变成新的根,再将c染黑,g染红,这样双旋变色后,以c为根的子树黑色结点的数量不变,同时也解决了连续的红色结点问题,也不需要继续往上更新,c的双亲结点为黑为红为空都不违反红黑树的规则,处理结束。

同理当p是g的右,c插入在p的左,处理方式与上面也类似,如下图所示:

先以p为旋转点进行右单旋,再以g为旋转点进行左单旋,右左双旋后c变为新的根,再将c染黑,g染红,右左双旋变色后,以c为根的子树黑色结点的数量不变,同时也解决了连续的红色结点问题,也不需要继续往上更新,c的双亲结点为黑为红为空都不违反红黑树的规则,处理结束。

当u存在且为黑时,则c是由c的子树更新上来的,即c的颜色由黑更新为红,p为g的左子树,c为p的右子树,可对c的左右子树抽象表示,以概括左右双旋+变色的所有场景,如下图所示:

在结点8的子树中插入新结点,结点8的颜色由黑更新为红,与结点6的颜色红红冲突,u结点颜色为黑,处理方法与上面类似,先以p为旋转点进行左单旋,a变为p的右子树,p变为8的左子树,再以g为旋转点进行右单旋,b变为10的左子树,10变为8的右子树,左右双旋后c变为新的根,再将c染黑,g染红,这样左右双旋变色后,以c为根的子树黑色结点数量不变,同时也解决了连续的红色结点问题,c的双亲结点颜色为黑为红为空都不与c冲突,不需要继续向上更新,处理结束。

同理当p为g的右子树,c为p的左子树,也采取类似的处理方式,如下图所示:

先以p为旋转点进行右单旋,c2变为p的左子树,p变为c的右子树,再以g为旋转点进行左单旋,c1变为g的右子树,g变为c的左子树,右左双旋后c变为新的根,再将c染黑,g染红,这样右左双旋后变色后,以c为根的子树黑色结点数量不变,同时也解决了连续的红色结点问题,c的双亲结点颜色为黑为红为空都不与c冲突,不需要继续向上更新,处理结束。

代码实现:

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

代码实现接着上面单旋+变色的思路,else则为双旋+变色的情况,当parent==grandfather->_left,cur==parent->_right时,先以parent为旋转点进行左单旋,RotateL(parent),再以grandfather为旋转点进行右单旋,RotateR(grandfather),左右双旋后cur变为新的根,再将cur染黑,grandfather染红,即cur->_col=BLACK,grandfather->_col=RED,左右双旋变色后不需要继续向上更新,break跳出循环,处理结束。同理当parent==grandfather->_right,cur==parent->_left时,处理方法也类似,先以parent为旋转点进行右单旋,RotateR(parent),再以grandfather为旋转点进行左单旋,RotateL(grandfather),右左双旋后cur变为新的根,再将cur染黑,grandfather染红,cur->_col=BLACK,grandfather->_col=RED,右左双旋变色后也不需要继续向上更新,break跳出循环,处理结束,以上就是红黑树插入实现的完整代码。

(3)查找

红黑树的查找与二叉搜索树的查找规则一致,即走key的查找路线,通过比较pair的first与key的大

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

小关系进行查找,cur初始化为_root,通过while循环遍历红黑树,当cur->_kv.first<key,cur往右子树方向继续查找,即cur=cur->_right,当cur->_kv.first>key,cur往左子树方向继续查找,即cur=cur->_left,else则二者相等,即cur->_kv.first==key,查找成功,return cur,while循环结束,cur为nullptr,则查找失败,return nullptr。

(4)结点数

求红黑树的结点数,与二叉树的处理方式一致,可通过递归来进行求解,结点数等于左子树结点数

cpp 复制代码
template<class K,class V>
class RBtree
{
	using Node = RBtreenode<K, V>;
public:
    int Size()
	{
		return _Size(_root);
	}
private:
    int _Size(Node* root)
	{
		if (root == nullptr)
			return 0;
		return _Size(root->_left) + _Size(root->_right) + 1;
	}
private:
	Node* _root = nullptr;
};

+右子树结点数+1(根结点),左右子树结点数可通过递归来求解,递归返回条件为root==nullptr,即空结点,return 0,需要注意的是_root为private成员,类外不能直接访问_root,因此可在类内实现成员函数Size访问_root来调用_Size,即 return _Size(_root),从而求出结点数,则类外就可以通过调用size成员函数来求出结点数。

(5)高度

求红黑树的高度,与二叉树处理方法类似,通过递归来进行求解,先递归求出左右子树的高度,则

cpp 复制代码
​
template<class K,class V>
class RBtree
{
	using Node = RBtreenode<K, V>;
public:
    int Height()
	{
		return _Height(_root);
	}
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;
	}
private:
	Node* _root = nullptr;
};

​

树的高度为左右子树高度的较大值+1(根结点),即return leftHeight>rightHeight?leftHeight+1:rightHeight+1,递归返回条件为root==nullptr,即空树,高度为0,return 0,与上面类似,由于_root为private成员,类外不能直接访问,故需在类内实现成员函数Height,通过访问_root调用_Height,即return _Height(_root),这样类外就可以通过调用成员函数Height来求出树的高度。

(6)中序

中序遍历实现与二叉树处理方式类似,key_value红黑树中序遍历结果为key的升序排序,通过递归

cpp 复制代码
​
​
template<class K,class V>
class RBtree
{
	using Node = RBtreenode<K, V>;
public:
    void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
private:
    void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}
private:
	Node* _root = nullptr;
};

​

​

实现,先递归遍历左子树,即_InOrder(root->_left),再遍历根,访问pair的first、second,即cout<<root->_kv.first<<":"<<root->_kv.second<<endl,最后递归遍历右子树,即_InOrder(root->_right),递归返回条件为root==nullptr,return;就完成了红黑树的中序遍历,同理也需在类里实现InOrder成员函数,通过访问_root调用_InOrder,即return _InOrder(_root),这样类外就可以通过调用成员函数Inorder完成红黑树的中序遍历。

(7)Isbalance

Isbalance成员函数用于检查红黑树是否满足它的4条规则,首先当_root==nullptr时,空树为红黑树

cpp 复制代码
​
​

template<class K,class V>
class RBtree
{
	using Node = RBtreenode<K, V>;
public:
    bool Isbalance()
	{
		int rednum = 0;
		if (_root == nullptr)
		{
			return true;
		}
		if (_root->_col == RED)
		{
			return false;
		}
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				rednum++;
			}
			cur = cur->_left;
		}
		return check(_root, 0, rednum);
	}
private:
    bool check(Node* root, int blacknum, const int rednum)
	{
		if (root == nullptr)
		{
			if (blacknum != rednum)
			{
				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, rednum) && check(root->_right, blacknum,
rednum);
	}
private:
	Node* _root = nullptr;
};

​

​

​

return true,当根结点为红色,即_root->_col==RED,与红黑树规则不符,return false,接下来检查规则3与规则4,是否有连续的红色结点以及每条路径的黑色结点是否相等,可先计算出一条路径的黑色结点数量,其他路径分别与该路径进行比较,若存在一条路径与它不相等,则不符合红黑树规则,rednum用于计算一条路径的黑色结点数量,cur初始化为_root,通过while循环,cur=cur->_left,cur向左子树方向遍历,若cur->_col==BLACK,rednum++,直到cur为nullptr,rednum即为一条路径的黑色结点数量,check用于其他路径黑色结点数量与该路径黑色结点数量的比较,思路为递归进行比较,采取前序递归遍历的方式,若root->_col==BLACK,blacknum++,当前序遍历走到空时,即root==nullptr,意味着一条路径已经走完了,比较该条路径的blacknum与rednum的大小,若rednum!=blacknum,即存在黑色结点数量不相等的路径,违反红黑树规则,return false,反之blacknum==rednum,return true,比较完一条路径后,再递归回溯至上一层继续比较,即return check(root->_left,blacknum,rednum) && check(root->_right,blacknum,rednum),对于连续红色结点的检查,检查红色结点的孩子结点不太方便,因为孩子结点有两个,且不一定存在,可以转换思路,由红黑树的规则可知,红色结点必有双亲结点,因此可以反过来检查红色结点的双亲结点,若红色结点的双亲结点也为红,即root->_col==RED && _root->_parent->_col==RED,则存在连续的红色结点,与红黑树规则不符,return false,至此,就完成了红黑树4条规则的检查。

(8)测试

对上面实现的红黑树进行测试,RBtree<int,int> t,构造红黑树t,通过范围for将arr数据以pair<int

cpp 复制代码
#include<iostream>
#include"RBtree.h"
using namespace std;
void testRBtree01()
{
	int arr[] = { 16,3,7,11,9,26,18,14,15 };
	RBtree<int, int> t; 
	for (auto e : arr)
	{
		t.insert({ e,e });
	}
	t.InOrder();
	cout << t.Isbalance() << endl;
}
int main()
{
	testRBtree01();
	return 0;
}

,int>插入到t中,t.InOrder(),对其进行中序遍历,结果应为key的升序排序,t.Isbalance(),检查是否满足红黑树的4条规则,结果如下:

t.InOrder()中序遍历结果为key的升序排序,t.Isbalance()为1,满足红黑树规则,结果正确,测试通过。

三、结语

红黑树在二叉搜索树的基础上对其结构进行了规则约束,根结点为黑,路径上不允许有连续的红色结点,从根到NIL结点的每条路径黑色结点数量相同,这几条规则使得红黑树最长路径不会超过最短路径的2倍,因此红黑树在结构上大体是平衡的,平衡程度介于二叉搜索树与AVL树之间,其时间复杂度也为O(logN)这个量级,红黑树对于平衡的控制没有AVL树那么严格,插入相同数量的结点,红黑树的旋转次数要比AVL树旋转次数少,因此实战中红黑树应用更为广泛,红黑树结点的插入需要根据具体情况处理,分为两大类,当u结点存在且为红时,这时将p和u变黑,g变红,继续向上更新处理,这种情况下采取变色即可,当u不存在或u存在且为黑时,需要旋转+变色,根据具体情况分为单旋+变色、双旋+变色,与AVL树结点的插入类似,AVL树结点插入需要进行单旋或双旋以及平衡因子的更新。红黑树的查找、结点数、高度、中序实现与二叉搜索树实现思路类似,红黑树结构的检查,前几条规则检查较为简单,对于每条路径黑色结点数量相等的检查,可事先计算出一条路径的黑色结点数量,再通过前序递归遍历算出每条路径的黑色结点数量,依次与其进行比较,从而完成对每条路径黑色结点数量的检查。对于红黑树,黑高平衡是性能的保证,红黑规则是维持平衡的手段,红黑树通过放宽严格的平衡条件,从而减少了旋转次数,在实践中相比AVL树应用更为广泛,红黑树结构的实现恰恰是规则与约束共同作用下所诠释的平衡之美!

相关推荐
yaoh.wang2 小时前
力扣(LeetCode) 70: 爬楼梯 - 解法思路
python·算法·leetcode·面试·职场和发展·动态规划·递归
逸风尊者2 小时前
开发可掌握的知识:推荐系统
java·后端·算法
名誉寒冰2 小时前
深入理解fd_set:从基础到实战应用(Linux/C++)
java·linux·c++
Learner__Q2 小时前
每天五分钟:二分查找-LeetCode高频题解析_day4
python·算法·leetcode
智者知已应修善业2 小时前
【字符串提取3个整数求和】2024-2-11
c语言·c++·经验分享·笔记·算法
唯唯qwe-2 小时前
Day21:贪心算法 | 加油站,分发糖果
算法·贪心算法
博语小屋2 小时前
Linux 地址转换函数详解
linux·运维·服务器·c++
特立独行的猫a2 小时前
C++开发中的构建工具:现代CMake实战速成
开发语言·c++·cmake·入门教程
点云侠2 小时前
粒子群优化算法求解三维变换矩阵的数学推导
线性代数·算法·矩阵