C++进阶—【红黑树】

目录

1. 红黑树的概念

2. 红黑树的性质

3. 红黑树节点的定义

4. 红黑树结构

5. 红黑树的插入操作

1. 按照二叉搜索的树规则插入新节点

2. 插入后看颜色是否符合要求

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

情况2:c为红,p为红,g为黑,u不存在/u存在且为黑

情况3:c为红,p为红,g为黑,u不存在/u存在且为黑

1. 以升序插入构建红黑树 

6.  红黑树的验证

7. 红黑树与AVL树的比较

8. 红黑树的应用

9. 红黑树模拟实现STL中的map与set

 9.1 红黑树的迭代器

 9.2 改造红黑树


1. 红黑树的概念

        红黑树,是一种 二叉搜索树 ,但 在每个结点上增加一个存储位表示结点的颜色,可以是 Red Black 。 通过对 任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路 径会比其他路径长出俩倍 ,因而是 接近平衡 的。

2. 红黑树的性质

  1. 每个结点不是红色就是黑色
  2. 根节点是黑色的 
  3. 如果一个节点是红色的,则它的两个孩子结点是黑色的 (也就是说不能有连续的红色节点)
  4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点 (每条路径(左右子树)的黑色节点相同)
  5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
        为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点 个数的两倍?
        因为首先不能有连续的红色节点,其次每条路径上的黑色节点要相同,那么就会出现最坏情况和最好的情况,最坏的情况就是一条路径上全是黑色节点(比如,根的左子树全是黑色节点),那么最好的情况就是路径上一黑一红交替(比如根的右子树是一黑一红交替的节点),这个最好的情况是最坏情况的两倍,所以不会超过两倍。


3. 红黑树节点的定义

这里为什么节点的颜色默认是红色的?

因为节点如果默认是黑色,那么只要新增节点就一定会破坏红黑树的第四个条件(每个路径上的黑色节点相同),而节点默认是红色,则有概率插入到正确的位置,即使出现连续的红色节点,我们也可以通过调整节点的颜色来使红黑树恢复正常。

enum Color
{
	RED,
	BLACK
};

template<class K,class V>
struct RBTreeNode
{
	RBTreeNode(const pair<K, V>& Data)
		: _Data(Data)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_col(RED)			//默认是红色
	{}

	pair<K, V> _Data;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	Color _col;					//节点的颜色

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

4. 红黑树结构

这里和stl库中的红黑树结构稍有不同,库里为了实现关联式容器简单,红黑树的实现中增加一个头结点,因为跟节点必须为黑色,为了 与根节点进行区分,将头结点给成黑色,并且让头结点的 pParent 域指向红黑树的根节点,pLeft 域指向红黑树中最小的节点,_pRight域指向红黑树中最大的节点,如下图:

5. 红黑树的插入操作

红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:

1. 按照二叉搜索的树规则插入新节点

        二叉搜索树的插入前面有讲,这里就不说了。

2. 插入后看颜色是否符合要求

        因为新节点的颜色默认是红色,如果他的父节点是黑色那么符合条件3(不能有连续的红色)的要求,如果他的父节点是红色,那么违反条件3,需要进行调整,那么调整有三种情况:

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

注: c -> cur,   p -> parent,   g -> grandParent,   u -> uncle,

如果插入后出现情况一,那么这时就需要对节点进行更改颜色以达到红黑树的条件要求,将p和u的颜色改为黑色,将g的颜色改为红色,g如果是根节点,那么会在调整结束后对g进行修改颜色;如果g是子树,那么g肯定有父亲,g的父亲还是红色,那么把g当成c,继续向上调整。

情况1变情况1:

        当然情况1也有可能是通过子树调整后得到的,如下图,插入c后出现了连续的红色节点,出现了情况1,那么就需要将p和u的颜色变为黑,g的颜色变为红色。继续向上更新位置,可以看到又出现了情况1,那么继续调整颜色,将p和u的颜色变为黑色,g的颜色变为红色,调整完成后发现不符合循环的条件了,那么调整结束,最后将根的颜色改为黑色,就符合红黑树的要求了。

