红黑树:比AVL抽象、自由的、更广泛的近似平衡树

RBT与AVL树的比较

  1. AVL:高度要求差不超过1
  2. 红黑树:RBT要求最长路径不超过短路径的2倍,不需要像AVL一样太平衡,稍微自由,所以旋转较少。
  • AVL和RBT树性能比较:
    插入同样的数据,AVL树旋转更多,红黑树旋转更少。从查找来说,红黑树略慢,100W红黑树2LogN,需要40次,而AVL100W需要20次,而10亿量级节点时,两者分别是60次和30次。对计算机来说,根本没事。
    有人称,AVL树是天才设计,而红黑树是由天才加大佬设计的。

RTB:红黑树

概念:符合二叉搜索树且以属性为红色、黑色的节点组成的树,因此称为红黑树。
红黑树的性质(必须满足以下5点)

  1. 每个节点不是红就是黑。
  2. 根节点是黑色的。
  3. 红色节点的孩子都黑。
  4. 从任意固定节点到后代所有叶子节点的路径,黑色节点数量相同。
  5. 叶子都是黑色。(这里的叶子节点指的是空节点NIL)。传统叶子节点如果是黑色,假设两个节点时,有三条路径,如下图,不满足条件4。所以这里的意思是,不是说传统叶子节点,而是空叶子节点必须黑。如下面那张大图。
    请添加图片描述

请添加图片描述

思考:为什么满足上面条件就能保证:最长路径不超过短路径的2倍?

  答:因为性质4(任意节点起到叶子的路径上,黑色节点数量少),假设从根到叶子的所有路径上黑色节点有N个,而最短路径最短为N个黑色节点,最长路径满足黑色N个,所以是红黑红黑…,黑色节点和红色节点数量相同,长度为2N。所以红黑树中,根到叶子最长路径不能超过最短路径两倍。

红黑树实现

红黑树的节点定义:

每个节点需要存KV对,而KV需要使用模板。此外,每个节点和AVL树的节点一样,只是需要多一个红黑属性。每个节点默认给红色属性。此外,枚举类型定义颜色。

template<class K, class V>
struct RBTreeNode
{
	//三叉链
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;

	//存储的键值对
	pair<K, V> _kv;

	//结点的颜色
	int _col; //红/黑

	//构造函数
	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _col(RED)
	{}
};

//枚举定义结点的颜色
enum Colour
{
	RED,
	BLACK
};

为什么默认节点颜色为红色?

  1. 如果插入黑色,那么路径上黑色节点数目就多余其它路径,破坏性质4,需要对红黑树调整。
  2. 如果插入红色,破坏性质3,需要调整;但是如果父亲是黑色,那就不需要调整。
  3. 综上,插入红色更方便,总体上调整会少一点。

红黑树的定义

对于一个RBT,主要是给一个根节点。

template <class K, class V>
class RBTree
{
    typedef RBTreeNode<K, V> Node;
public:
    bool Insert(const pair<K, V>& kv){};


private:
    RBTreeNode<K, V>* _root = nullptr;
};

RBT的函数

insert(): 参数:const pair<K, V>& kv, 给红黑树插入的是pair<k, v>,而插入函数返回的类型是:pair<Node*, bool>,意思是告诉你当前这个节点是否插入成功,返回方式是:利用make_pair(_root, true\false);

  • 步骤:
  1. 按BST思路,找合适位置:需要区分是否为根节点。
  2. 插入树中,且需要父节点
  3. 插入节点的父亲节点是红色,则需要调整(性质3,不能有连续红)。
  4. 如果插入节点的父亲是黑,没有破坏性质,不需要调整。
  • 调整的三种情况:(性质3较性质4容易维护)以下都是因为出现连续红节点而出现的调整。红黑树得看叔节点情况

情况1. 插入节点的父亲为红,叔叔存在也为红。

因为不能有连续红,把父和叔都变黑,再把祖父变红。相当于,p、u两条路径上多了黑色,而让g变红,和其它路径也保持了黑色节点数量相同。然后继续以祖父为cur(假设它是新插入的红色),继续向上观察它父亲是否也是红色,判断是否需要做因出现连红的现象而调整的操作。只变色即可,且不换位置,不用管p、u相对位置
但是如果祖父节点是根节点,则需要变回黑色
请添加图片描述

情况2. 插入节点叔叔存在,叔叔为黑

状态一:祖父、父、儿节点为直线, **当然也可以是向右的一条直线 **。
请添加图片描述状态二:祖父、父、儿节点为折线,当然,也可以是g右是p,然后向左是cur请添加图片描述
  如上述两种状态的第一张图所示,叔叔为黑,这种情况一定是在红黑树插入不理想而做向上调整过程中出现的。

  • 证明如下:

假设给p插入了红色, 造成的该连红局面。设祖父g节点之前,有黑点x个,叔叔之下有黑点y个,则在插入节点的那边cur一路加上g,共有x+1个黑点,而右边一路有x+2(g和u)+y个节点,即使y为0,也说明在插入cur时,各个路上的黑点数量不等,所以情况2一定是向上调整过程中产生的不理想情况
请添加图片描述
调整:
  状态一时,先以祖父g的右旋,再还是变父p和叔u为黑,祖父g为红;如果是状态一的右直线,先左旋g,再变父p和叔u为黑,祖父为红。
  状态二时,以父p节点做左旋,再以祖父g做右单旋,把儿cur变黑,祖父变红;而是向右的折线,先右旋p,再左旋g。

注意:情况2时,调整完就可停止向上。

情况3. 插入节点叔叔不存在

状态一:祖父、父亲、孩子 直线
请添加图片描述
调整:
  先以祖父g右单旋,再变色p为黑。

状态二:祖父、父亲、孩子 折线时
请添加图片描述
调整:
  先以父p左单旋,再以祖父g右单旋,再变cur为黑色,祖父变红。
到这里发现情况2和情况3的折线都需要旋转两次。

  • 注意:该情况一定是新插入的节点cur,而不是情况1调整便来的。
    请添加图片描述
    因为叔不存在,祖父到null路径上就一个黑色节点,而父p下面一定没有黑色节点了,不然cur之前是祖父,祖父一定有叔叔,且叔父都是黑色。

总结:
上面情况我们发现,需要调整的情况中:

  1. 情况1:当叔叔节点和父亲节点为红,我们需要同时变黑后使祖父变红,继续向上更新,是根则停止。
  2. 而情况2、3可以写为一类,因为直线不管是情况2、还是情况3,做法一样,而折线或直线的情况2和情况3,做法一样,我们的情况2、情况3都有直线和折线的两种。uncle在情况2中不涉及变色,所以它不存在也一样的操作。 折线最终都是cur黑,p、g为红。直线最终都是p黑,cur和g红。此外,直线都旋转一次,而折线旋转两次。
  3. 但又因为折线和直线各自又有向左和向右的区别,所以还是分了4种。所以代码和情况1相对的else是以直线和折线、向左和向右区分的4种情况。
//插入函数
pair<Node*, bool> Insert(const pair<K, V>& kv)
{
	if (_root == nullptr) //若红黑树为空树,则插入结点直接作为根结点
	{
		_root = new Node(kv);
		_root->_col = BLACK; //根结点必须是黑色
		return make_pair(_root, true); //插入成功
	}
	//1、按二叉搜索树的插入方法,找到待插入位置
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (kv.first < cur->_kv.first) //待插入结点的key值小于当前结点的key值
		{
			//往该结点的左子树走
			parent = cur;
			cur = cur->_left;
		}
		else if (kv.first > cur->_kv.first) //待插入结点的key值大于当前结点的key值
		{
			//往该结点的右子树走
			parent = cur;
			cur = cur->_right;
		}
		else //已经存在
		{
			return make_pair(cur, false); //插入失败
		}
	}

	//2、将待插入结点插入到树中
	cur = new Node(kv); //根据所给值构造一个结点
	Node* newnode = cur; //记录新插入的结点(便于后序返回)
	if (kv.first < parent->_kv.first) //新结点的key值小于parent的key值
	{
		//插入到parent的左边
		parent->_left = cur;
		cur->_parent = parent;
	}
	else //新结点的key值大于parent的key值
	{
		//插入到parent的右边
		parent->_right = cur;
		cur->_parent = parent;
	}

	//3、颜色调整:当新插节点默认是红,且父也红,连续红色则调整

	while (parent && parent->_col == RED) {	// 条件一定是父亲存在且父也红
		Node* grandfather = parent->_parent;
		Node* uncle = nullptr;				// 定位uncle,根据父位判断叔
		if (parent == grandfather->_left)
			uncle = grandfather->_right;
		else
			uncle = grandfather->_left;

		// 情况1:叔存在且红
		if (uncle && uncle->_col == RED) {
			// 叔叔存在且为红
			parent->_col = BLACK;
			uncle->_col = BLACK;

			grandfather->_col = RED;
			cur = grandfather;
			parent = cur->_parent;
		}
		else {
			// 情况2+3:叔叔不存在或者叔叔存在且为黑
			if (parent == grandfather->_left && cur == parent->_left)	// 左直线
			{	
				// 此时,左左,右单旋+变色
				// 先变色也可以
				parent->_col = BLACK;
				grandfather->_col = RED;
				RotateR(grandfather);
			}
			else if (parent == grandfather->_right && cur == parent->_right) {	// 右直线
				// 右右,左单旋
				parent->_col = BLACK;
				grandfather->_col = RED;
				RotateL(grandfather);
			}
			// 折线情况下,因为情况3的uncle不存在,且情况2uncle颜色不变
			// 此外,情况2、3的折线翻转再变色后相同逻辑位置的cur、p、g最终颜色也一样 
			else if (parent == grandfather->_right && cur == parent->_left) {	
				// cur为红,parent为红,grandfather为黑。
				// 右左双旋。
                   RotateR(parent);
                   RotateL(grandfather);
                   // 记住这里是上黑,下面俩红即可。
                   cur->_col = BLACK;
                   grandfather->_col = RED;

			}
			else if (parent == grandfather->_left && cur == parent->_right) {
				RotateL(parent);
				RotateR(grandfather);
				// 记住这里是上黑,下面俩红即可。
				cur->_col = BLACK;
				grandfather->_col = RED;
				/*RotateL(parent);
				std::swap(cur, parent);
				parent->_col = BLACK;
				grandfather->_col = RED;
				RotateR(grandfather);*/
			}
			break;
		}
		// 当前是根才做 直接做也行
		if (cur == _root) {
			cur->_col = BLACK;
		}
	}
	return make_pair(newnode, true);
}
	

