这就是红黑树么

github地址:https://github.com/duchenlong/Cpp/tree/master/RB_tree/RB_tree

红黑树

什么是红黑树

红黑树,也是二叉搜索树的一种形式。

在红黑树中,每个节点上增加了一个节点的颜色(RED 和 BLACK)。他通过对从根节点到叶子节点的路径上的各个节点的颜色,来判断是否是接近平衡的。

红黑树的性质

  1. 每一个结点不是红色就是黑色
  2. 根节点始终为黑色
  3. 如果一个节点是红色的,那么他的两个孩子节点以及父亲节点一定是黑色的(没有相连的红色节点)
  4. 对于每一个结点,从该节点到叶子节点的所有路径中,均包含相同数目的黑色节点

在这里插入图片描述
所以说,一个红黑树,他的最长路径中的节点数一定不会超过最短路径节点个数的两倍。

  • 最长路径:从根节点开始,就是一个黑节点,一个红色节点交替连接
  • 最短路径:全部都是黑色节点的路径

在这里插入图片描述
对比AVL树:

  1. 时间复杂度:AVL树的时间复杂度为O(log N),红黑树的时间复杂度为O(log 2 * N)
  2. AVL树中,他是一个严格平衡的树,也就是当左右子树一旦出现不平衡的状态,就直接进行旋转操作。
  3. 红黑树中,只有当破坏了红黑树性质的条件时,才进行旋转调节操作。

所以说,AVL树红黑树对比而言,他们的时间复杂度都是差不多的,只是AVL树存在着大量的旋转操作,比较费时间;而红黑树的旋转操作相对而言比较少。

红黑树节点的定义

  • 节点的颜色
enum Color
{
	BLACK,
	RED,
};

在STL中对于节点的颜色采用的时候两个bool类型的变量红色节点为false,黑色节点为true

  • 节点的定义
template<class T>
struct RBTreeNode
{
	typedef RBTreeNode<T> node;

	node* _left;
	node* _right;
	node* _parent;
	T _data;
	Color _col;
	
	//构造的节点为红色节点
	RBTreeNode(const T& data)
	{
		_left = _right = _parent = nullptr;
		_data = data;
		_col = RED;
	}
};
  • 红黑树中类的定义
template<class K,class T,class KOfT>
class RBTree
{
public:
	typedef RBTreeNode<T> node;
	//迭代器
	typedef __TreeIterator<T, T&, T*> iterator;
	typedef __TreeIterator<T, const T&, const T*> const_iterator;
public:
	RBTree()
		:_root(nullptr)
	{}
private:
	node* _root;
};

其中,K表示键值,T表示树中节点的数据类型,一般是一个pair类型的结构体,KOFT是一个结构体,他的作用就是取出T结构体中的键值进行比较

一般KOFT的描述可以这样来,在他的里面必须重载()

template<class K,class V>
struct cmp
{
	const K& operator()(const pair<K, V>& kv)
	{
		return kv.first;
	}
}

插入操作

	pair<iterator,bool> Insert(const T& data)
	{
	}

在插入一个新的节点之前,我们需要考虑的是这个新插入的节点是什么颜色的?

  • 由于红黑树的性质二 ,根节点始终为黑色

那么当插入节点为根节点的时候,就是黑色的节点

  • 红黑树的性质四,对于每一个结点,从该节点到叶子节点的所有路径中,均包含相同数目的黑色节点

如果我们新插入一个黑色的节点,那么很容易就会破坏这个特性,所以说我们就让新插入的节点为红色节点,这样对整体结构的影响比较小,然后再进行向上调整。

在这里插入图片描述
同AVL树,我们在插入的时候,先要找到待插入的这个节点的位置。

		//找到插入节点
		if (_root == nullptr)
		{
			_root = new node(data);
			_root->_col = BLACK;
			return make_pair(iterator(_root),true);
		}

		KOfT koft;//比较的函数
		node* parent = nullptr;
		node* cur = _root;
		while (cur)
		{
			if (koft(cur->_data) == koft(data))
				return make_pair(iterator(cur),false);
			parent = cur;
			cur = (koft(cur->_data) < koft(data)) ? cur->_right : cur->_left;
		}

		//此时cur为插入节点的位置,cur即为nullptr
		cur = new node(data);
		node* newNode = cur;
		if (koft(parent->_data) < koft(cur->_data))
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else if (koft(parent->_data) > koft(cur->_data))
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

接下来就是调节的过程了,对于调节而言,因为我们的新插入的节点为红色,那么当他的父亲节点也是红色的时候,就需要进行调节了。

