对红黑树的理解与实现(C++实现)

认识红黑树

在看到此篇文章之前最好还是先了解一下左右旋也就是AVL树的插入数据该如何处理。AVL树的插入详解-CSDN博客

红黑树,也属于是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是红色(red)或黑色(black)。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。相较于AVL树而言红黑树就没有那么繁琐,也就是旋转的频率没有这么高,但是依旧可以保证查找数据和插入删除数据的效率在O(log2n)的时间复杂度。归于红黑树的特征限制:

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点是黑色。
  3. 叶子节点(NIL节点)是黑色的。(此叶子结点指的是空节点)
  4. 如果一个节点是红色的,则它的两个子节点都是黑色的。(不能有连续的两个红节点)
  5. 对于每个节点,从该节点到其后代叶子节点的所有路径上,包含相同数量的黑色节点。

为了保证以上的特征使得我们的红黑树确保没有一条路径会比其他路径长出两倍。由于每条路径(从根节点到空节点才是一条路径)的黑色节点数目都相同,所以我们可以假设每条路径的黑色节点个数为N,而且不能出现连续的黑节点,且根节点为黑,所以从根到叶子最长的情况就是黑红黑红...所以最长节点数也就是2N,因此可以轻松得出:每条路径的节点数目在范围:[N,2N]之间。

红黑树和AVL树

红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(log_2 N),红黑树不追求绝对平衡,红黑树主要采用变色和旋转而达到满足红黑树的条件,其只需保证最长路径不超过最短路径的2倍。

而AVL树的要求更为严格,要保证任何节点的左右子树高度差为1,所以一旦高度差为2时就要开始进行旋转。

相对而言,红黑树降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,但是可能相对没有AVL树查找数据时那么高效但是此高效相较于不断地旋转而言也可以忽略,所以实际运用中红黑树更多。

红黑树的定义

enum colar//枚举类型
{
	red,
	black
};

template<class K, class V>
struct RBTreeNode
{
	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _co(red)//
	{}
    //三叉链
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;

	pair<K, V> _kv;//实际数据
	colar _co;//颜色
	
};

数据插入 

首先我们要知道插入数据是按照搜索树的方式进行插入的,但是对于二叉树我们关键的是颜色处理,所以我们对于新的节点初始化就要考虑是红还是黑,这就要考虑红黑树的性质了,我们对于要插入数据之前的红黑树肯定是满足性质的,也就是每条路径的黑色节点个数相同,但是对于新插入的节点如果初始为黑色的话,那么肯定是直接打乱了整个红黑树的结构,会导致每条路径的黑色节点个数不同,所以我们采用插入节点都按照初始为红色节点的方式进行插入,所以此时就得判断新插入的节点对应的父节点是红还是黑,如果是黑节点则表明该插入没毛病,但是如是红结点的话就需要进行处理。

    bool insert(const pair<K, V>& kv)
	{
		Node* newroot = new Node(kv);//默认为红
		if (_root == nullptr)
		{
			_root = newroot;
			return true;
		}
		Node* parent = _root, * cur = _root;
		//插入数据
		while (1)
		{
			parent = cur;
			if (cur->_kv.first > kv.first)
			{
				cur = cur->_left;
				if (cur == nullptr)
				{
					parent->_left = newroot;
					newroot->_parent = parent;
					break;
				}
			}
			else if (cur->_kv.first < kv.first)
			{
				cur = cur->_right;
				if (cur == nullptr)
				{
					parent->_right = newroot;
					newroot->_parent = parent;
					break;
				}
			}
			else
			{
				return false;
			}

		}
        //判断父节点
        ……
    }

插入节点的父节点为黑

我们知道当父亲节点是黑色的话,而插入节点默认初始化都是红色,所以此次插入既不会影响路径的黑色节点数目,也不会出现连续的红节点,所以此时直接插入就可以。

插入节点的父节点为红

此时我们就要分析一下,当我们所插入节点的父亲也是红色的话,且我们知道插入前该树肯定是符合红黑树的性质的,那么此时我们插入节点的祖父节点肯定存在并且是黑色的(因为父亲节点是红色,且不会有连续的红色节点,而且根节点为黑)而此时取决定性因素的节点其实就是叔叔节点(也就是祖父节点的另一个子节点)


