【C++】了解map和set及平衡二叉树和红黑树的原理

news2024/11/19 5:46:15

目录

​编辑

一、关联式容器

二、 键值对

三、pair介绍

 四、树形结构的关联式容器

4.1 set

4.2 map

4.3 multiset

4.4 multimaps

 五、底层结构(重点)

5.1 AVL 树

5.1.1 AVL树的概念

5.1.2 AVL树节点的定义

5.1.3 AVL树的旋转

5.1.4 AVL树的插入

5.1.5 AVL树的删除(了解)

5.1.6 AVL树的性能 

5.2  红黑树

5.2.1 红黑树的概念

5.2.2 红黑树的性质

5.2.3 红黑树节点的定义

5.2.4 红黑树结构

5.2.5 红黑树的插入操作

4.2.6 红黑树的删除

4.2.7 红黑树与AVL树的比较

4.2.8 红黑树的应用

结尾:


一、关联式容器

在初阶阶段,我们已经接触过STL中的部分容器,比如:vector、list、deque、

forward_list(C++11)等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。那什么是关联式容器?它与序列式容器有什么区别?

关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是结构的键值对,在数据检索时比序列式容器效率更高。


二、 键值对

用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。

SGI-STL中关于键值对的定义:

template <class T1, class T2>
struct pair
{
	typedef T1 first_type;
	typedef T2 second_type;
	T1 first;
	T2 second;
	pair() 
		: first(T1()), second(T2())
	{}
	pair(const T1& a, const T2& b)
		: first(a), second(b)
	{}
};

三、pair介绍

在C++中,pair 是一种标准库模板,定义在头文件 <utility> 中。它用于存储一对值,这两个值可以是不同的数据类型。pair 的第一个元素称为 first,第二个元素称为 second。pair 类型提供了一种将两个数据组合成一个单元的方式,这在很多情况下都非常有用,比如当函数需要返回两个值时。

pair的常用接口


 四、树形结构的关联式容器

根据应用场景的不桶,STL总共实现了两种不同结构的管理式容器:树型结构与哈希结构。树型结 构的关联式容器主要有四种map、set、multimap、multiset。这四种容器的共同点是:使用平衡搜索树(即红黑树)作为其底层结果,容器中的元素是一个有序的序列。下面一依次介绍每一 个容器。

4.1 set

  1. set是按照一定次序存储元素的容器
  2. 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
  3. 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。
  4. set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对子集进行直接迭代。
  5. set在底层是用二叉搜索树(红黑树)实现的。

注意:

  1. 与map/multimap不同,map/multimap中存储的是真正的键值对,set中只放value,但在底层实际存放的是由构成的键值对。
  2. set中插入元素时,只需要插入value即可,不需要构造键值对。
  3. set中的元素不可以重复(因此可以使用set进行去重)。
  4. 使用set的迭代器遍历set中的元素,可以得到有序序列
  5. set中的元素默认按照小于来比较
  6. set中查找某个元素,时间复杂度为:$log_2 n$
  7. set中的元素不允许修改(为什么?)
  8. set中的底层使用二叉搜索树(红黑树)来实现。

1. set的模板参数列表

T: set中存放元素的类型,实际在底层存储的键值对。

Compare:set中元素默认按照小于来比较

Alloc:set中元素空间的管理方式,使用STL提供的空间配置器管理

2. 构造方法

3. set的迭代器

4.set的容量

5. set修改操作 

6. set的使用举例  

#include <set>
void TestSet()
{
	// 用数组array中的元素构造set
	int array[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0, 1, 3, 5, 7, 9, 2, 4,
6, 8, 0 };
	set<int> s(array, array + sizeof(array) / sizeof(array));
	cout << s.size() << endl;
	// 正向打印set中的元素,从打印结果中可以看出:set可去重
	for (auto& e : s)
		cout << e << " ";
	cout << endl;
	// 使用迭代器逆向打印set中的元素
	for (auto it = s.rbegin(); it != s.rend(); ++it)
		cout << *it << " ";
	cout << endl;
	// set中值为3的元素出现了几次
	cout << s.count(3) << endl;
}