//调节完毕的条件
while (parent && parent->_col == RED)

对于调整,我们需要根据叔叔节点来判断调整的方式。为了找到叔叔节点(u),我们就需要祖父节点的帮助(g)

先判断,新插入的节点在祖父节点的左子树中的情况

  1. 叔叔节点存在,并且为红色节点

在这里插入图片描述

	// 1. uncle 存在,并且该节点为红色
	if (uncle != nullptr && uncle->_col == RED)
	{
		parent->_col = uncle->_col = BLACK;
		grandfather->_col = RED;

		//继续向上处理
		cur = grandfather;
		parent = cur->_parent;
	}

继续向上处理时,我们需要保证cur为红色节点,也就是祖父节点(g)。

  1. 叔叔节点不存在,那么cur节点一定是新增的节点(因为在插入这个节点之前,这棵树就是一个红黑树)

在这里插入图片描述
如果叔叔节点存在,那么在这个分支中,叔叔节点一定是一个黑色节点,我们同样也进行一次右旋操作
在这里插入图片描述
直接对祖父节点进行右旋,然后变色就可以了,这个时候就可以退出循环了。

	Left_Left(grandfather);
	parent->_col = BLACK;
	grandfather->_col = RED;
  1. 当前节点(cur)是 parent 节点的右子树节点

在这里插入图片描述

	//3.cur 是parent的右子树节点
	if (cur == parent->_right)
	{
		Right_Right(parent);
		swap(parent, cur);//单纯的交换两个指针的位置,让他变成第二种情况
	}
	
	//2.uncle节点为nullptr,说明cur结点一定是新增节点,而不是向上调整后的节点
	//uncle节点存在,那么此时他一定是黑色节点
	//对grandfather进行一次右旋
	Left_Left(grandfather);
	parent->_col = BLACK;
	grandfather->_col = RED;

	break;

同理,当新插入的节点是祖父节点的右子树时
在这里插入图片描述
当需要旋转的时候,
在这里插入图片描述
在这里插入图片描述

	else if (grandfather->_right = parent)
	{
		node* uncle = grandfather->_left;

		if (uncle != nullptr && uncle->_col == RED)
		{
			parent->_col = uncle->_col = BLACK;
			grandfather->_col = RED;

			cur = grandfather;
			parent = cur->_parent;
		}
		else
		{
			if (cur == parent->_left)
			{
				Left_Left(parent);
				swap(parent, cur);
			}

			Right_Right(grandfather);
			grandfather->_col = RED;
			parent->_col = BLACK;

			break;
		}
	}

右旋

	//右旋
	void Left_Left(node* parent)
	{
		node* pParent = parent->_parent;
		node* pLeft = parent->_left;
		node* pLRight = pLeft->_right;

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

		pLeft->_right = parent;
		parent->_parent = pLeft;

		if (pParent == nullptr)
			_root = pLeft;
		else
			(parent == pParent->_left) ? pParent->_left = pLeft: pParent->_right = pLeft;
		pLeft->_parent = pParent;
	}

左旋

	//左旋
	void Right_Right(node* parent)
	{
		node* pParent = parent->_parent;
		node* pRight = parent->_right;
		node* pRLeft = pRight->_left;

		parent->_right = pRLeft;
		if(pRLeft)	pRLeft->_parent = parent;

		pRight->_left = parent;
		parent->_parent = pRight;

		if (pParent == nullptr)
			_root = pRight;
		else
			(pParent->_left == parent) ? pParent->_left = pRight : pParent->_right = pRight;
		pRight->_parent = pParent;
	}

红黑树的检测

对于红黑树的检测,我们需要严格根据红黑树的性质进行判断

  1. 根节点为黑色
  2. 对于每一个结点,从该节点到叶子节点的所有路径中,均包含相同数目的黑色节点
  3. 没有相连的红色节点
	//判断是否是红黑树
	bool IsRBTree()
	{
		node* proot = _root;
		//空树也是红黑树
		if (nullptr == proot)
			return true;

		//根节点必须为黑色
		if (BLACK != proot->_col)
		{
			cout << "根节点不为黑色" << endl;
			return false;
		}

		//获取任意一条路径中,黑色节点的数量
		size_t blackNum = 0;
		node* cur = proot;
		while (cur != nullptr)
		{
			if (BLACK == cur->_col)
				blackNum++;
			cur = cur->_left;
		}

		size_t k = 0;
		return _IsRBTree(proot, k, blackNum);
	}