叔叔节点为红

当我们插入的节点的叔叔节点为红色节点的话,此时的处理就比较轻松。1.先将父亲节点置为黑,同时也将叔叔节点置为黑,2.将组父节点置为红,3.将当前节点指向祖父节点再次循环判断。

此时我们要了解为什么第二步要将组父节点置为红??其实主要就是因为红黑树要保证每条路径的黑色节点数目不变,所以在插入数据前后,我们的黑色节点个数不能发生改变。因此要将组父节点置为红,但是祖父节点的父亲是否为黑色节点或者红色节点,所以此时属于循环判断,也就是继续向上判断调整。


   叔叔节点为黑

这种情况相较于上一种的区别就是叔叔节点为黑的,但是你是否会疑惑为什么会有这种情况,这种情况下会导致每条路径不满足有相同个数的黑节点,但是这种情况虽然在刚插入数据的时候不可能发生,但是在插入数据之后就极有可能发生该种情况:如下

此时如果再像第一种情况一样只采用变色处理的话是远远不够的,因为,无论怎样变都无法保证插入节点前与变色处理后的每条路径黑色节点数目相等。所以此时根据AVL树插入数据的方式就不难发现可以采用旋转的方式进行节点的颜色处理。

此时显然采用单旋就能够轻松解决问题,而此时得结构特征是pparent->_left == parent && parent->_left == cur也就是我们所了解到的直线型,所以此时采用单旋并且将新的根节点设为黑,两个子节点设为红。此时与插入节点前的路径黑节点数保持一致,显然是完全没毛病的。但是貌似也可以将根设为红,两个子节点设为黑,但是此时无疑就复杂了很多,那么就还得继续向上判断调整,而且如果当新的根就是整个数的根的话,最后还得再进行调整回来。


有了单旋的情况自然就少不了双旋(折线型):

双旋其实就是两次单旋,所以说理解了单旋,双旋自然也就直接调用即可,而此时的节点颜色变化发生了一些改变,在第一次进行左单旋时,显然并没有反生节点颜色变化,依旧还是两个连续的红节点,但是在旋转过后,直接就变成了右单旋的情况,此时cur节点变成了新的根节点(变黑),而pparent变成了其中一个子节点(变红)

左右单旋代码

	void RotateL(Node* parent)//左单旋
	{
		Node* cur = parent->_right;
		Node* curl = cur->_left;
		Node* pparent = parent->_parent;//提前记录

		parent->_right = curl;
		if (curl)
		{
			curl->_parent = parent;
		}
		cur->_left = parent;
		parent->_parent = cur;

		//处理pparent与parent的连接
		if (_root == parent)//根旋转
		{
			_root = cur;
			cur->_parent = nullptr;//别忘了将根节点的parent置空防止遍历造成死循环
		}
		else//非根节点旋转
		{
			if (pparent->_left == parent)
				pparent->_left = cur;
			else
				pparent->_right = cur;
			cur->_parent = pparent;
		}

	}
	void RotateR(Node* parent)//右单旋
	{
		{
			Node* cur = parent->_left;
			Node* curr = cur->_right;
			Node* pparent = parent->_parent;//提前记录

			parent->_left = curr;
			if (curr)
			{
				curr->_parent = parent;
			}
			cur->_right = parent;
			parent->_parent = cur;

			//处理pparent与parent的连接
			if (_root == parent)
			{
				_root = cur;
				cur->_parent = nullptr;
			}
			else
			{
				if (pparent->_left == parent)
					pparent->_left = cur;
				else
					pparent->_right = cur;
				cur->_parent = pparent;
			}

		}
	}