4.2 map

  1. 1. map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。
  2. 在map中,键值key通常用于排序和惟一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,为其取别名称为pair:typedef pair value_type;
  3. 在内部,map中的元素总是按照键值key进行比较排序的。
  4. map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。
  5. map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。
  6. map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))。

1. map的模板参数说明

key: 键值对中key的类型

T: 键值对中value的类型

Compare: 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比

较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递)

Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器

2. map的构造

 3. map的迭代器

4. map的容量与元素访问 

注意:在元素访问时,有一个与operator[]类似的操作at()(该函数不常用)函数,都是通过 key找到与key对应的value然后返回其引用,不同的是:当key不存在时,operator[]用默认 value与key构造键值对然后插入,返回该默认value,at()函数直接抛异常。  

5. map中元素的修改

6. map的使用举例

#include <string>
#include <map>
void TestMap()
{
	map<string, string> m;
	// 向map中插入元素的方式:
	// 将键值对<"peach","桃子">插入map中,用pair直接来构造键值对
	m.insert(pair<string, string>("peach", "桃子"));
	// 将键值对<"peach","桃子">插入map中,用make_pair函数来构造键值对
	m.insert(make_pair("banan", "香蕉"));

	// 借用operator[]向map中插入元素
	   /*
	operator[]的原理是:
	 用<key, T()>构造一个键值对,然后调用insert()函数将该键值对插入到map中
	 如果key已经存在,插入失败,insert函数返回该key所在位置的迭代器
	 如果key不存在,插入成功,insert函数返回新插入元素所在位置的迭代器
	 operator[]函数最后将insert返回值键值对中的value返回
	*/
	// 将<"apple", "">插入map中,插入成功,返回value的引用,将“苹果”赋值给该引
	用结果,
		m["apple"] = "苹果";
	// key不存在时抛异常
	//m.at("waterme") = "水蜜桃";
	cout << m.size() << endl;
	// 用迭代器去遍历map中的元素,可以得到一个按照key排序的序列
	for (auto& e : m)
		cout << e.first << "--->" << e.second << endl;
	cout << endl;
	// map中的键值对key一定是唯一的,如果key存在将插入失败
	auto ret = m.insert(make_pair("peach", "桃色"));
	if (ret.second)
		cout << "<peach, 桃色>不在map中, 已经插入" << endl;
	else
		cout << "键值为peach的元素已经存在:" << ret.first->first << "--->"
		<< ret.first->second << " 插入失败" << endl;
	// 删除key为"apple"的元素
	m.erase("apple");
	if (1 == m.count("apple"))
		cout << "apple还在" << endl;
	else
		cout << "apple被吃了" << endl;
}

 【总结】

  1. map中的的元素是键值对
  2. map中的key是唯一的,并且不能修改
  3. 默认按照小于的方式对key进行比较
  4. map中的元素如果用迭代器去遍历,可以得到一个有序的序列
  5. map的底层为平衡搜索树(红黑树),查找效率比较高$O(log_2 N)$6. 支持[]操作符,operator[]中实际进行插入查找。

4.3 multiset

  1. multiset是按照特定顺序存储元素的容器,其中元素是可以重复的。
  2. 在multiset中,元素的value也会识别它(因为multiset中本身存储的就是组成 的键值对,因此value本身就是key,key就是value,类型为T). multiset元素的值不能在容器 中进行修改(因为元素总是const的),但可以从容器中插入或删除。
  3. 在内部,multiset中的元素总是按照其内部比较规则(类型比较)所指示的特定严格弱排序准则 进行排序。
  4. multiset容器通过key访问单个元素的速度通常比unordered_multiset容器慢,但当使用迭 代器遍历时会得到一个有序序列。
  5. multiset底层结构为二叉搜索树(红黑树)。

注意:

  1. multiset中再底层中存储的是的键值对
  2. mtltiset的插入接口中只需要插入即可
  3. 与set的区别是,multiset中的元素可以重复,set是中value是唯一的
  4. 使用迭代器对multiset中的元素进行遍历,可以得到有序的序列 
  5. multiset中的元素不能修改
  6. 在multiset中找某个元素,时间复杂度为O(log_2 N)
  7. multiset的作用:可以对元素进行排序

