从C语言到C++_28(红黑树RedBlackTree)概念+插入接口实现

目录

1. 红黑树的引入和简介

2. 红黑树的性质和定义

3. 红黑树的插入

3.1 调整情况一

3.2 调整情况二

3.2.1 调整情况二中的单旋+变色

3.2.2 调整情况二中的双旋+变色

3.3 调整情况三

3.4 红黑树插入完整代码

4. 红黑树的验证和完整代码

4.1 验证是不是搜索树:

4.2 验证是不是红黑树:

4.3 红黑树完整代码:

5. 红黑树笔试选择题

答案:

6. 红黑树的零碎知识

本篇完。


1. 红黑树的引入和简介

        前面学了AVL树,平衡二叉树最大的作用就是查找,AVL树的查找、插入和删除在平均和最坏情况下都是O(logN)。AVL树的效率就是高在这个地方。

        如果在AVL树中插入或删除节点后,使得高度之差大于1。此时,AVL树的平衡状态就被破坏,它就不再是一棵二叉树;为了让它重新维持在一个平衡状态,就需要对其进行旋转处理,那么创建一颗平衡二叉树的成本其实不小。

        这个时候就有人开始思考,并且提出了红黑树的理论,红黑树在业界应用很广泛,比如 Java 中的 TreeMap,JDK 1.8 中的 HashMap、C++ STL 中的 set和map 均是基于红黑树结构实现的。

        那么红黑树到底比AVL树好在哪里?AVL树对平衡的要求太严格了,以至于它更多的会用到旋转,下面学的红黑树对平衡的要求就没有这么严格,所以它不会用到太多旋转。

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

        红黑树是一种自平衡的二叉查找树,是一种高效的查找树。它是由 Rudolf Bayer 于1978年发明在当时被称为平衡二叉 B 树(symmetric binary B-trees)。

        后来,在1978年被 Leo J. Guibas 和Robert Sedgewick 修改为如今的红黑树。红黑树具有良好的效率,它可在 O(logN) 时间内完成查找、增加、删除等操作。它是具备了某些特性的二叉搜索树,能解决非平衡树问题,红黑树二是一种接近平衡的叉树(说它是接近平衡因为它并没有像AVL树的平衡因子的概念,它只是靠着满足红黑节点的5个性质来维持一种接近平衡的结构,进而提升整体的性能,并没有严格的卡定某个平衡因子来维持绝对平衡)。


2. 红黑树的性质和定义

红黑树是怎么保证最长路径不超过最短路径的两倍的?

  1. 每个结点不是红色就是黑色
  2. 根节点是黑色的
  3. 如果一个节点是红色的,则它的两个孩子结点是黑色的
  4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
  5. 每个空结点都是黑色的(这里的空结点也叫NIL结点,只是为了方便知道有多少条路径)

思考:为什么满足上面的性质,红黑树就能保证:最长路径不超过最短路径的两倍?

因为上面的性质形成了互斥:

最短路径:路径上全是黑色结点。

最长路径:红黑交替,黑色结点和红色结点的个数相等。

因为每条路径黑色结点个数是一样的,所以最长路径不超过最短路径的两倍。


红黑树的定义RedBlackTree.h:

#pragma once

#include <iostream>
using namespace std;

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; // 比AVL树少了平衡因子,多了颜色

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

template<class K, class V>
struct RBTree
{
	typedef RBTreeNode<K, V> Node;

protected:
	Node* _root = nullptr;
};

3. 红黑树的插入

插入结点如果让你插入你是会插入红色结点还是黑色结点?

        当我们向红黑树插入结点时,若我们插入的是黑色结点,那么插入路径上黑色结点的数目就比其他路径上黑色结点的数目多了一个,即破坏了红黑树的性质4,此时我们就需要对红黑树进行调整。

        若我们插入红黑树的结点是红色的,此时如果其父结点也是红色的,那么表明出现了连续的红色结点,即破坏了红黑树的性质3,此时我们需要对红黑树进行调整;但如果其父结点是黑色的,那我们就无需对红黑树进行调整,插入后仍满足红黑树的要求。

总结一下:

  • 插入黑色结点,一定破坏红黑树的性质4,必须对红黑树进行调整。
  • 插入红色结点,可能破坏红黑树的性质3,可能对红黑树进行调整。

权衡利弊后,我们在构造结点进行插入时,将结点的颜色设置为红色。


红黑树插入结点的逻辑分为三步:

  1. 按二叉搜索树的插入方法,找到待插入位置。
  2. 将待插入结点插入到树中。
  3. 若插入结点的父结点是红色的,则需要对红黑树进行调整。