 情况2:c为红,p为红,g为黑,u不存在/u存在且为黑

u的情况分两种:1. u不存在,那么c一定是新增,2. u存在且为黑,那么c原来的颜色是黑,只不过是由于c的子树调整时将c改成了红色。

出现情况2后,更改颜色并不能满足红黑树的要求了,所以此时进行旋转操作,以下图为例,u不存在,直接跟AVL树一样右旋即可,旋转完成后将g改为红色,p改为黑色,旋转完成后也就不需要再进行调整了,直接break即可。

情况1变情况2:
        以下图为例,情况2是由情况1演变而来的,那么先对情况1进行处理,而后再处理情况2,。这里的情况2和AVL树的右单旋是一样的,以g为轴点旋转即可,旋转完成后将g改为红色,p改为黑色即可。

情况3:c为红,p为红,g为黑,u不存在/u存在且为黑

        以下图为例,出现情况3时,跟AVL树一样,g,p,c连成的线是一条曲线,单旋只能将曲线 水平翻转 ,而不能满足要求,那么就要双旋,先以p为轴点向左旋转,再以g为轴点向右旋转,旋转完成后再讲g改成红色,c改成黑色,调整就完成了。

 情况1变情况3:

        以下图为例,情况1调整后变成情况3,情况3以p为轴点进行左单旋,调整后变成情况2,再以g为轴点进行右单旋,调整完成后红黑树就符合条件啦。

当然上图只是一部分例子,还有不同的各种各样的例子出现,比如上面的p是g的左树,而p如果是g的右树,只需要对应情况对代码做一点修改即可。

bool Insert(const pair<K, V>& value)
	{
		if (_root == nullptr)
		{
			_root = new Node(value);
			_root->_col = BLACK;
			return true;
		}

		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_Data.first > value.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_Data.first < value.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}
		
		cur = new Node(value);
		if (parent->_Data.first > value.first)
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_right = cur;
			cur->_parent = parent;

		}
		while ( parent && parent->_col == RED)
		{
			Node* grandParent = parent->_parent;

			if (parent == grandParent->_left)
			{
				Node* uncle = grandParent->_right;
				//情况一
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandParent->_col = RED;

					cur = grandParent;
					parent = cur->_parent;
				}
				else
				{
					//情况二
					if (parent->_left == cur)
					{
						RotateR(grandParent);
						grandParent->_col = RED;
						parent->_col = BLACK;
					}
					else //情况三
					{	
						RotateL(parent);
						RotateR(grandParent);
						grandParent->_col = RED;
						cur->_col = BLACK;
					}
					break;
				}
			}
			else
			{
				Node* uncle = grandParent->_left;
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandParent->_col = RED;

					cur = grandParent;
					parent = cur->_parent;
				}
				else
				{
					if (parent->_right == cur)
					{
						RotateL(grandParent);
						grandParent->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						RotateR(parent);
						RotateL(grandParent);
						grandParent->_col = RED;
						cur->_col = BLACK;
					}
					break;
				}
			}
		}
		_root->_col = BLACK;
		return true;
	}

动态效果演示:

1. 以升序插入构建红黑树 

 2. 降序插入构建红黑树

 


6.  红黑树的验证

验证一棵树是否是红黑树,要 检测其是否满足红黑树的性质。
  1. 空树也是红黑树;
  2. 根节点必须是黑色的;
  3. 不能有连续的红色节点,如果当前节点为红色,那么我们去检查他的父亲是否为红色,如果是则证明这棵树不是红黑树,这里为什么不去检查节点的孩子呢?因为节点的孩子是不确定的,我们不知道他是否有左孩子或或者右孩子,但是他一定有父亲节点。
  4. 每条路径上的黑色节点相同,先记录一条路径的黑色节点的个数作为基准值,再统计每条路径的黑色节点的个数进行判断,看是否符合要求。
	bool IsBalance()
	{
		//空树也是红黑树
		if (_root == nullptr)
			return true;
		//根节点的颜色必须为黑色
		if (_root->_col != BLACK)
			return false;
		//拿最左路径的黑色节点,作为每条路径黑色节点的参考值
		int ref = 0;//黑色节点参考值
		Node* cur = _root;
		while (cur)
		{
			if(cur->_col == BLACK)
				ref++;

			cur = cur->_left;
		}
		return _IsBalance(_root,0,ref);
	}
	//用blacknum 记录每条路径的黑色节点的个数
	bool _IsBalance(Node* root,int blacknum,const int& ref)
	{
		if (root == nullptr)
		{
			//每条路径上的黑色节点相同
			if (blacknum != ref)
			{
				cout << "异常:当前路径的黑色节点与参考值不同" << endl;
				return false;
			}

			return true;
		}
		
		//不能有连续的红色节点
		if (root->_col == RED &&  root->_parent->_col == RED)
		{
			cout << root->_Data.first<<"异常:出现了连续的红色节点" << endl;
			return false;
		}

		//统计黑色节点的个数
		if (root->_col == BLACK)
			++blacknum;

		return _IsBalance(root->_left, blacknum, ref)
			&& _IsBalance(root->_right, blacknum, ref);
	}