multiset的使用

此处只简单演示set与multiset的不同,其他接口接口与set相同,可参考set。

#include <set>
void TestSet()
{
	int array[] = { 2, 1, 3, 9, 6, 0, 5, 8, 4, 7 };

	// 注意:multiset在底层实际存储的是<int, int>的键值对
	multiset<int> s(array, array + sizeof(array) / sizeof(array[0]));
	for (auto& e : s)
		cout << e << " ";
	cout << endl;
	return 0;
}

4.4 multimaps

​​​​​​multimap

  1. Multimaps是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对,其中多个键值对之间的key是可以重复的。
  2. 在multimap中,通常按照key排序和惟一地标识元素,而映射的value存储与key关联的内 容。key和value的类型可能不同,通过multimap内部的成员类型value_type组合在一起, value_type是组合key和value的键值对: typedef pair value_type;
  3. 在内部,multimap中的元素总是通过其内部比较对象,按照指定的特定严格弱排序标准对 key进行排序的。
  4. multimap通过key访问单个元素的速度通常比unordered_multimap容器慢,但是使用迭代 器直接遍历multimap中的元素可以得到关于key有序的序列。
  5. multimap在底层用二叉搜索树(红黑树)来实现。

注意:multimap和map的唯一不同就是:map中的key是唯一的,而multimap中key是可以 重复的。

multimap的使用

multimap中的接口可以参考map,功能都是类似的。

注意:

  1. multimap中的key是可以重复的。
  2. multimap中的元素默认将key按照小于来比较
  3. multimap中没有重载operator[]操作(同学们可思考下为什么?)。
  4. 使用时与map包含的头文件相同:

 五、底层结构(重点)

前面对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个 共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中 插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此 map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。

5.1 AVL 树

5.1.1 AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查 找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis在1962年 发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右 子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均 搜索长度。

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O(log_2 n),搜索时间复杂度O(log_2 n)。 

5.1.2 AVL树节点的定义
template <class K, class V>
class AVLNode
{
public:
	AVLNode(const pair<K, V>& kv)
		: _kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}

	pair<K, V> _kv;
	AVLNode<K, V>* _left;
	AVLNode<K, V>* _right;
	AVLNode<K, V>* _parent;
	int _bf;
};
5.1.3 AVL树的旋转

如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构, 使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:

1. 新节点插入较高左子树的左侧---左左:右单旋

/*
  上图在插入前,AVL树是平衡的,新节点插入到30的左子树(注意:此处不是左孩子)中,30左
子树增加
  了一层,导致以60为根的二叉树不平衡,要让60平衡,只能将60左子树的高度减少一层,右子
树增加一层,
  即将左子树往上提,这样60转下来,因为60比30大,只能将其放在30的右子树,而如果30有
右子树,右子树根的值一定大于30,小于60,只能将其放在60的左子树,旋转完成后,更新节点
的平衡因子即可。在旋转过程中,有以下几种情况需要考虑:
  1. 30节点的右孩子可能存在,也可能不存在
  2. 60可能是根节点,也可能是子树
     如果是根节点,旋转完成后,要更新根节点
     如果是子树,可能是某个节点的左子树,也可能是右子树
     
*/

void RotateR(Node* parent)
		{
			Node* cur = parent->_left;
			parent->_left = cur->_right;
			if (cur->_right)
				cur->_right->_parent = parent;

			cur->_right = parent;
			Node* pphead = parent->_parent;
			parent->_parent = cur;

			if (pphead == nullptr)
			{
				_root = cur;
				cur->_parent = nullptr;
			}
			else
			{
				if(pphead->_left == parent)
				{
					pphead->_left = cur;
				}
				else
				{
					pphead->_right = cur;
				}
			}
			parent->_bf = cur->_bf = 0;
		}

2. 新节点插入较高右子树的右侧---右右:左单旋 