其中前两步与二叉搜索树插入结点时的逻辑相同,红黑树的关键在于第三步对红黑树的调整。

        实际上,在插入结点后并不是一定会对红黑树进行调整,若插入结点的父结点是黑色的,那么我们就不用对红黑树进行调整,因为本次结点的插入并没有破坏红黑树的五点性质。

        只有当插入结点的父结点是红色时才需要对红黑树进行调整,因为我们默认插入的结点就是红色的,如果插入结点的父结点也是红色的,那么此时就出现了连续的红色结点,因此需要对红黑树进行调整。

        因为插入结点的父结点是红色的,说明父结点不是根结点(根结点是黑色的),因此插入结点的祖父结点(父结点的父结点)就一定存在。

        红黑树调整时具体应该如何调整,主要是看插入结点的叔叔(插入结点的父结点的兄弟结点),根据插入结点叔叔的不同,可将红黑树的调整分为三种情况。

这里定义:cur为当前节点,p为parent父节点,g为grandfather祖父节点,u为uncle叔叔节点。

以下三种情况都是cur为红,p为红,g为黑,然后看叔叔的情况


3.1 调整情况一

调整情况一:插入结点的叔叔存在,且叔叔的颜色是红色。

        此时为了避免出现连续的红色结点,我们可以将父结点变黑,但为了保持每条路径黑色结点的数目不变,因此我们还需要将祖父结点变红,再将叔叔变黑。这样一来既保持了每条路径黑色结点的数目不变,也解决了连续红色结点的问题。

        但调整还没有结束,因为此时祖父结点变成了红色,如果祖父结点是根结点,那我们直接再将祖父结点变成黑色即可,此时相当于每条路径黑色结点的数目都增加了一个。

        但如果祖父结点不是根结点的话,我们就需要将祖父结点当作新插入的结点,再判断其父结点是否为红色,若其父结点也是红色,那么又需要根据其叔叔的不同,进而进行不同的调整操作。

因此,情况一的抽象图表示如下:

注意: 叔叔存在且为红时,cur结点是parent的左孩子还是右孩子,调整方法都是一样的。

        情况一解决方法简记:将父亲和叔叔改为黑,祖父改为红,然后把祖父当成cur,parent变祖父(cur)parent继续向上调整。


3.2 调整情况二

调整情况二:插入结点的叔叔存在,且叔叔的颜色是黑色。

        需要注意:从根结点一直走到空位置就算一条路径,而不是从根结点走到左右结点均为空的叶子结点时才算一条路径。

        情况二和情况三均需要进行旋转处理,旋转处理后无需继续往上进行调整,所以说情况二一定是由情况一往上调整的过程中出现的。出现叔叔存在且为黑时,单纯使用变色已经无法处理了,这时我们需要进行旋转处理。


3.2.1 调整情况二中的单旋+变色

        若祖孙三代的关系是直线(cur、parent、grandfather这三个结点为一条直线),颜色调整后这棵被旋转子树的根结点是黑色的,因此无需继续往上进行处理。

抽象图表示如下:

此时parent是grandfather的左孩子,cur也是parent的左孩子时,

另一种情况:当直线关系为,parent是grandfather的右孩子,cur是parent的右孩子时,就需要先进行左单旋操作,再进行颜色调整。


3.2.2 调整情况二中的双旋+变色

        若祖孙三代的关系是折线(cur、parent、grandfather这三个结点为一条折线),则我们需要先进行双旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根是黑色的,因此无需继续往上进行处理。

抽象图表示如下:

此时parent是grandfather的左孩子,cur也是parent的右孩子时,左右双旋:

        另一种情况: 当折线关系为,parent是grandfather的右孩子,cur是parent的左孩子时,就需要先进行右左双旋操作,再进行颜色调整。 


3.3 调整情况三

调整情况三:插入结点的叔叔不存在。

        在这种情况下的cur结点一定是新插入的结点,而不可能是由情况一变化而来的,因为叔叔不存在说明在parent的下面不可能再挂黑色结点了,如下图:

        如果插入前parent下面再挂黑色结点,就会导致图中两条路径黑色结点的数目不相同,而parent是红色的,因此parent下面自然也不能挂红色结点,所以说这种情况下的cur结点一定是新插入的结点。

        和情况二一样,若祖孙三代的关系是直线(cur、parent、grandfather 这三个结点为一条直线)则需要先进行单旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根结点是黑色的,因此无需继续往上进行处理。

