数据结构进阶——红黑树

数据结构进阶------红黑树

  • [1. 红黑树的概念](#1. 红黑树的概念)
  • [2. 红黑树的性质](#2. 红黑树的性质)
  • [3. 红黑树节点的定义](#3. 红黑树节点的定义)
  • [4. 红黑树的插入](#4. 红黑树的插入)
  • [5. 红黑树的验证](#5. 红黑树的验证)
  • [6. 红黑树的删除](#6. 红黑树的删除)
  • [7. 红黑树完整代码+测试](#7. 红黑树完整代码+测试)

1. 红黑树的概念


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


2. 红黑树的性质


  • 1. 每个结点不是红色就是黑色
  • 2. 根节点是黑色的
  • 3. 如果一个节点是红色的,则它的两个孩子结点是黑色的
  • 4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
  • 5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点,是红黑树特有的NIL节点)

其中,第5条性质,是为了帮大家更好的理解性质4,举个例子:下面这棵树符合红黑树的定义吗?

很显然,性质1,2,3都是符合的,但是性质4符合吗?所有路径上,是否有相同数量的黑色节点?是不是大眼一看感觉也符合性质4,别急,下面我们把NIL节点也带上:

可以看到,用蓝色线条画出的这条路径 ,是不是很容易被忽略?从根节点向下看 ,其他路径上的黑色节点数都是2个(不算NIL节点),唯独蓝色线路上的黑色节点只有1个,所以上面这棵树是不符合红黑树定义的,它不是红黑树!

满足了如上5条性质,就可以保证,最长路径不超过最短路径的两倍,但是这是为什么?感兴趣的同学可以下去思考一下,只能说,这是天才的设计,很抽象,从学习者的角度来讲,只要能使用并控制红黑树的结构,这就够了,没必要向天才看齐。


3. 红黑树节点的定义

下面我们先实现一颗kv结构的红黑树。


cpp 复制代码
// 颜色定义
enum Colour
{
	RED,
	BLACK
};

// 红黑树节点
template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;		// 左节点
	RBTreeNode<K, V>* _right;		// 右节点
	RBTreeNode<K, V>* _parent;		// 父节点
	pair<K, V> _kv;					// 节点保存的数据
	Colour _col;					// 节点颜色

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

4. 红黑树的插入


1. 思考,如果我们要新插入一个节点,这个节点应该是红色还是黑色?

  • 先假设我们插入黑色节点,这会导致什么?会导致一整条路径上的黑色节点数量+1,这代价是不是太大了,我们要进行很多调整结构的操作,使得树重新满足性质4。

  • 如果我们插入红色节点呢?

    • 如果新节点的父节点是黑色的 ,那么就结束了 ,不用做什么其他调整,就算插入成功了 ,因为这完全不会影响树的整体结构。
    • 如果新节点的父节点是红色 ,那么为了满足性质3(红色节点的子节点必须是黑色 ),就需要做出一些调整。
  • 单单分析到这,就发现,插入红色节点,要比插入黑色节点代价小的多,很多情况下竟然只需要变色,就可以使红黑树结构平衡,而不用调整节点间的父子关系(旋转)。

2. 确定了新节点必须是红色节点,接下来就是分情况讨论

  • 如果新节点的双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;

  • 但当新插入节点的双亲节点颜色为红色 时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论;

  • 约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点(cur不一定是新插入节点,也可能是调整上去的)。

  • 如果一颗子树需要被调整,那么至少有三个节点的颜色,我们是确定的1. cur当前节点为红色。2. cur的父节点一定是红色。3. cur的爷爷节点,也就是父节点的父节点,一定是黑色。 因为调整之前,以cur为根节点的子树的上一级树必然是符合规则的,如果不符合,还调整什么?这棵树就是有问题的。

  • 所以接下来,我们讨论的实际上是叔叔节点不同颜色or存在/不存在的情况 + cur在p的左节点or右节点的情况 + p在g的左节点or右节点的情况(叔叔节点也有可能不存在)。

  • 将上面这些情况全部综合起来,一共有 4 * 4 = 16 种情况需要讨论u节点存在or不存在or红色or黑色,共四种 X 上图中四种节点位置状态,共四种。
  • 有同学可能会想,如果u节点是黑色,那么整个树就不满足规则4了啊,平衡被打破了啊。所以u节点要么是红色,要么不存在。这就是没有考虑到,cur可能不是新插入节点,可能是原来是黑色的,只是调整的过程中,由黑变红了。这种情况下,整棵树的黑节点数量还是平衡的。

下面的讨论中有一些情况会合并成一种情况(好编程),最终呈现出来,会小于16种。但是我们需要知道,本质上,就是对这16种情况进行处理。

2. 开始讨论

  • 情况一(叔叔红):

    • 情况1.1:pg的左节点,curp的左节点,u存在且为红(左左红)。

      • 解决方案:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。
    • 情况1.2:pg的右节点,curp的右节点,u存在且为红(右右红)。

      • 解决方案和情况一完全相同,这里不画图了,大家对着情况一的图,自行脑补一下。
    • 情况1.3:pg的左节点,curp的右节点,u存在且为红(左右红)。

      • 解决方案和情况一完全相同
    • 情况1.4:pg的右节点,curp的左节点,u存在且为红(右左红)。

      • 解决方案和情况一完全相同
    • 将上面所有的情况1.*,总结为一种,即叔叔为红色(叔叔红),统一解决方案:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整

  • 情况二(左左黑):pg的左节点,curp的左节点,u不存在/u存在且为黑。

    • 解决方案:先对g节点进行右单旋,然后将p,g变色--p变黑,g变红。
  • 情况三(右右黑):pg的右节点,curp的右节点,u不存在/u存在且为黑。

    • 解决方案: 与情况三相比,唯一的不同是,需要对g进行左单旋 。变色部分完全一样,p,g变色--p变黑,g变红
  • 情况四(左右黑):pg的左节点,curp的右节点,u不存在/u存在且为黑。

    • 解决方案:先对p进行左单旋,转化为情况二(左左黑),再根据情况二进行处理。 总结一下就是,先左右双旋,再将cur变黑,g变红。
  • 情况五(右左黑):pg的右节点,curp的左节点,u存在且为红。

    • 解决方案:先对p进行右单旋,转化为情况三(右右黑),再根据情况三进行处理。 总结一下就是,先进行右左双旋,再将cur变黑,g变红。
cpp 复制代码
template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
private:
	Node* _root = nullptr;

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 (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		// 设定新增节点为红色
		cur = new Node(kv);
		cur->_col = RED;
		if (kv.first > parent->_kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		// 如果父节点存在并且是红色,再处理,是黑色不用处理
		while(parent && parent->_col == RED)
		{
			Node* grandFather = parent->_parent;
			
			if (parent == grandFather->_left)
			{
				// 这里只是说父亲在爷爷左边,而cur有可能在父亲的左节点也有可能在右节点
				// 后面还要再做讨论
				//		g
				//	  p	  u
				//	  c
				// 父亲是爷爷的左节点,那么叔叔就是爷爷的右节点
				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)
					{
						// cur 在父亲左边,右单旋(左左黑)
						//		g
						//	  p
						//	c
						RotateR(grandFather);
						parent->_col = BLACK;
						grandFather->_col = RED;
					}
					else
					{
						// cur 在父亲右边,左右双旋(左右黑)
						//		g
						//	  p
						//		c
						RotateL(parent);
						RotateR(grandFather);
						grandFather->_col = RED;
						cur->_col = BLACK;
					}
					// 旋转之后直接就平衡了,直接break
					break;
				}
			}
			else // parent == grandFather->_right
			{
				// 这里只是说父亲在爷爷右边,而cur有可能在父亲的左节点也有可能在右节点
				// 后面还要再做讨论
				//		g
				//	  u	  p
				//		  c
				// 父亲是爷爷的右节点,那么叔叔就是左节点
				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)
					{
						// cur 在父亲右边,左单旋(右右黑)
						//		g
						//		  p
						//			c
						RotateL(grandFather);
						parent->_col = BLACK;
						grandFather->_col = RED;
					}
					else
					{
						// cur 在父亲左边,右左双旋(右左黑)
						//		g
						//		  p
						//		c
						RotateR(parent);
						RotateL(grandFather);
						grandFather->_col = RED;
						cur->_col = BLACK;
					}
					// 旋转之后直接就平衡了,直接break
					break;
				}
			}
		}
			
		// 不管前面如何处理,最后都要把根节点变黑
		_root->_col = BLACK;
		return true;
	}
}

5. 红黑树的验证


红黑树的检测分为两步:

  • 检测其是否满足二叉搜索树(中序遍历是否为有序序列);
  • 检测其是否满足红黑树的性质。
cpp 复制代码
template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
private:
	Node* _root = nullptr;

public:
	...

	// 中序遍历
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	// 检查
	//	1. 红色节点的子节点是否为黑色节点
	//	2. 根节点->当前节点这条路径的黑色节点的数量
	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, refVal)
			&& Check(root->_right, blacknum, refVal);
	}

	// 看看红黑树是否符合要求
	bool IsBalance()
	{
		return _IsBalance(_root);
	}

	bool _IsBalance(Node* root)
	{
		if (root == nullptr)
			return true;

		if (root->_col == RED)
			return false;

		// 参考值,算出最左路径的黑节点数量
		int refVal = 0;
		Node* cur = root;
		while (cur) {
			if (cur->_col == BLACK)
				refVal++;
			cur = cur->_left;
		}

		int blacknum = 0;	
		return Check(root, blacknum, refVal);
	}
};