void RotateL(Node* parent)
		{
			Node* cur = parent->_right;
			parent->_right = cur->_left;
			if (cur->_left)
				cur->_left->_parent = parent;

			cur->_left = parent;
			Node* pphead = parent->_parent;
			parent->_parent = cur;

			if (pphead == nullptr)
			{
				_root = cur;
				cur->_parent = nullptr;
			}
			else
			{
				if (pphead->_left == parent)
				{
					pphead->_left = cur;
				}
				else
				{
					pphead->_right = cur;
				}
				cur->_parent = pphead;
			}
			parent->_bf = cur->_bf = 0;
		}

3. 新节点插入较高左子树的右侧---左右:先左单旋再右单旋

 将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,旋转完成后再 考虑平衡因子的更新。

// 旋转之前,60的平衡因子可能是-1/0/1,旋转完成之后,根据情况对其他节点的平衡因子进
行调整
void RotateLR(Node* parent)
		{
			Node* cur = parent->_left;
			Node* curright = cur->_right;
// 旋转之前,保存pSubLR的平衡因子,旋转完成之后,需要根据该平衡因子来调整其他节
点的平衡因子

			int bf = curright->_bf;

			RotateL(parent->_left);
			RotateR(parent);

			if (bf == 0)
			{
				parent->_bf = 0;
				cur->_bf = 0;
				curright->_bf = 0;
			}
			else if (bf == -1)
			{
				parent->_bf = 1;
				cur->_bf = 0;
				curright->_bf = 0;
			}
			else if (bf == 1)
			{
				parent->_bf = 0;
				cur->_bf = -1;
				curright->_bf = 0;
			}
		}

 4. 新节点插入较高右子树的左侧---右左:先右单旋再左单旋

void RotateRL(Node* parent)
		{
			Node* cur = parent->_right;
			Node* curleft = cur->_left;
			int bf = curleft->_bf;

			RotateR(parent->_right);
			RotateL(parent);

			if (bf == 0)
			{
				cur->_bf = 0;
				curleft->_bf = 0;
				parent->_bf = 0;
			}
			else if (bf == 1)
			{
				cur->_bf = 0;
				curleft->_bf = 0;
				parent->_bf = -1;
			}
			else if (bf == -1)
			{
				cur->_bf = 1;
				curleft->_bf = 0;
				parent->_bf = 0;
			}
			else
			{
				assert(false);
			}
		}

总结:

假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑

1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR

  • 当pSubR的平衡因子为1时,执行左单旋
  • 当pSubR的平衡因子为-1时,执行右左双

2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL

  • 当pSubL的平衡因子为-1是,执行右单旋当
  • pSubL的平衡因子为1时,执行左右双旋

旋转完成后,原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。

5.1.4 AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么 AVL树的插入过程可以分为两步:

1. 按照二叉搜索树的方式插入新节点

2. 调整节点的平衡因子