抽象图表示如下:

        另一种情况:当直线关系为,parent是grandfather的右孩子,cur是parent的右孩子时,就需要先进行左单旋操作,再进行颜色调整。

        若祖孙三代的关系是折线(cur、parent、grandfather这三个结点为一条折线),则我们需要先进行双旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根是黑色的,因此无需继续往上进行处理。

抽象图表示如下:

        另一种情况:当折线关系为,parent是grandfather的右孩子,cur是parent的左孩子时,就需要先进行右左双旋操作,再进行颜色调整。

        分析完情况三,我们可以把情况三和情况二写在一起:在三种情况分类下,红黑树调整后,需要将根结点的颜色变为黑色,下一句return true;因为红黑树的根结点可能在情况一的调整过程中被变成了红色。


3.4 红黑树插入完整代码

(旋转还是用AVL树写的旋转,把平衡因子删掉,所以只需复制两个单旋)

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

		cur = new Node(kv);
		cur->_col = RED; // 默认插入红色结点
		if (parent->_kv.first < kv.first) // 找到位置后插入结点
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_parent = parent;

		while (parent && parent->_col == RED) // 父亲存在且为红才需要处理
		{
			Node* grandfather = parent->_parent;
			assert(grandfather); // 确定的可以断言下,否则就是插入前就不是红黑树
			assert(grandfather->_col == BLACK);
			if (grandfather->_left == parent)
			{
				Node* uncle = grandfather->_right;

				if (uncle && uncle->_col == RED) // 情况一,叔叔存在且为红(可以直接复制到下面uncle在左边)
				{    // 将父亲和叔叔改为黑,祖父改为红,然后把祖父当成cur,parent变祖父parent继续向上调整。
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况二或情况三:叔叔存在且为黑或叔叔不存在
				{
					if(cur == parent->_left) // 情况二的右旋+变色(parent在左)
					{
						//     g      
						//   p   u
						// c
						RotateR(grandfather);
						parent->_col = BLACK; // 父亲变为根了
						grandfather->_col = RED;
					}
					else // 情况二的左右双旋+变色(parent在左)
					{
						//      g      
                        //   p     u
                        //    c
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK; // cur变为根了
						grandfather->_col = RED;
					}
					break;
				}
			}
			else
			{
				Node* uncle = grandfather->_left;

				if (uncle && uncle->_col == RED) // 情况一,叔叔存在且为红
				{    // 将父亲和叔叔改为黑,祖父改为红,然后把祖父当成cur,parent变祖父parent继续向上调整。
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况二或情况三:叔叔存在且为黑或叔叔不存在
				{
					if (cur == parent->_right) // 情况二的左旋+变色(parent在右)
					{
						//     g      
						//   u   p
						//        c
						RotateL(grandfather);
						parent->_col = BLACK; // 父亲变为根了
						grandfather->_col = RED;
					}
					else // 情况二的右左双旋+变色(parent在右)
					{
						//       g      
						//    u     p
						//         c
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK; // cur变为根了
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		
		_root->_col = BLACK;
		return true;
	}

4. 红黑树的验证和完整代码

4.1 验证是不是搜索树:

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

protected:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}

先拿AVL树的测试过来,Test.c:

#include "RedBlackTree.h"

void TestRBTree1()
{
	//int arr[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	int arr[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	RBTree<int, int> t1;
	for (const auto& e : arr)
	{
		t1.Insert(make_pair(e, e));
	}

	t1.InOrder();
	//cout << "IsBalance:" << t1.IsBalance() << endl;
}

int main()
{
	TestRBTree1();

	return 0;
}


4.2 验证是不是红黑树:

        但中序有序只能证明是二叉搜索树,给你验证一颗树是不是红黑树你会怎么验证?是验证最长路径不超过最短路径的两倍还是红黑树的那几条性质?

        要证明二叉树是红黑树还需验证该二叉树是否满足红黑树的性质。因为最长路径不超过最短路径的两倍也不能保证是红黑树,只有满足全部红黑树的性质的才是。

  1. 每个结点不是红色就是黑色
  2. 根节点是黑色的
  3. 如果一个节点是红色的,则它的两个孩子结点是黑色的
  4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
  5. 每个空结点都是黑色的(这里的空结点也叫NIL结点,只是为了方便知道有多少条路径)
  • 第一条,枚举就能保证。
  • 第二条,红色就false,一句代码解决。
  • 第三条,我们可以遍历这颗树,用逆向思维,遇到红色结点就反过来判断它的父亲是不是红色的。
  • 第四条,我们可以用前序遍历,遍历第一遍的时候保存黑色结点个数,后面不同就false。
  • 第五条,不用管。

4.3 红黑树完整代码:

RedBlackTree.h:

#pragma once

#include <iostream>
#include <assert.h>
#include <time.h>
using namespace std;

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; // 比AVL树少了平衡因子,多了颜色

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

template<class K, class V>
struct RBTree
{
	typedef RBTreeNode<K, V> Node;
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 (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}

		cur = new Node(kv);
		cur->_col = RED; // 默认插入红色结点
		if (parent->_kv.first < kv.first) // 找到位置后插入结点
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_parent = parent;

		while (parent && parent->_col == RED) // 父亲存在且为红才需要处理
		{
			Node* grandfather = parent->_parent;
			assert(grandfather); // 确定的可以断言下,否则就是插入前就不是红黑树
			assert(grandfather->_col == BLACK);
			if (grandfather->_left == parent)
			{
				Node* uncle = grandfather->_right;

				if (uncle && uncle->_col == RED) // 情况一,叔叔存在且为红(可以直接复制到下面uncle在左边)
				{    // 将父亲和叔叔改为黑,祖父改为红,然后把祖父当成cur,parent变祖父parent继续向上调整。
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况二或情况三:叔叔存在且为黑或叔叔不存在
				{
					if(cur == parent->_left) // 情况二的右旋+变色(parent在左)
					{
						//     g      
						//   p   u
						// c
						RotateR(grandfather);
						parent->_col = BLACK; // 父亲变为根了
						grandfather->_col = RED;
					}
					else // 情况二的左右双旋+变色(parent在左)
					{
						//      g      
                        //   p     u
                        //    c
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK; // cur变为根了
						grandfather->_col = RED;
					}
					break;
				}
			}
			else
			{
				Node* uncle = grandfather->_left;

				if (uncle && uncle->_col == RED) // 情况一,叔叔存在且为红
				{    // 将父亲和叔叔改为黑,祖父改为红,然后把祖父当成cur,parent变祖父parent继续向上调整。
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况二或情况三:叔叔存在且为黑或叔叔不存在
				{
					if (cur == parent->_right) // 情况二的左旋+变色(parent在右)
					{
						//     g      
						//   u   p
						//        c
						RotateL(grandfather);
						parent->_col = BLACK; // 父亲变为根了
						grandfather->_col = RED;
					}
					else // 情况二的右左双旋+变色(parent在右)
					{
						//       g      
						//    u     p
						//         c
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK; // cur变为根了
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		
		_root->_col = BLACK;
		return true;
	}

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

	bool IsBalance()
	{
		if (_root == nullptr)
		{
			return true;
		}

		if (_root->_col == RED) // 验证性质二
		{
			cout << "根节点不是黑色" << endl;
			return false;
		}

		int benchmark = 0; // 黑色节点数量基准值
		//Node* cur = _root; // 这种方法是先遍历一遍,然后传值,不过我们可以传引用
		//while (cur)
		//{
		//	if (cur->_col == BLACK)
		//	{
		//		++benchmark;
		//	}
		//	cur = cur->_left;
		//}
		return PrevCheck(_root, 0, benchmark); // 验证性质三和四
	}

protected:
	bool PrevCheck(Node* root, int blackNum, int& benchmark)
	{
		if (root == nullptr)
		{
			if (benchmark == 0)
			{
				benchmark = blackNum;
				return true;
			}

			if (blackNum != benchmark) // 验证性质三
			{
				cout << "某条黑色节点的数量不相等" << endl;
				return false;
			}
			else
			{
				return true;
			}
		}

		if (root->_col == BLACK)
		{
			++blackNum;
		}

		if (root->_col == RED && root->_parent->_col == RED) // 验证性质四
		{
			cout << "存在连续的红色节点" << endl;
			return false;
		}

		return PrevCheck(root->_left, blackNum, benchmark)
			&& PrevCheck(root->_right, blackNum, benchmark);
	}

	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}

	void RotateL(Node* parent)
	{
		Node* subR = parent->_right; // 动了三个标记了的结点,共更新六个指针,这更新两个指针
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if (subRL) // subRL不为空才更新
		{
			subRL->_parent = parent;
		}

		Node* ppNode = parent->_parent; // 记录parent的parent,防止parent是一颗子树的头结点

		subR->_left = parent; // 再更新两个指针
		parent->_parent = subR;

		if (_root == parent)  // 最后更新两个指针
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else // parent是一颗子树的头结点
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}
			subR->_parent = ppNode;
		}
	}

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

		parent->_left = subLR; // 更新两个节点
		if (subLR)
		{
			subLR->_parent = parent;
		}

		Node* ppNode = parent->_parent;

		subL->_right = parent; // 再更新两个节点
		parent->_parent = subL;

		if (_root == parent) // 最后更新两个结点
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subL;
			}
			else
			{
				ppNode->_right = subL;
			}

			subL->_parent = ppNode;
		}
	}

	Node* _root = nullptr;
};

Test.c:

#include "RedBlackTree.h"

void TestRBTree1()
{
	//int arr[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	int arr[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	RBTree<int, int> t1;
	for (const auto& e : arr)
	{
		t1.Insert(make_pair(e, e));
	}

	t1.InOrder();
	cout << "IsBalance:" << t1.IsBalance() << endl;
}

void TestRBTree2()
{
	size_t N = 10000;
	srand(time(0));
	RBTree<int, int> t1;
	for (size_t i = 0; i < N; ++i)
	{
		int x = rand();
		cout << "Insert:" << x << ":" << i << endl;
		t1.Insert(make_pair(x, i));
	}
	cout << "IsBalance:" << t1.IsBalance() << endl;
}

int main()
{
	TestRBTree2();

	return 0;
}


5. 红黑树笔试选择题

1.关于红黑树以下说法正确的是()

A.空树不是红黑树,因为红黑树要求根节点必须为黑色,而空树中没有根节点

B.红黑树也是二叉搜索树,因此其按照前序遍历可以得到有序序列

C.红黑树是一棵真正平衡的二叉树

D.红黑树最长路径中节点个数可能会等于最短路径中节点个数的两倍

2. 红黑树的插入算法复杂度最坏情况是 ()

A.O(n)

B.O(log(n))

C.O(nlog(n))

D.其他都不对

3. 下面关于红黑树的特性说法错误的是()

A.红黑树最左侧节点一定是最小的,最右侧节点一定是最大的

B.红黑树在实现时必须要有头结点

C.红黑树中可能会出现连在一起的黑色节点

D.红黑树的旋转不需要依靠平衡因子

4. 关于AVL树和红黑树的区别说法不正确的是()

A.AVL树和红黑树保证平衡性的方式不同

B.AVL树和红黑树都是平衡树,因此查找的时间复杂度都是O(log_2N)

C.AVL树和红黑树的性质遭到破坏时,都需要进行旋转

D.AVL树和红黑树中序遍历都可以得到有序序列,因为它们都是二叉搜索树

答案:

1. D

A:错误,空树也是红黑树,性质5中规定树中的空指针域为叶子节点,因此空树也是有节点的

B:错误,红黑树也是二叉搜索树,按照中序遍历才可以得到有序序列

C:红黑树不像AVL树那么严格,是一棵近似平衡的二叉搜索树

D:正确,比如红黑树中只有两个节点

2. B

红黑树是近似的平衡树,没有什么最坏情况,插入的时间复杂度为O(log(N))

3. B

A:该题不严谨,这个取决于红黑树中元素的比较规则,最左侧节点可能是最大的,也可能是最小的,没有规定,取决于创建树时的比较方式

B:错误,红黑树在实现时可以没有头节点,这个根据需要是否给出这里头结点和带头双向顺序链表的差不多(我们实现的红黑树就没有)

C:正确,但是一定不能出现连在一起的红色节点

D:正确,红黑树是通过节点颜色以及红黑树的性质来保证其平衡性的,AVL树需要平衡因子保证

4. C

A:正确,AVL树通过节点的平衡因子保证,红黑树通过节点的颜色以及红黑树的特性保证

B:正确,AVL树是严格平衡的,红黑树虽然是近似平衡,但其性能往往比AVL树好,而且实现简 单,因此他们的查找    效率都是O(logN)

C:错误,AVL树是一定需要旋转,红黑树不一定,红黑树有时只需要改变节点的颜色即可

D:正确,参考概念


6. 红黑树的零碎知识

红黑树的删除和AVL树一样不做讲解,有兴趣可参考《STL源码剖析》


红黑树与AVL树的比较

        红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(logN)。

        红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数。所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。


红黑树插入动图

1. 以升序插入构建红黑树 


2. 以降序插入构建红黑树


随机插入的找不到动图了,放个图片看看:(都是一样保持基本平衡的)


红黑树的应用

1. C++ STL库 -- map / set、mutil_map / mutil_set
2. Java 库
3. linux内核
4. 其他一些库


本篇完。

下一篇:改造红黑树用来封装set和map(红黑树迭代器的实现),再下一篇:哈希。

(穿越回来复习顺便贴个下篇链接:从C语言到C++_29(红黑树封装set和map)红黑树迭代器的实现_c语言基于linux 红黑树封装map-CSDN博客

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

GR鲸鱼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值