7. 红黑树与AVL树的比较

        红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(log_2 N),红黑树不追 求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数, 所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红 黑树更多。

8. 红黑树的应用

1. C++ STL -- map/setmutil_map/mutil_set

2. Java 库

3. linux内核

4. 其他一些库

9. 红黑树模拟实现STL中的mapset

 9.1 红黑树的迭代器

迭代器的好处是可以方便遍历,使数据结构的底层实现与用户透明。

        这里我实现的与stl库中的迭代器稍有不同,库中的红黑树添加了一个哨兵位的头节点header,header的左指针指向树中的最小值(最左节点),header的右指针指向树中的最大值(最右节点)。begin()与end()代表的是一段前闭后开的区间而通过中序遍历可以得到一个有序的序列,stl库里的begin()是最左节点,end()是header自己。

 因为我实现的红黑树没有哨兵位的头结点,所以我的begin()先找到最左节点,再用节点构造一个迭代器返回,中序遍历走到根的父亲就结束(表示左子树,根和右子树都已经走完了),所以end()为空。

    typedef __RBtree_Iterator<T,T&,T*> iterator;

    iterator begin()
	{
        //找最左节点
		Node* left = _root;
		while (left->_left)
		{
			left = left->_left;
		}
        //用迭代器构造一个返回
		return iterator(left);
	}
    iterator end()
	{
		return iterator(nullptr);
	}

 那么有了begin()和end(),还差一个迭代器的类,这里要注意的就是++和--,我们以++为例(走的是中序遍历,因为中序遍历会得到一个有序的序列),分两种情况:

1.右不为空,那么说明左子树和根走完了,那么就要找右子树的最小节点,找到后将min的给node,就相当于++走到了下一个;

2.右为空,那么说明子树的左子树,根和右子树都走完了,需要向上找孩子是父亲的左的那个父亲,找到了就把parent给node,就相当于找到这颗子树的父亲,有个特殊情况需要处理,如果已经走到最右节点,那么迭代器再++就该结束了,cur和parent会一直向上更新,直到parent为空,再把parent给node,那么迭代器就结束了。

--只需用++反向推导即可。

template<class T,class Ref,class Ptr>
struct __RBtree_Iterator
{
	typedef RBTreeNode<T> Node;
	typedef __RBtree_Iterator<T,Ref,Ptr> self;

	Node* _node;

	__RBtree_Iterator( Node* node)
		:_node(node)
	{}

	Ref operator*()const
	{
		return _node->_Data;
	}
	Ptr operator->()const
	{
		return &_node->_Data;
	}

	self& operator++()
	{
        //右不为空,就找右边的最左节点
		if (_node->_right)
		{
			Node* min = _node->_right;
			while (min->_left)
			{
				min = min->_left;
			}
			_node = min;
		}
		else//右为空,要向上找孩子是父亲的左的那个父亲
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && parent->_right == cur)
			{
				cur = parent;
				parent = cur->_parent;
			}
			_node = parent;

		}

		return *this;
	}
	self& operator--()
	{
		if (_node->_left)
		{
			Node* mix = _node->_left;
			while (mix->_right)
			{
				mix = mix->_right;
			}
			_node = mix;
		}
		else
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && parent->_left == cur)
			{
				cur = parent;
				parent = cur->_parent;
			}
			_node = parent;
		}
		return *this;
	}
	
	bool operator!=(const self& node) const
	{
		return _node != node._node;
	}
	bool operator==(const self& node) const
	{
		return _node == node._node;
	}
};

 9.2 改造红黑树

        因为我们后面要用红黑树一个类封装出map和set两个对象,所以要对红黑树的结构稍作修改。

        在修改红黑树之前先把map和set的插入实现一下,直接复用红黑树的插入,因为这里map和set 的结构不大一样,用一个insert封装两个对象,还需要有仿函数来实现不同的情况,这里的仿函数主要是插入时会用key进行比较,如果是set直接返回key,如果是map就返回pair里的k。因为Data并不知道自己的类型是key还是pair<K,V>,所以要用仿函数多套一层。
    template<class K,class V>
	class map
	{
		struct MapKeyOfT
		{
            //提取key值
			const K& operator()(const pair<const K, V>& kv)
			{
				return kv.first;
			}
		};
        bool insert(const pair<const K, V>& kv)
		{
			return _t.Insert(kv);
		}
	private:
		RBTree<K, pair<const K,V>, MapKeyOfT> _t;
	};