bool Insert(const pair<K, V>& kv)
		{
			Node* parent = nullptr;
			Node* cur = _root;
			if (_root == nullptr)
			{
				Node* newnode = new Node(kv);
				_root = newnode;
				return true;
			}
			else
			{
				while (cur)
				{
					if (cur->_kv.first > kv.first)
					{
						parent = cur;
						cur = cur->_left;
					}
					else if (cur->_kv.first < kv.first)
					{
						parent = cur;
						cur = cur->_right;
					}
					else
					{
						return false;
					}
				}
			}

			cur = new Node(kv);
			if (parent->_kv.first > kv.first)
			{
				parent->_left = cur;
			}
			else
			{
				parent->_right = cur;
			}
			cur->_parent = parent;
			//뫘劤틱뷜凜綾

			while (parent)
			{
				if (cur == parent->_left)
				{
					parent->_bf--;
				}
				else
				{
					parent->_bf++;
				}

				if (parent->_bf == -1 || parent->_bf == 1)
				{
					cur = parent;
					parent = parent->_parent;
				}
				else if (parent->_bf == 0)
				{
					break;
				}
				else if(parent->_bf == 2 || parent->_bf == -2)
				{
					
					if (parent->_bf == 2 && cur->_bf == 1)
					{
						//璘데旗
						RotateL(parent);
					}
					else if (parent->_bf == -2 && cur->_bf == -1)
					{
						//塘데己
						RotateR(parent);
					}
					else if (parent->_bf == 2 && cur->_bf == -1)
					{
						RotateRL(parent);
					}
					else if (parent->_bf == -2 && cur->_bf == 1)
					{
						RotateLR(parent);
					}
					break;
				}
				else
				{
					assert(false);
				}
			}
			return true;
		}

		void RotateRL(Node* parent)
		{
			Node* cur = parent->_right;
			Node* curleft = cur->_left;
			int bf = curleft->_bf;

			RotateR(parent->_right);
			RotateL(parent);

			if (bf == 0)
			{
				cur->_bf = 0;
				curleft->_bf = 0;
				parent->_bf = 0;
			}
			else if (bf == 1)
			{
				cur->_bf = 0;
				curleft->_bf = 0;
				parent->_bf = -1;
			}
			else if (bf == -1)
			{
				cur->_bf = 1;
				curleft->_bf = 0;
				parent->_bf = 0;
			}
			else
			{
				assert(false);
			}
		}

		void RotateLR(Node* parent)
		{
			Node* cur = parent->_left;
			Node* curright = cur->_right;
			int bf = curright->_bf;

			RotateL(parent->_left);
			RotateR(parent);

			if (bf == 0)
			{
				parent->_bf = 0;
				cur->_bf = 0;
				curright->_bf = 0;
			}
			else if (bf == -1)
			{
				parent->_bf = 1;
				cur->_bf = 0;
				curright->_bf = 0;
			}
			else if (bf == 1)
			{
				parent->_bf = 0;
				cur->_bf = -1;
				curright->_bf = 0;
			}
		}

		void RotateL(Node* parent)
		{
			Node* cur = parent->_right;
			parent->_right = cur->_left;
			if (cur->_left)
				cur->_left->_parent = parent;

			cur->_left = parent;
			Node* pphead = parent->_parent;
			parent->_parent = cur;

			if (pphead == nullptr)
			{
				_root = cur;
				cur->_parent = nullptr;
			}
			else
			{
				if (pphead->_left == parent)
				{
					pphead->_left = cur;
				}
				else
				{
					pphead->_right = cur;
				}
				cur->_parent = pphead;
			}
			parent->_bf = cur->_bf = 0;
		}

		void RotateR(Node* parent)
		{
			Node* cur = parent->_left;
			parent->_left = cur->_right;
			if (cur->_right)
				cur->_right->_parent = parent;

			cur->_right = parent;
			Node* pphead = parent->_parent;
			parent->_parent = cur;

			if (pphead == nullptr)
			{
				_root = cur;
				cur->_parent = nullptr;
			}
			else
			{
				if(pphead->_left == parent)
				{
					pphead->_left = cur;
				}
				else
				{
					pphead->_right = cur;
				}
			}
			parent->_bf = cur->_bf = 0;
		}
5.1.5 AVL树的删除(了解)

因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不 错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。

可参考【C++融会贯通】二叉树进阶-CSDN博客

5.1.6 AVL树的性能 

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这 样可以保证查询时高效的时间复杂度,即$log_2 (N)$。但是如果要对AVL树做一些结构修改的操 作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时, 有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数 据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。


5.2  红黑树

5.2.1 红黑树的概念

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

5.2.2 红黑树的性质
  1. 每个结点不是红色就是黑色
  2. 根节点是黑色的 
  3. 如果一个节点是红色的,则它的两个孩子结点是黑色的 
  4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点 
  5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)
5.2.3 红黑树节点的定义
enum Colour
	{
		Red,
		Black
	};

	template<class K,class V>
	struct RBTreeNode
	{
		RBTreeNode(const pair<K, V>& kv)
			:_left(nullptr)
			, _right(nullptr)
			, _parent(nullptr)
			, col(Red)
			,_kv(kv)
		{}

		RBTreeNode<K,V>* _left;
		RBTreeNode<K,V>* _right;
		RBTreeNode<K,V>* _parent;

		Colour col;
		pair<K, V> _kv;
	};
5.2.4 红黑树结构