6. 红黑树的删除


红黑树的删除本节不做讲解,有兴趣的同学可参考:《算法导论》或者《STL源码剖析》http://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html


7. 红黑树完整代码+测试


1. 完整代码

cpp 复制代码
#pragma once

#include <iostream>

enum Colour
{
	RED,
	BLACK
};

using namespace std;

template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	Colour _col;

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


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

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 (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		// 设定新增节点为红色
		cur = new Node(kv);
		cur->_col = RED;
		if (kv.first > parent->_kv.first)
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		// 如果父节点存在并且是红色,再处理,是黑色不用处理
		while(parent && parent->_col == RED)
		{
			Node* grandFather = parent->_parent;
			
			if (parent == grandFather->_left)
			{
				// 这里只是说父亲在爷爷左边,而cur有可能在父亲的左节点也有可能在右节点
				// 后面还要再做讨论
				//		g
				//	  p	  u
				//	  c
				// 父亲是爷爷的左节点,那么叔叔就是爷爷的右节点
				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)
					{
						// cur 在父亲左边,右单旋(左左黑)
						//		g
						//	  p
						//	c
						RotateR(grandFather);
						parent->_col = BLACK;
						grandFather->_col = RED;
					}
					else
					{
						// cur 在父亲右边,左右双旋(左右黑)
						//		g
						//	  p
						//		c
						RotateL(parent);
						RotateR(grandFather);
						grandFather->_col = RED;
						cur->_col = BLACK;
					}
					// 旋转之后直接就平衡了,直接break
					break;
				}
			}
			else // parent == grandFather->_right
			{
				// 这里只是说父亲在爷爷右边,而cur有可能在父亲的左节点也有可能在右节点
				// 后面还要再做讨论
				//		g
				//	  u	  p
				//		  c
				// 父亲是爷爷的右节点,那么叔叔就是左节点
				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)
					{
						// cur 在父亲右边,左单旋(右左黑)
						//		g
						//		  p
						//			c
						RotateL(grandFather);
						parent->_col = BLACK;
						grandFather->_col = RED;
					}
					else
					{
						// cur 在父亲左边,右左双旋(左右黑)
						//		g
						//		  p
						//		c
						RotateR(parent);
						RotateL(grandFather);
						grandFather->_col = RED;
						cur->_col = BLACK;
					}
					// 旋转之后直接就平衡了,直接break
					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 != nullptr)
		{
			subRL->_parent = parent;
		}
		
		// 将子树链接进整体
		if (_root == parent)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else if (parentParent->_left == parent)
		{
			parentParent->_left = subR;
			subR->_parent = parentParent;
		}
		else
		{
			parentParent->_right = subR;
			subR->_parent = parentParent;
		}
	}

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

		// 更新左右节点
		parent->_left = subLR;
		subL->_right = parent;

		Node* parentParent = parent->_parent;

		// 更新父节点
		parent->_parent = subL;
		if (subLR != nullptr)
		{
			subLR->_parent = parent;
		}

		// 将子树链接进整体
		if (_root == parent)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else if (parentParent->_left == parent)
		{
			parentParent->_left = subL;
			subL->_parent = parentParent;
		}
		else
		{
			parentParent->_right = subL;
			subL->_parent = parentParent;
		}
	}

	// 中序遍历
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}

	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	// 检查
	//	1. 红色节点的子节点是否为黑色节点
	//	2. 根节点->当前节点这条路径的黑色节点的数量
	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, refVal)
			&& Check(root->_right, blacknum, refVal);
	}

	// 看看红黑树是否符合要求
	bool IsBalance()
	{
		return _IsBalance(_root);
	}

	bool _IsBalance(Node* root)
	{
		if (root == nullptr)
			return true;

		if (root->_col == RED)
			return false;

		// 参考值,算出最左路径的黑节点数量
		int refVal = 0;
		Node* cur = root;
		while (cur) {
			if (cur->_col == BLACK)
				refVal++;
			cur = cur->_left;
		}

		int blacknum = 0;	
		return Check(root, blacknum, refVal);
	}

	bool 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 true; // 找到了,返回真
			}
		}

		return false; // 找不到,返回假
	}

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

	int Height()
	{
		return _Height(_root);
	}

	int _Size(Node* root)
	{
		if (root == nullptr)
		{
			return 0;
		}
		return _Size(root->_left) + _Size(_root->_right) + 1;
	}

	int Size()
	{
		return _Size(_root);
	}
};