template<class K>
	class set
	{
		struct SetKeyOfT
		{
            //提取key值        
			const K& operator()(const K& key)
			{
				return key;
			}
		};
        bool insert(const K& key)
        {
			return _t.Insert(key);
		}
	private:
		RBTree<K, K, SetKeyOfT> _t;
	};
关联式容器中存储的是<key, value>的键值对,因此 k  为key的类型,value我用T来表示
 
           T:  如果是map,则为pair<K, V>;   如果是set,则为key,
 KeyOfT:  通过T来获取key的一个仿函数类。
​
template<class T>
struct RBTreeNode
{
	RBTreeNode(const T& Data)
		: _Data(Data)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_col(RED)			//默认是红色
	{}

	T _Data;
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;
	Color _col;					//节点的颜色
};

​
template<class K,class T,class KeyOfT>
class RBTree
{
public:
	typedef RBTreeNode<T> Node;
	typedef __RBtree_Iterator<T,T&,T*> iterator;
	typedef __RBtree_Iterator<T, const T&,const T*> const_iterator;

public:
    iterator begin()
	{
		Node* left = _root;
		while (left->_left)
		{
			left = left->_left;
		}

		return iterator(left);
	}
	const_iterator begin()const
	{
		Node* left = _root;
		while (left->_left)
		{
			left = left->_left;
		}

		return const_iterator(left);
	}

	iterator end()
	{
		return iterator(nullptr);
	}
	const_iterator end()const
	{
		return const_iterator(nullptr);
	}
private:
	Node* _root = nullptr;

        那么有了仿函数还需要对插入做一些修改,先实例化一个仿函数对象,再把以值进行比较的地方做一点修改。

         那么插入实现了,得看看插入的值对不对呀,所以再把map和set的迭代器再封装一个。这里有个前提条件是不能通过迭代器去修改map和set中的key值,因为修改以后他这个红黑树可能就不符合要求了,所以使用的时候是不能通过迭代器去修改key值,但是map可以通过迭代器去修改second的值,因为map要进行统计。

        看下图,stl库中set的迭代器底层全都是const迭代器,而map中普通就是迭代器,const就是const。既然map的迭代器是正常的,那为什么map中的key值也不能修改呢?那是因为在定义的时候pair里的key值就加了const,所以不能修改,大家可以试一试把pair里的const去掉看一看key值能不能修改。

        在set这里没有提供两个版本的迭代器,只提供了普通的迭代器,但这个普通迭代器底层也是const迭代器。为什么他在这里给函数加了一个const就行了?因为函数加了const,const对象可以调用,普通对象也可以调用,普通对象调用权限缩小,就让这里的 t强行变成const对象,那么const对象调用这个迭代器也就可以了。

 当然map就需要实现两个版本喽。

	class set
	{

	public:
		typedef typename RBTree<K, K, SetKeyOfT>::const_iterator iterator;
		typedef typename RBTree<K, K, SetKeyOfT>::const_iterator const_iterator;

		iterator begin()const
		{
			return _t.begin();
		}
		iterator end()const
		{
			return _t.end();
		}
	private:
		RBTree<K, K, SetKeyOfT> _t;
    };
class map
	{
	
	public:
		typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::iterator iterator;
		typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::const_iterator const_iterator;


		iterator begin()
		{
			return _t.begin();
		}
		iterator end()
		{
			return _t.end();
		}
		const_iterator begin()const
		{
			return _t.begin();
		}
		const_iterator end()const
		{
			return _t.end();
		}
private:
		RBTree<K, pair<const K,V>, MapKeyOfT> _t;
	};

 通过上面一顿输出,set和map就可以简单使用啦!


        那么到这map还想通过[ ]实现统计次数,那么库中map的[ ]是通过插入k值,插入后返回一个带有迭代器的pair,然后pair取到迭代器,再通过迭代器取到second,然后返回。