//左单旋
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* parentParent = parent->_parent;

	//建立subRL与parent之间的联系
	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;

	//建立parent与subR之间的联系
	subR->_left = parent;
	parent->_parent = subR;

	//建立subR与parentParent之间的联系
	if (parentParent == nullptr)
	{
		_root = subR;
		_root->_parent = nullptr;
	}
	else
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subR;
		}
		else
		{
			parentParent->_right = subR;
		}
		subR->_parent = parentParent;
	}
}

//右单旋
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* parentParent = parent->_parent;

	//建立subLR与parent之间的联系
	parent->_left = subLR;
	if (subLR)
		subLR->_parent = parent;

	//建立parent与subL之间的联系
	subL->_right = parent;
	parent->_parent = subL;

	//建立subL与parentParent之间的联系
	if (parentParent == nullptr)
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subL;
		}
		else
		{
			parentParent->_right = subL;
		}
		subL->_parent = parentParent;
	}
}

//左右双旋
void RotateLR(Node* parent)
{
	RotateL(parent->_left);
	RotateR(parent);
}

//右左双旋
void RotateRL(Node* parent)
{
	RotateR(parent->_right);
	RotateL(parent);
}

find(): 参数:const K& key (按key值查找)

  1. 树空,则返nullptr,按左右大小比较的方式走到了空,说明没有这个值。
  2. 只要当前存在,就按key找。
Node* Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (key < cur->_kv.first)	// 小就给左
			cur = cur->_left;
		else if (key > cur->_kv.first)	// 大就给 右
			cur = cur->_right;
		else
			return cur;	// 返回该节点
	}
	return nullptr;
}

红黑树的验证:

  1. 首先它是BST树,所以中序是有序的。
  2. 判断是否是红黑树:通过基本的5条性质来判断。
    其中,关于判断性质3不能有连续红节点和性质4黑色节点数不相等的方法。

  红黑树判定子函数

  1. 如果当前到了nullptr,说明到某条空叶路径,且它算黑的,但这里代码中,之前到了空,没算上,所以下面子函数中求黑色节点,也不计算这个。发现数量不对应,就返回错误,否则返回true。
  2. 当前节点,如果当前节点是红而父节点也红,则返回false。因为不符合性质3
  3. 当前颜色是黑,则计算一下
  4. 返回递归左孩子和递归右孩子的情况。
//判断是否为红黑树
//中序遍历
	void Inorder()
	{
		_Inorder(_root);
	}

	// 判断RBTree:性质1、调用性质3、4
	bool ISRBTree()
	{
		if (_root == nullptr)
			return true;
		if (_root->_col == RED)
		{
			cout << "根红error" << endl;
			return false;
		}
		// 以最左路径的黑节点为参考,虽然可能最左是错的,但是我们只关注所有的路径上黑色节点数是否一致
		Node* cur = _root;
		int blackcount = 0;
		while (cur)
		{
			if (cur->_col == BLACK)
				blackcount++;
			cur = cur->_left;
		}
		int count = 0;	// 根为黑,以根起始,看每条路径
		return _isRBTTree(_root, count, blackcount);
	}

private:
	//中序遍历子函数
	void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;
		_Inorder(root->_left);
		cout << root->_kv.first << " ";
		_Inorder(root->_right);
	}
	
	// 红黑树的判断
	bool _isRBTTree(Node* root, int cur_balck, int total_balck)
	{
		if (root == nullptr)	// 当前走到空,判断这条路径钟点是否黑点一样多
		{
			if (cur_balck != total_balck)
			{
				cout << "黑子数量不同" << endl;
				return false;
			}
			return true;
		}
		// 顺便看 性质3:不能连红:当前红,一定有父亲,因为根不能红,不必判断父存在
		if (root->_col == RED && root->_parent->_col == RED)
		{
			cout << "连续红错误" << endl;
			return false;
		}
		// 当前还没到叶子,如果当前黑,则计数++
		if (root->_col == BLACK)
			cur_balck++;
		return _isRBTTree(root->_left, cur_balck, total_balck) && _isRBTTree(root->_right, cur_balck, total_balck);
	}

效果:
请添加图片描述

红黑树和AVL树的比较

  • 行为属性上:
    红黑树是较为自由的AVL树,因为它没有严格要求左右高度差,只是控制节点颜色,让最长路径别超最短2倍。
  • 效率上:
    AVL树复杂在旋转多,而红黑树降低了插入的旋转,在增、删操作中比AVL树更有,使得红黑树应用更广泛
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值