2. 测试Insert和Find的效率

cpp 复制代码
#include <iostream>
#include <chrono>
#include <vector>

using namespace std;

#include "RBTree.h"

// 小范围测试
void Test1()
{
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	RBTree<int, int> t;
	for (auto e : a)
	{
		t.Insert(make_pair(e, e));
	}
	t.InOrder();

	cout << "IsBalance: " << t.IsBalance() << endl;
}

// 大范围测试
void Test2()
{
    const int N = 100 * 10000;  // 测试100w条数据的插入
    vector<int> v;
    v.reserve(N);
    srand((unsigned int)time(0));

    for (size_t i = 0; i < N; i++)
    {
        v.push_back(rand() + (int)i);
    }

    RBTree<int, int> t;

    // 测试插入耗时
    auto start1 = chrono::high_resolution_clock::now();
    for (auto e : v)
    {
        t.Insert(make_pair(e, e));
    }
    auto end1 = chrono::high_resolution_clock::now();
    double insertTime = chrono::duration_cast<chrono::microseconds>(end1 - start1).count() / 1e6;

    // 测试查找耗时
    auto start2 = chrono::high_resolution_clock::now();
    if (t.Find(v[100])) cout << "Find Success!!!" << endl;
    auto end2 = chrono::high_resolution_clock::now();
    double findTime = chrono::duration_cast<chrono::microseconds>(end2 - start2).count() / 1e6;

    cout << "IsBalance: " << t.IsBalance() << endl;
    cout << "Heigh: " << t.Height() << endl;
    cout << "Insert cost: " << insertTime << " s" << endl;
    cout << "Find cost: " << findTime << " s" << endl;
}