我们在第一个函数中,先找到一条路径上的黑色节点数量,然后在判断这棵树是否满足所有路径的黑色节点数量相等

private:
	bool _IsRBTree(node* root, size_t k, size_t blackNum)
	{
		//走到了叶子节点,判断黑色节点是否相等
		if (nullptr == root)
		{
			if (k != blackNum)
			{
				cout << "路径中黑色节点数量不相等" << endl;
				return false;
			}
			return true;
		}

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

		// 检测当前节点与其双亲是否都为红色
		node* pParent = root->_parent;
		if (pParent != nullptr && RED == pParent->_col && RED == root->_col)
		{
			cout << "有两个相连的红色节点" << endl;
			return false;
		}
		return _IsRBTree(root->_left, k, blackNum) && _IsRBTree(root->_right, k, blackNum);
	}

红黑树中的迭代器

跟list容器一样,我们对于迭代器的功能进行了一下封装,组成一个迭代器的类(这个类在红黑树类中得是public的,因为map和set的底层还需要使用这个迭代器)

  • 迭代器的框架

Ref为数据的引用类型,Ptr为数据的指针类型

template<class T,class Ref,class Ptr>
struct __TreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef __TreeIterator<T, Ref, Ptr> Self;
	Node* _node;

	__TreeIterator(Node* node)
		:_node(node)
	{}
};
  1. 自增操作的重载(++)

首先,我们需要知道,自增操作的每一步都是找一个比当前节点数据大的节点,这就被分成了两步

第一步,如果当前节点存在右子树,那么自增的下一步就是右子树中最小的节点
在这里插入图片描述

	//如果存在右子树,那么下一个节点就是右子树中最小的那个节点
	if (_node->_right)
	{
		Node* cur = _node->_right;
		while (cur->_left)
			cur = cur->_left;
		_node = cur;
	}

第二步,如果当前节点的右子树不存在,那么就需要向上找第一个没有被访问的节点
在这里插入图片描述

	else
	{
		//右子树不存在,
		Node* cur = _node;
		Node* parent = cur->_parent;
		while (parent && cur == parent->_right)
		{
			cur = cur->_parent;
			parent = parent->_parent;
		}
		_node = parent;
	}
  1. 自减操作的重载(- -)

同理,只是在自减的操作中,需要找的是下一个比自己小的节点,那么就需要先判断左子树,其他步骤同自增

	Self& operator--()
	{
		if (_node->_left != nullptr)
		{
			//找到左子树中最大的一个节点
			Node* cur = _node->_left;
			while (cur->_right)
				cur = cur->_right;
			_node = cur;
		}
		else
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_left)
			{
				cur = parent;
				parent = parent->_parent;
			}
			_node = parent;
		}

		return *this;
	}
  1. *,->,==,!=,运算符重载
	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;
	}

	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}

	bool operator==(const Self& s)
	{
		return _node == s._node;
	}
  • 红黑树中的begin,end函数
	//迭代器
	iterator begin()//返回最小的节点的iterator迭代器
	{
		node* cur = _root;
		while (cur && cur->_left)
			cur = cur->_left;
		return iterator(cur);
	}

	iterator end()//返回最大元素的下一个元素,即为空
	{
		return iterator(nullptr);
	}

STL中 map的实现

map类的框架

	template<class K,class V>
	class map
	{
		//比较的函数
		struct MapKOfV
		{
			const K& operator()(const pair<K, V>& data)
			{
				return data.first;
			}
		};
		typedef typename RBTree<K, pair<K, V>, MapKOfV> RB_type;
		typedef typename RB_type::iterator iterator;
		
		private:
			RB_type _t;//红黑树
	};

在map的底层,其实是调用的红黑树的接口,所使用的数据都是通过红黑树来存储的。

  • []运算符重载

[]运算符中,他的内部其实是使用了一次插入的操作,不管该节点是否存在,我们直接对他进行插入操作。

只是在插入的时候,我们只知道键值,而不知道所存储的数据,所以我们对于pair结构体的第二个数据,直接调用这个数据类型的构造函数,创建一个匿名对象。

	V& operator[](const K& k)
	{
		pair<iterator, bool> ans = _t.Insert(make_pair(k, V()));
		return ans.first->second;
	}

对于他的返回值问题,我们需要返回的是,这个红黑树节点(ans.first)的第二个数据(ans.first->second)的引用。
在这里插入图片描述
其他的函数没有什么可说的,就是调用的红黑树的接口。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章