這就是紅黑樹麼

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)的引用。
在這裏插入圖片描述
其他的函數沒有什麼可說的,就是調用的紅黑樹的接口。

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