int main()
{
	Test2();
	return 0;
}
  • Test2大范围测试的结果,数据量100w:
0 复制代码
Find Success!!!
IsBalance: 1
Heigh: 27
Insert cost: 0.304353 s
Find cost: 0.00027 s
  • 可以看到插入和查找效率非常高。

相关推荐
Croa-vo2 小时前
PayPal OA 全流程复盘|题型体验 + 成绩反馈 + 通关经验
数据结构·经验分享·算法·面试·职场和发展
是苏浙2 小时前
零基础入门C语言之贪吃蛇的实现
c语言·开发语言·数据结构
FMRbpm2 小时前
链表中出现的问题
数据结构·c++·算法·链表·新手入门
Pluchon6 小时前
硅基计划6.0 柒 JavaEE 浅谈JVM&GC垃圾回收
java·jvm·数据结构·java-ee·gc
不穿格子的程序员6 小时前
从零开始刷算法——二分-搜索旋转排序数组
数据结构·算法
sin_hielo7 小时前
leetcode 2536
数据结构·算法·leetcode
what_20188 小时前
list集合使用
数据结构·算法·list
im_AMBER9 小时前
数据结构 11 图
数据结构·笔记·学习·图论
xiaoye-duck10 小时前
数据结构之二叉树-链式结构(上)
数据结构