为了后续实现关联式容器简单,红黑树的实现中增加一个头结点,因为跟节点必须为黑色,为了 与根节点进行区分,将头结点给成黑色,并且让头结点的 pParent 域指向红黑树的根节点,pLeft 域指向红黑树中最小的节点,_pRight域指向红黑树中最大的节点,如下:

5.2.5 红黑树的插入操作

红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:

1. 按照二叉搜索的树规则插入新节点

2.检测新节点插入后,红黑树的性质是否造到破坏

因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何 性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连 在一起的红色节点,此时需要对红黑树分情况来讨论:

约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点

情况一: cur为红,p为红,g为黑,u存在且为红

cur和p均为红,违反了性质三,此处能否将p直接改为黑?

解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。  

情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑

p为g的左孩子,cur为p的左孩子,则进行右单旋转;相反,

p为g的右孩子,cur为p的右孩子,则进行左单旋转

p、g变色--p变黑,g变红  

情况三: cur为红,p为红,g为黑,u不存在/u存在且为黑

p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;相反,

p为g的右孩子,cur为p的左孩子,则针对p做右单旋转

则转换成了情况2  

针对每种情况进行相应的处理即可:

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

			//判断是否还维持着红黑树
			while (parent && parent->col == Red)
			{
				Node* grandfather = parent->_parent;
				if (parent == grandfather->_left)
				{
					Node* uncle = grandfather->_right;
					//存在且为红
					if (uncle && uncle->col == Red)
					{
						//变色
						uncle->col = parent->col = Black;
						grandfather->col = Red;
						//继续向上调整
						cur = grandfather;
						parent = cur->_parent;
					}
					else//不存在或者存在且为黑
					{
						//     g
						//   p
						// c
						if (cur == parent->_left)
						{
							RotateR(grandfather);
							parent->col = Black;
							grandfather->col = Red;
						}
						else
						{
							//  g
							//t
							//  h
							RotateL(parent);
							RotateR(grandfather);
							cur->col = Black;
							grandfather->col = Red;
						}
						break;
					}
				}
				else   // parent == grandfather->_right
				{
					Node* uncle = grandfather->_left;
					// u存在且为红
					if (uncle && uncle->col == Red)
					{
						// 变色
						parent->col = uncle->col = Black;
						grandfather->col = Red;

						// 继续向上处理
						cur = grandfather;
						parent = cur->_parent;
					}
					else
					{
						if (cur == parent->_right)
						{
							// g
							//	  p
							//       c
							RotateL(grandfather);
							grandfather->col = Red;
							parent->col = Black;
						}
						else
						{
							// g
							//	  p
							// c
							RotateR(parent);
							RotateL(grandfather);
							cur->col = Black;
							grandfather->col = Red;
						}

						break;
					}
				}
			}

			_root->col = Black;

			return true;
		}


		void RotateL(Node* parent)
		{
			++_rotateCount;

			Node* cur = parent->_right;
			Node* curleft = cur->_left;

			parent->_right = curleft;
			if (curleft)
			{
				curleft->_parent = parent;
			}

			cur->_left = parent;

			Node* ppnode = parent->_parent;

			parent->_parent = cur;


			if (parent == _root)
			{
				_root = cur;
				cur->_parent = nullptr;
			}
			else
			{
				if (ppnode->_left == parent)
				{
					ppnode->_left = cur;
				}
				else
				{
					ppnode->_right = cur;

				}

				cur->_parent = ppnode;
			}
		}


		void RotateR(Node* parent)
		{
			++_rotateCount;

			Node* cur = parent->_left;
			Node* curright = cur->_right;

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

			Node* ppnode = parent->_parent;
			cur->_right = parent;
			parent->_parent = cur;

			if (ppnode == nullptr)
			{
				_root = cur;
				cur->_parent = nullptr;
			}
			else
			{
				if (ppnode->_left == parent)
				{
					ppnode->_left = cur;
				}
				else
				{
					ppnode->_right = cur;
				}

				cur->_parent = ppnode;
			}
		}
4.2.6 红黑树的删除

红黑树的删除本节不做讲解,有兴趣的同学可参考:《算法导论》或者《STL源码剖析》

红黑树 - _Never_ - 博客园