         [ ]在前面的文章里讲过,如果这个值不存在那么就插入成功,如果存在就返回所在节点的迭代器。要支持这个[ ]就需要对insert的返回类型和返回值进行修改,返回类型不用说,一样的,返回值就用当前节点构造一个迭代器返回。

	 pair<iterator,bool> Insert(const T& Data)
	{
		if (_root == nullptr)
		{
			_root = new Node(Data);
			_root->_col = BLACK;
			return make_pair(iterator(_root),true);//通过root节点构造一个迭代器
		}
		KeyOfT kot;
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (kot(cur->_Data) > kot(Data))
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (kot(cur->_Data) < kot(Data))
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return make_pair(iterator(cur), false);
			}
		}
		
		cur = new Node(Data);
		Node* newnode = cur;//保存新插入的节点,后面要用这个节点构造迭代器,cur会随着循环丢失位置
		cur->_col = RED;
		if (kot(parent->_Data) > kot(Data))
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_right = cur;
			cur->_parent = parent;

		}
		//如果parent的颜色是红色那么就继续调整,是黑色就结束
		while ( parent && parent->_col == RED)
		{
			Node* grandParent = parent->_parent;

			if (parent == grandParent->_left)
			{
				Node* uncle = grandParent->_right;
				//情况一
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandParent->_col = RED;

					cur = grandParent;
					parent = cur->_parent;
				}
				else
				{
					//情况二
					if (parent->_left == cur)
					{
						RotateR(grandParent);
						grandParent->_col = RED;
						parent->_col = BLACK;
					}
					else //情况三
					{	
						RotateL(parent);
						RotateR(grandParent);
						grandParent->_col = RED;
						cur->_col = BLACK;
					}
					break;
				}
			}
			else
			{
				Node* uncle = grandParent->_left;
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandParent->_col = RED;

					cur = grandParent;
					parent = cur->_parent;
				}
				else
				{
					if (parent->_right == cur)
					{
						RotateL(grandParent);
						grandParent->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						RotateR(parent);
						RotateL(grandParent);
						grandParent->_col = RED;
						cur->_col = BLACK;
					}
					break;
				}
			}
		}
		_root->_col = BLACK;
		return make_pair(iterator(newnode), true);
	}

        insert修改完成后,再把[ ]实现了,这里红黑树结构里不用实现[ ],在map里封装一个就可以。在[ ]里调用insert,插入key值和value,value就用匿名函数(调用自己的默认构造),返回时先取到迭代器,再用迭代器找second。那么map里的insert也相应的修改一下。

		//map
        pair<iterator, bool> insert(const pair<const K, V>& kv)
		{
			return _t.Insert(kv);
		}
		V& operator[](const K& key)
		{
			 pair<iterator, bool> ret = insert(make_pair(key, V()));
			 return ret.first->second;
		}

        set这里如果再像map一样写那么就会报错,为什么呢?还是那个原因,普通对象调用const迭代器当然会出错,所以这里不能直接用iterator,而要用红黑树里的iterator,先接收再构造一个pair返回。

		//set
  		pair<iterator, bool> insert(const K& key)
		{
			//return _t.Insert(key);//会报错
			pair<typename RBTree<K, K, SetKeyOfT>::iterator, bool> ret = _t.Insert(key);

			return pair<iterator, bool>(ret.first, ret.second);
		}

        如果直接像map一样 ,就会出现下图的报错,无法把普通对象转换为const对象。

         那么要想让set的insert正常使用还需要给红黑树里的迭代器类里加下面这样的函数。下面的函数是什么意思呢?如果你是普通迭代器那么就是一个拷贝构造,如果你是一个const迭代器那么就是一个构造函数,用普通迭代器构造一个const迭代器。

	//参数不是self而是迭代器
    typedef __RBtree_Iterator<T, T&, T*> iterator;
	__RBtree_Iterator(const iterator& s)
		:_node(s._node)
	{}

 这里统计次数就也能用了。

注:这里进行修改时,要一步一步的来,肯定会有很多问题出现,而模板的报错又特别恶心,所以要多注意。

完整代码:RBTree/RBTree/RBTree.h · 晚风不及你的笑/作业库 - 码云 - 开源中国 (gitee.com)

猜你喜欢

转载自blog.csdn.net/weixin_68993573/article/details/129031976