插入数据的全部代码

	bool insert(const pair<K, V>& kv)
	{
		Node* newroot = new Node(kv);//默认为红
        //为空就直接插入
		if (_root == nullptr)
		{
			_root = newroot;
			_root->_co = black;//设为黑
			return true;
		}

		Node* parent = _root, * cur = _root;
		//插入数据(搜索树的插入)
		while (1)
		{
			parent = cur;
			if (cur->_kv.first > kv.first)
			{
				cur = cur->_left;
				if (cur == nullptr)
				{
					parent->_left = newroot;
					newroot->_parent = parent;
					break;
				}
			}
			else if (cur->_kv.first < kv.first)
			{
				cur = cur->_right;
				if (cur == nullptr)
				{
					parent->_right = newroot;
					newroot->_parent = parent;
					break;
				}
			}
			else
			{
				return false;
			}

		}

		//父节点的判断(为黑1直接插入,为红还需调整)
		cur = newroot;//当前节点就是新插入的节点

		while (parent && parent->_co == red)//父亲节点可能不存在(防止空指针的解引用)
		{
			Node* pparent = parent->_parent;//parent为红,不可能是根,一定存在pparent节点
			Node* uncle = nullptr;
			//找叔叔节点(决定着连续的两个红色节点是该变色还是旋转)
			if(pparent->_right==parent)
				uncle = parent->_parent->_left;
			else
				uncle = parent->_parent->_right;

			if (uncle&&uncle->_co == red)//叔叔存在且为红
			{
				//变色
				parent->_co = uncle->_co = black;
				pparent->_co = red;//祖父节点有可能是根节点
				//继续向上更新处理
				cur = pparent;
				parent = cur->_parent;
			}
			else//叔叔节点不存在或为黑色
			{
				//旋转
				if (pparent->_left == parent && parent->_left == cur)
				{
					//右单旋
					RotateR(pparent);
					parent->_co = black;
					pparent->_co = red;
				}
				else if (pparent->_right == parent && parent->_right == cur)
				{
					//左单旋
					RotateL(pparent);
					parent->_co = black;
					pparent->_co = red;
				}
				else if (pparent->_right == parent && parent->_left == cur)
				{
					//右左双旋
					RotateR(parent);
					RotateL(pparent);
					cur->_co = black;
					pparent->_co = red;
				}
				else if (pparent->_left == parent && parent->_right == cur)
				{
					//左右双旋
					RotateL(parent);
					RotateR(pparent);
					cur->_co = black;
					pparent->_co = red;
				}
				break;//旋转之后新的根节点都是黑色,所以直接不用再向上判断调整了
			}
	
		}

		_root->_co = black;//循环体内(叔叔为黑节点时)很有可能将根节点改为红
		return true;
	}

平衡判断

我们判断我们插入的树是否为红黑树,主要就是取决于是否满足红黑树的所有性质:

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点是黑色。
  3. 叶子节点(NIL节点)是黑色的。(此叶子结点指的是空节点)
  4. 如果一个节点是红色的,则它的两个子节点都是黑色的。(不能有连续的两个红节点)
  5. 对于每个节点,从该节点到其后代叶子节点的所有路径上,包含相同数量的黑色节点。

对于以上的性质,主要就是判断4和5两个性质,所以我们要实现一个函数判断该树是否有连续的两个红节点,而且每条路径的黑节点个数是否相等。

	// 根节点->当前节点这条路径的黑色节点的数量
	bool Check(Node* cur,int blacknum,int ref_val)
	{
		if (cur == nullptr)
		{
			if (blacknum == ref_val)
				return true;
			cout << "每条路径的黑色节点个数不同" << endl;
			return false;
		}
		Node* parent = cur->_parent;
		if (cur->_co == red && parent->_co == red)//向上判断,向下判断的节点可能为空或其它的。
			return false;
		if (cur->_co == black)
			blacknum++;

		return Check(cur->_left,blacknum,ref_val) && Check(cur->_right,blacknum,ref_val);
	}
	bool Is_balance()
	{
		if (_root->_co == red)
			return false;
		if (_root == nullptr)
			return true;

		//不能出现连续红节点
		//每条路径黑色节点要保证相同
		int blacknum = 0;//必须传值,相当于是每个节点都有一个变量表示从根到当前的黑节点个数
		int ref_val = 0;//参考值,求出任意一条路径中黑色节点数目
		Node* cur = _root;
		while (cur)
		{
			if (cur->_co == black)
				ref_val++;
			cur = cur->_left;
		}
		return Check(_root,blacknum,ref_val);
	}

 实现的如果有问题欢欢迎留言!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

高居沉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值