4.2.7 红黑树与AVL树的比较

红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O($log_2 N$),红黑树不追 求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数, 所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红 黑树更多。

4.2.8 红黑树的应用

1. C++ STL库 -- map/set、mutil_map/mutil_set

2. Java 库

3. linux内核

4. 其他一些库


结尾:

如果有什么建议和疑问,或是有什么错误,希望大家可以在评论区提一下。
希望大家以后也能和我一起进步!!
如果这篇文章对你有用的话,请大家给一个三连支持一下!!

谢谢大家收看🌹🌹

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2243179.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LeetCode 力扣 热题 100道(五)最长回文子串(C++)

最长回文子串 给你一个字符串 s&#xff0c;找到 s 中最长的 回文子串。 回文性 如果字符串向前和向后读都相同&#xff0c;则它满足 回文性 子字符串子字符串 是字符串中连续的 非空 字符序列。 动态规划法 class Solution { public:string longestPalindrome(string s) {i…

Spring Boot汽车资讯:科技与汽车的新融合

摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了汽车资讯网站的开发全过程。通过分析汽车资讯网站管理的不足&#xff0c;创建了一个计算机管理汽车资讯网站的方案。文章介绍了汽车资讯网站的系统分析部分&…

逆向攻防世界CTF系列41-EASYHOOK

逆向攻防世界CTF系列41-EASYHOOK 看题目是一个Hook类型的&#xff0c;第一次接触&#xff0c;虽然学过相关理论&#xff0c;可以看我的文章 Hook入门(逆向)-CSDN博客 题解参考&#xff1a;https://www.cnblogs.com/c10udlnk/p/14214057.html和攻防世界逆向高手题之EASYHOOK-…

【网络】HTTP 协议

目录 基本概念基于 HTTP 的系统组成HTTP 的基本性质 HTTP 请求头 & 响应头HTTP 的请求方法HTTP 的返回码HTTP 的 CookieHTTP 缓存 Cache-Control会话HTTP/1.x 的连接管理 基本概念 HTTP&#xff08;Hypertext Transfer Protocol&#xff0c;超文本传输协议&#xff09;是一…

执行flink sql连接clickhouse库

手把手教学&#xff0c;flink connector打通clickhouse大数据库&#xff0c;通过下发flink sql&#xff0c;来使用ck。 组件版本jdk1.8flink1.17.2clickhouse23.12.2.59 1.背景 flink官方不支持clickhouse连接器&#xff0c;工作中难免会用到。 2.方案 利用GitHub大佬提供…

笔记|M芯片MAC (arm64) docker上使用 export / import / commit 构建amd64镜像

很简单的起因&#xff0c;我的东西最终需要跑在amd64上&#xff0c;但是因为mac的架构师arm64&#xff0c;所以直接构建好的代码是没办法跨平台运行的。直接在arm64上pull下来的docker镜像也都是arm64架构。 检查镜像架构&#xff1a; docker inspect 8135f475e221 | grep Arc…

免费送源码:Java+Springboot+MySQL Springboot多租户博客网站的设计 计算机毕业设计原创定制

Springboot多租户博客网站的设计 摘 要 博客网站是当今网络的热点&#xff0c;博客技术的出现使得每个人可以零成本、零维护地创建自己的网络媒体&#xff0c;Blog站点所形成的网状结构促成了不同于以往社区的Blog文化&#xff0c;Blog技术缔造了“博客”文化。本文课题研究的“…

代码随想录第46期 单调栈

这道题主要是单调栈的简单应用 class Solution { public:vector<int> dailyTemperatures(vector<int>& T) {vector<int> result(T.size(),0);stack<int> st;st.push(0);for(int i1;i<T.size();i){if(T[i]<T[st.top()]){st.push(i);}else{wh…

【Three.js基础学习】24. shader patterns

前言 课程回顾: ShaderMaterial 这里用的是着色器材质 所以顶点和片段着色器就不需要像原始着色器那样添加需要的属性 然后写 片段着色器需要属性 &#xff1a; 顶点 属性 -》变化 -》 片段中 顶点中的属性不需要声明 只需要声明传送的变量 例如 varying vec vUv; vUv uv; 补充…

力扣整理版七:二叉树(待更新)

满二叉树&#xff1a;如果一棵二叉树只有度为0的结点和度为2的结点&#xff0c;并且度为0的结点在同一层上&#xff0c;则这棵二叉树为满二叉树。深度为k&#xff0c;有2^k-1个节点的二叉树。 完全二叉树&#xff1a;在完全二叉树中&#xff0c;除了最底层节点可能没填满外&am…

173. 二叉搜索树迭代器【 力扣(LeetCode) 】

文章目录 零、原题链接一、题目描述二、测试用例三、解题思路四、参考代码 零、原题链接 173. 二叉搜索树迭代器 一、题目描述 实现一个二叉搜索树迭代器类BSTIterator &#xff0c;表示一个按中序遍历二叉搜索树&#xff08;BST&#xff09;的迭代器&#xff1a; BSTIterato…

自动驾驶系列—深入解析自动驾驶车联网技术及其应用场景

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

STL序列式容器之list

相较于vector的连续性空间&#xff0c;list相对比较复杂&#xff1b;list内部使用了双向环形链表的方式对数据进行存储&#xff1b;list在增加元素时&#xff0c;采用了精准的方式分配一片空间对数据及附加指针等信息进行存储&#xff1b; list节点定义如下 template<clas…

Element-ui Select选择器自定义搜索方法

效果图 具体实现 <template><div class"home"><el-selectref"currencySelect"v-model"currency"filterable:spellcheck"false"placeholder"请选择":filter-method"handleCurrencyFilter"change&q…

Linux debian系统安装ClamTk开源图形用户界面(GUI)杀毒软件

一、ClamTk简介 ClamTk 是一个基于 ClamAV 的开源图形用户界面&#xff08;GUI&#xff09;杀毒软件。它使用 GTK2-Perl 脚本构建而成&#xff0c;支持32位与64位操作系统。ClamTk 提供了一个直观的用户界面&#xff0c;使得用户无需深入了解命令行即可完成大部分操作。它具备…

MIT6.5840 Lab 1: MapReduce(6.824)

结果 介绍 在本实验中&#xff0c;您将构建一个MapReduce系统。您将实现一个调用应用程序Map和Reduce函数并处理文件读写的工作进程&#xff0c;以及一个将任务分发给工作进程并处理失败的工作进程的协调进程。您将构建类似于MapReduce论文的东西。&#xff08;注意&#xff1a…

Kafka进阶_1.生产消息

文章目录 一、Controller选举二、生产消息2.1、创建待发送数据2.2、创建生产者对象&#xff0c;发送数据2.3、发送回调2.3.1、异步发送2.3.2、同步发送 2.4、拦截器2.5、序列化器2.6、分区器2.7、消息可靠性2.7.1、acks 02.7.2、acks 1(默认)2.7.3、acks -1或all 2.8、部分重…

SpringBoot多环境配置的实现

前言 开发过程中必然使用到的多环境案例&#xff0c;通过简单的案例分析多环境配置的实现过程。 一、案例 1.1主配置文件 spring:profiles:active: prod server:port: 80801.2多环境配置文件 开发环境 blog:domain: http://localhost:8080测试环境 blog:domain: https:/…

鸿蒙HarmonyOS 地图定位到当前位置 site查询等操作

应用服务Map使用 地图定位 地点查询及导航 周边查询 点位标记定义等 地图定位 前提地图已经能正常显示&#xff0c;若不能显示请大家参考之前的那篇如何显示地图的博文 地图相关的api 位置效果图&#xff1a; module.json5配置权限 "requestPermissions": [{&…

AntFlow 0.11.0版发布,增加springboot starter模块,一款设计上借鉴钉钉工作流的免费企业级审批流平台

AntFlow 0.11.0版发布,增加springboot starter模块,一款设计上借鉴钉钉工作流的免费企业级审批流平台 传统老牌工作流引擎比如activiti,flowable或者camunda等虽然功能强大&#xff0c;也被企业广泛采用&#xff0c;然后也存着在诸如学习曲线陡峭&#xff0c;上手难度大&#x…