C++ map和set

news2024/9/23 23:35:10

目录

1. 关联式容器

2. 键值对

3. 树形结构的关联式容器

3.1 set

3.1.1 set的介绍

3.1.2 set的使用

 3.2 map

3.2.1 map的介绍

3.2.2 map的使用

3.3 multiset

3.3.1 multiset的介绍

3.3.2 multiset的使用

3.4 multimap

3.4.1 multimap的介绍

3.5 在OJ中的使用

4. 底层结构(难)

4.1 AVL 树

4.1.1 AVL树的概念

 4.1.2 AVL树节点的定义

4.1.3 AVL树的插入(重点理解原理)

4.1.4 AVL树的旋转(难)

4.1.5 AVL树的验证

4.1.6 AVL树的删除(了解)

4.1.7 AVL树的性能

4.2 红黑树

4.2.1 红黑树的概念

4.2.2 红黑树的性质

4.2.3 红黑树节点的定义

4.2.4 红黑树的插入操作

4.2.5 红黑树的验证

4.2.6 红黑树的删除

4.2.7 红黑树与AVL树的比较

4.2.8红黑树完整实现代码

4.3 红黑树模拟实现STL中的map与set

4.3.1 红黑树的迭代器

4.3.2 改造红黑树

4.3.3 set的模拟实现 && map的模拟实现 


1. 关联式容器

vector list deque 、forward_list(C++11)等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面 存储的是元素本身。
关联式容器 也是用来存储数据的,与序列式容器不同的是,其 里面存储的是 <key, value> 结构的 键值对,在数据检索时比序列式容器效率更高

2. 键值对

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

3. 树形结构的关联式容器

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

3.1 set

3.1.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 中存储的是真正的键值对 <key, value> set 中只放
value ,但在底层实际存放的是由 <value, value> 构成的键值对。
2. set中插入元素时,只需要插入value即可,不需要构造键值对
3. set中的元素不可以重复(因此可以使用set进行去重)。
4. 使用set的迭代器遍历set中的元素,可以得到有序序列
5. set 中的元素默认按照小于来比较
6. set 中查找某个元素,时间复杂度为: O(logN)
7. set 中的元素不允许修改(如果修改会破坏结构)
8. set 中的底层使用二叉搜索树 ( 红黑树 ) 来实现。

3.1.2 set的使用

1. set 的模板参数列表

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

Compare set 中元素默认按照小于来比较,这里我们可以通过写仿函数进行自己想要的序列
Alloc set 中元素空间的管理方式,使用 STL 提供的空间配置器管理
2. set 的构造:空构造,迭代器区间构造,拷贝构造

 3. set的迭代器 :正向迭代器以及反向迭代器,及其const版本:

 4.set修改操作(重点)

主要看看这个insert:

 后面的和我们之前学习过的都类似:

删除:

查找,返回该节点的迭代器

 

返回该元素出现的个数,这里set并没有什么作用,因为set是不允许出现重复的元素的,但是对multiset来说就有这个意义了,因为multiset允许出现重复的元素。

 

 其他的可以在C++网站自行查找,使用成本不高。

5.set的使用举例

int main()
{
	// 用数组array中的元素构造set
	int array[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0, 1, 3, 5, 7, 9, 2, 4,6, 8, 0 };
	int sz = sizeof(array) / sizeof(array[0]);
	//迭代器区间初始化
	set<int> s(array,array+sz);
	//打印
	for (auto& e : s)
	{
		cout << e << " ";
	}
	cout << endl;
	//反向打印
	for (auto i = s.rbegin(); i != s.rend(); ++i)
	{
		cout << *i << " ";
	}
	cout << endl;
	// set中值为3的元素出现了几次,因为实现了去重,所以元素都是1次
	cout << s.count(3) << endl;
	return 0;
}

结果:

 3.2 map

3.2.1 map的介绍

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

3.2.2 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() 函数直接抛异常
这里operator[]的重载实现等我们看完find和insert的接口后再看
5. map 中元素的修改

 

 剩下的和set其实很类似自行查文档C++网站

下面我们来解释一下operator[]的函数重载实现:

operator[]兼具3个功能:查找,插入,修改

 operator[]的原理是:

  <key, T()> 构造一个键值对,然后调用 insert() 函数将该键值对插入到 map
  如果 key 已经存在,插入失败, insert 函数返回该 key 所在位置的迭代器
  如果 key 不存在,插入成功, insert 函数返回新插入元素所在位置的迭代器
  operator[] 函数最后将 insert 返回值键值对中的 value 返回
insert需注意:
map 中的键值对 key 一定是唯一的,如果 key 存在将插入失败
map的使用:
int main()
{
	map<string, string> m;
	m.insert(pair<string, string>("字符串","string"));
	//直接使用make_pair就可以不用写那么复杂
	m.insert(make_pair("banan", "香蕉"));
	// 将<"apple", "">插入map中,插入成功,返回value的引用,将“苹果”赋值给该引用结果,
	m["apple"] = "苹果";
	for (auto& e : m)
	{
		cout << e.first << " " << e.second << endl;
	}
	cout << endl;
    // 删除key为"apple"的元素
	m.erase("apple");
	if (1 == m.count("apple"))
		cout << "apple还在" << endl;
	else
		cout << "apple被删除" << endl;
	return 0;
}

结果:这里的count一般都是看key是否在map中,如果是multimap就是多个key,这样就可以找到其中的个数

 【总结】

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

3.3 multiset

3.3.1 multiset的介绍

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

3.3.2 multiset的使用

和set的使用几乎相同,只是其中允许重复元素的出现

3.4 multimap

3.4.1 multimap的介绍

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

3.5 OJ中的使用

前K个高频单词

思路:我们可以使用map来对string进行排序,然后放入vector中,用stable_sort稳定地对次数排序,这样可以保证string的相对顺序不变。最后取数组中的前k个元素即可

class Solution {
public:
    struct compare{
        bool operator()(const pair<int,string>& l,const pair<int,string>& r)
        {
            return l.first > r.first;
        }
    };
    vector<string> topKFrequent(vector<string>& words, int k) {
        //把元素放入map中,然后把数据放入vector中再针对次数进行排序,其中要注意稳定性
        map<string,int> m;
        for(auto& e:words)
        {
            m[e]++;
        }
        vector<pair<int,string>> v;
        //将map元素放入v中
        for(auto& e:m)
        {
            v.push_back(make_pair(e.second,e.first));
        }
        //进行稳定排序
        stable_sort(v.begin(),v.end(),compare());
        //插入结果集中
        vector<string> result;
        for(int i = 0;i<k;++i)
        {
            result.push_back(v[i].second);
        }
        return result;
    }
};

两个数组的交集

思路:使用set进行排序加去重,之后遍历两个set找相同的元素插入结果集中

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        //使用set进行排序加去重,然后在两个set中找相同的元素
        set<int> s1(nums1.begin(),nums1.end());
        set<int> s2(nums2.begin(),nums2.end());
        vector<int> result;
        auto it1 = s1.begin();
        auto it2 = s2.begin();
        //比较谁的元素小就++谁的iterator
        while(it1 != s1.end() && it2 != s2.end())
        {
            if(*it1 == *it2)
            {
                result.push_back(*it1);
                ++it1;
                ++it2;
            }
            else if(*it1 > *it2)
            {
                ++it2;
            }
            else{
                ++it1;
            }
        }
        return result;
    }
};

4. 底层结构(难)

4.1 AVL

4.1.1 AVL树的概念

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

 4.1.2 AVL树节点的定义

template <class K ,class V>
struct AVLTreeNode
{
	pair<K, V> _kv;
	AVLTreeNode* _left;
	AVLTreeNode* _right;
	AVLTreeNode* _parent;
	//平衡因子  balance factor
	int _bf;
	//构造
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}
};

4.1.3 AVL树的插入(重点理解原理)

AVL 树就是在二叉搜索树的基础上引入了平衡因子,因此 AVL 树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:
1. 按照二叉搜索树的方式插入新节点
2. 调整节点的平衡因子
思路:
1. 先按照二叉搜索树的规则将节点插入到 AVL 树中
2. 新节点插入后, AVL 树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否
破坏了 AVL 树的平衡性
是否破环平衡性可以根据平衡因子来确定,如果平衡因子的绝对值超过2就说明要旋转调整了。

4.1.4 AVL树的旋转(难)

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

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

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

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

 总结:

假如以p Parent 为根的子树不平衡,即p Parent 的平衡因子为 2 或者 -2 ,分以下情况考虑
1. pParent的平衡因子为2,说明pParent的右子树高 ,设 pParent 的右子树的根为 pSubR
当pSubR的平衡因子为1时,执行左单旋
当pSubR的平衡因子为-1时,执行右左双旋
2. pParent的平衡因子为-2,说明pParent的左子树高 ,设 pParent 的左子树的根为 pSubL
当pSubL的平衡因子为-1是,执行右单旋
当pSubL的平衡因子为1时,执行左右双旋
旋转完成后,原 pParent 为根的子树个高度降低,已经平衡,不需要再向上更新。

4.1.5 AVL树的验证

AVL 树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证 AVL 树,可以分两步:
1. 验证其为二叉搜索树
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
2. 验证其为平衡树
每个节点子树高度差的绝对值不超过 1( 注意节点中如果没有平衡因子 )
节点的平衡因子是否计算正确
完整实现代码:
template <class K ,class V>
struct AVLTreeNode
{
	pair<K, V> _kv;
	AVLTreeNode* _left;
	AVLTreeNode* _right;
	AVLTreeNode* _parent;
	//平衡因子  balance factor
	int _bf;
	//构造
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}
};

template <class K ,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	//插入,和搜索二叉树类似
	bool insert(const pair<K, V>& kv)
	{
		//第一个
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_parent = nullptr;
			return true;
		}
		Node* cur = _root;
		Node* parent = nullptr;
		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->_right = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
		//判断平衡因子是否符合AVL树
		while (parent)//_root的parent是空
		{
			//插入节点在parent的左就--_bf
			//插入节点在parent的右就++_bf
			if (parent->_left == cur)
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}
			//判断平衡因子是否正确
			//如果parent的bf为0说明之前不平衡,现在平衡了
			//parent的bf为-1或者1说明parent原来是0即平衡,新增节点会改变更上面的节点的bf
			//parent的bf为-2或者2就需要赶紧调平
			if (parent->_bf == 0)
			{
				break;
			}
			else if (parent->_bf == -1 || parent->_bf == 1)
			{
				//更新上面节点的bf
				cur = parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == -2 || parent->_bf == 2)
			{
				//旋转
				//parent为-2 cur为-1需要右旋
				if (parent->_bf == -2 && cur->_bf == -1)
				{
					RotateR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == 1)
				{
					RotateL(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					RotateLR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					RotateRL(parent);
				}
				break;
			}
			else
			{
				//不存在,但是你有可能写错,预防错误
				assert(false);
			}
		}
		return true;
	}
	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;
		//ppNode可能为空
		if (ppNode == nullptr)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subL;
			}
			else
			{
				ppNode->_right = subL;
			}
			subL->_parent = ppNode;
		}
		//更新平衡因子
		parent->_bf = subL->_bf = 0;
	}
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		parent->_right = subRL;
		if (subRL)
		{
			subRL->_parent = parent;
		}
		Node* ppNode = parent->_parent;
		parent->_parent = subR;
		subR->_left = parent;
		if (_root == parent)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}
			subR->_parent = ppNode;
		}
		//更新平衡因子
		parent->_bf = subR->_bf = 0;
	}
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		//记录subLR的bf,在其左子树插入则parent最终bf就为1,在其右子树插入则subL最终为-1
		int bf = subLR->_bf;
		//先左旋,再右旋
		RotateL(subL);
		RotateR(parent);
		//更新平衡因子
		if (bf == 0)
		{
			//这种就是插入subLR的情况,一共3个节点,刚好平衡
			parent->_bf = 0;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == -1)//在subLR左子树插入
		{
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 1)//在subLR右子树插入
		{
			parent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else
		{
			//不存在这种情况
			assert(false);
		}
	}
	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;
		//在左插入subR最终bf为1,在右插入parent的bf最终为-1
		RotateR(subR);
		RotateL(parent);
		if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == 1)//在右插入
		{
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == -1)//在左插入
		{
			parent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}
		else 
		{
			assert(false);
		}
	}
	void Inorder()
	{
		_Inorder(_root);
	}
	bool Isbalance()
	{
		return _Isbalance(_root);
	}
private:
	int Height(Node* _root)
	{
		if (_root == nullptr)
			return 0;
		int lh = Height(_root->_left);
		int rh = Height(_root->_right);
		return lh > rh ? lh + 1 : rh + 1;
	}
	bool _Isbalance(Node* _root)
	{
		if (_root == nullptr)
			return true;
		int lh = Height(_root->_left);
		int rh = Height(_root->_right);
		if (rh - lh != _root->_bf)
		{
			cout <<_root->_kv.first<< "平衡因子异常" << endl;
			return false;
		}
		//查看子树
		return abs(rh - lh) < 2 && _Isbalance(_root->_left) && _Isbalance(_root->_right);
	}
	void _Inorder(Node* _root)
	{
		if (_root == nullptr)
			return;
		_Inorder(_root->_left);
		cout << _root->_kv.first << endl;
		_Inorder(_root->_right);
	}
	Node* _root = nullptr;
};

void TestAVLTree1()
{
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16 , 14 };
	AVLTree<int, int> t;
	for (auto& e : a)
	{
		t.insert(make_pair(e, e));
	}
	t.Inorder();
}

void TestAVLTree2()
{
	srand(time(0));
	AVLTree<int, int> t;
	for (int i = 0; i < 100000; ++i)
	{
		int x = rand();
		t.insert(make_pair(x, x));
	}
	cout << t.Isbalance() << endl;
}

4.1.6 AVL树的删除(了解)

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

4.1.7 AVL树的性能

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

4.2 红黑树

4.2.1 红黑树的概念

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

4.2.2 红黑树的性质

1. 每个结点不是红色就是黑色
2. 根节点是黑色的 
3. 如果一个节点是红色的,则它的两个孩子结点是黑色的 (不能存在连续的红节点)
4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点 
(所有路径的黑色节点数是相同的)
5. 每个叶子结点都是黑色的 ( 此处的叶子结点指的是空结点 )
这些性质就保证了最长路径最多是最短路径的两倍,最短路径情况就是全都是黑色的节点,最长的情况就是一黑一红。

4.2.3 红黑树节点的定义

//定义红黑
enum Colour
{
	RED,
	BLACK,
};

//红黑树节点结构
template<class K, class V>
struct RBTreeNode
{
	pair<K, V> _kv;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	Colour _col;

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

4.2.4 红黑树的插入操作

红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:
1. 按照二叉搜索的树规则插入新节点
2. 检测新节点插入后,红黑树的性质是否造到破坏
因为 新节点的默认颜色是红色 ,因此:如果 其双亲节点的颜色是黑色,没有违反红黑树任何
性质 ,则不需要调整;但 当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连
在一起的红色节点 ,此时需要对红黑树分情况来讨论:
约定 :cur 为当前节点, p 为父节点, g 为祖父节点, u 为叔叔节点
情况一 : cur 为红, p 为红, g 为黑, u 存在且为红

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

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

 总结:最主要看uncle,如果uncle存在且为红,那么就是情况一,直接变色,然后迭代向上更新即可;如果uncle为其他情况,都是旋转加变色,具体怎么旋转和变色看具体情况,旋转的方式和AVL树是一样的,变色的话主要是为了满足规则。

4.2.5 红黑树的验证

红黑树的检测分为两步:
1. 检测其是否满足二叉搜索树 ( 中序遍历是否为有序序列 )
2. 检测其是否满足红黑树的性质
bool IsvalidRBTree()
	{
		//空树是红黑树
		if (_root == nullptr)
			return true;
		//根是黑色
		if (_root->_col != BLACK)
		{
			cout << "不满足规则2:根节点必须为黑色" << endl;
			return false;
		}
		//每个路径,黑色节点数量相同
		Node* cur = _root;
		int blackcount = 0;//记录黑色节点数量
		while (cur)
		{
			if (cur->_col == BLACK)
				++blackcount;
			cur = cur->_left;
		}
		int k = 0;//用来记录每个路径的黑色节点个数
		return _IsvaildRBTree(_root, k, blackcount);
	}
bool _IsvaildRBTree(Node* root, int k,const int blackcount)
	{
		if (root == nullptr)
		{
			if (k != blackcount)
			{
				cout << "不满足规则4,每个路径的黑色节点数相同" << endl;
				return false;
			}
			return true;
		}
		Node* parent = root->_parent;
		//往上找是否存在相连的红色节点
		if (parent && parent->_col == root->_col && root->_col == RED)
		{
			cout << "存在相连的红节点,不满足规则3" << endl;
			return false;
		}
		if (root->_col == BLACK)
			++k;
		return _IsvaildRBTree(root->_left, k, blackcount) && _IsvaildRBTree(root->_right, k, blackcount);
	}
 

4.2.6 红黑树的删除

这里不做讲解,可以看看这里:红黑树的插入删除

4.2.7 红黑树与AVL树的比较

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

4.2.8红黑树完整实现代码

//定义红黑
enum Colour
{
	RED,
	BLACK,
};

//红黑树节点结构
template<class K, class V>
struct RBTreeNode
{
	pair<K, V> _kv;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	Colour _col;

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

//红黑树实现
template <class K, class V>
class 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* cur = _root;
		Node* parent = nullptr;
		//找到空,插入节点
		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->_parent = parent;
		if (parent->_kv.first > kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		//判断是否满足红黑树的规则
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			//parent在grandfather的左
			if (grandfather->_left == parent)
			{
				Node* uncle = grandfather->_right;
				//分3种情况
				//1.uncle存在&&uncle和parent都是红,直接把他们改成黑,然后grandfather改成红迭代上去即可
				if (uncle && uncle->_col == RED)
				{
					uncle->_col = parent->_col = BLACK;
					grandfather->_col = RED;
					//迭代向上更新,有可能grandfather上面是红
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					//情况2或者3
					//情况2:cur在parent左边,形成直线型,直接右旋变色即可,parent变成黑,grandfather变成红
					if (parent->_left == cur)
					{
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//情况3:形成折线型,先左旋parent再右旋grandfather然后变色,cur最终变黑,grandfather变红
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					//无论是情况2还是3,最终上面的节点都是黑,就不需要更新了,直接跳出
					break;
				}
			}
			else
			{
				//parent在grandfather的右
				Node* uncle = grandfather->_left;
				//情况1
				if (uncle && uncle->_col == RED)
				{
					uncle->_col = parent->_col = BLACK;
					grandfather->_col = RED;
					//迭代
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					//情况2
					if (parent->_right == cur)
					{
						//左旋加变色,parent变黑,grandfather变红
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//情况3:先右旋再左旋,cur变黑,grandfather变红
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
				}
			}
		}
		//最终根一定是黑的
		_root->_col = BLACK;
	}
	void Inorder()
	{
		_Inorder(_root);
	}
	bool IsvalidRBTree()
	{
		//空树是红黑树
		if (_root == nullptr)
			return true;
		//根是黑色
		if (_root->_col != BLACK)
		{
			cout << "不满足规则2:根节点必须为黑色" << endl;
			return false;
		}
		//每个路径,黑色节点数量相同
		Node* cur = _root;
		int blackcount = 0;//记录黑色节点数量
		while (cur)
		{
			if (cur->_col == BLACK)
				++blackcount;
			cur = cur->_left;
		}
		int k = 0;//用来记录每个路径的黑色节点个数
		return _IsvaildRBTree(_root, k, blackcount);
	}
private:
	bool _IsvaildRBTree(Node* root, int k,const int blackcount)
	{
		if (root == nullptr)
		{
			if (k != blackcount)
			{
				cout << "不满足规则4,每个路径的黑色节点数相同" << endl;
				return false;
			}
			return true;
		}
		Node* parent = root->_parent;
		//往上找是否存在相连的红色节点
		if (parent && parent->_col == root->_col && root->_col == RED)
		{
			cout << "存在相连的红节点,不满足规则3" << endl;
			return false;
		}
		if (root->_col == BLACK)
			++k;
		return _IsvaildRBTree(root->_left, k, blackcount) && _IsvaildRBTree(root->_right, k, blackcount);
	}

	void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;
		_Inorder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_Inorder(root->_right);
	}
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		parent->_left = subLR;
		if (subLR)
		{
			subLR->_parent = parent;
		}
		Node* ppNode = parent->_parent;
		parent->_parent = subL;
		subL->_right = parent;
		if (ppNode == nullptr)//等价于_root == parent
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subL;
			}
			else
			{
				ppNode->_right = subL;
			}
			subL->_parent = ppNode;
		}
	}
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		parent->_right = subRL;
		if (subRL)
		{
			subRL->_parent = parent;
		}
		Node* ppNode = parent->_parent;
		parent->_parent = subR;
		subR->_left = parent;
		//ppNode可能为空
		if (ppNode == nullptr)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		else
		{
			//parent可能是ppNode的左右孩子
			if (ppNode->_left == parent)
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}
			subR->_parent = ppNode;
		}
	}
	Node* _root = nullptr;
};

void TestRBTree1()
{
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	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)); 
		//cout << e << " "<< t.IsvalidRBTree() << endl;
	}
	cout << endl;
	t.Inorder();
}

void TestRBTree2()
{
	srand(time(0));
	RBTree<int, int> t;
	for (int i = 0; i < 100000; ++i)
	{
		int x = rand();
		t.insert(make_pair(x, x));
	}
	//判断是否满足红黑树的规则
	//t.Inorder();
	cout << t.IsvalidRBTree() << endl;
}

4.3 红黑树模拟实现STL中的mapset

4.3.1 红黑树的迭代器

迭代器的好处是可以方便遍历,是数据结构的底层实现与用户透明。如果想要给红黑树增加迭代器,需要考虑以前问题
begin() end():
begin() 可以放在红黑树中最小节点 ( 即最左侧节点 ) 的位置,将 end() 放在头结点的位置。
operator++() operator--():
实现这两个 重载的思路:如果是operator++的话,我们要寻找其 右节点的位置,如果存在,那么就找右子树一个节点的最左节点即可,如果右子树不存在,我们就想办法找该节点的parent,如果parent的右节点是该节点,那么就迭代更新,知道parent为空,或者parent的右节点不是该节点。

4.3.2 改造红黑树

我们可以看到stl的源码设计部分的模板参数是这样设计的:
template < class K , class ValueType , class KeyOfValue >
因为关联式容器中存储的是 <key, value> 的键值对,因此
 k key 的类型,
ValueType: 如果是 map ,则为 pair<K, V>; 如果是 set ,则为 k
KeyOfValue: 通过 value 来获取 key 的一个仿函数类
这么设计的原因就是为了让一颗红黑树就满足map和set两个容器。

4.3.3 set的模拟实现 && map的模拟实现 

细节比较多,主要体现在了迭代器那么,这里迭代器的实现其实和list很相似,只是insert以及重载那里多了不少细节。

这里就贴上链接,有红黑树,set,map的模拟实现

红黑树,map,set实现

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

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

相关文章

Android框架源码分析-浅析OkHttp3

浅析OkHttp3 这篇文章主要用来回顾Okhttp3源码中&#xff0c;同步异步请求的区别、拦截器的责任链模式、连接池管理以及探讨socket通信到底在哪里实现。 列出的代码可能删掉了非核心部分的展示&#xff0c;如果有异议请查看源码 连接池涉及知识&#xff1a;可能根据 IP 地址…

iis7.5应用程序池的启动模式设置

最近发现公司的网站第一次登录时比较慢&#xff0c;甚至有超时的时候&#xff0c;当我检查应用程序池(IIS 7.5)时&#xff0c;应用程序池正常启动&#xff0c;但有时候处于停止状态&#xff0c;停止原因未知。所以必须第一时间重新启动它&#xff0c;以保证网站能被正常访问。于…

kubeadm Dashboard harbor

主机名IP地址安装组件master01192.168.186.10docker、kubeadm、kubelet、kubectl、flannelnode01192.168.186.20docker、kubeadm、kubelet、kubectl、flannelnode02192.168.186.30docker、kubeadm、kubelet、kubectl、flannelharbor192.168.186.40docker、docker-compose、harb…

python语言基础(最详细版)

文章目录一、程序的格式框架缩进1、定义2、这里就简单的举几个例子注释二、语法元素的名称三、数据类型四、数值运算符五、关系运算六、逻辑运算七、运算符的结合性八、字符串一、程序的格式框架 缩进 1、定义 &#xff08;1&#xff09;python中通常用缩进来表示代码包含和…

Python迭代器、生成器和装饰器

一、迭代器 1、迭代器简介 迭代操作是访问集合元素的一种方式&#xff0c;是 Python最强大的功能之一。 迭代器是用来迭代取值的工具&#xff0c;是一个可以记住遍历的位置的对象。 迭代器对象从集合的第一个元素开始访问&#xff0c;直到所有的元素被访问完结束。迭代器只能…

QT项目_RPC(进程间通讯)

QT项目_RPC(进程间通讯) 前言&#xff1a; 两个进程间通信、或是说两个应用程序之间通讯。实际情况是在QT开发的一个项目中&#xff0c;里面包含两个子程序&#xff0c;子程序有单独的界面和应用逻辑&#xff0c;这两个子程序跑起来之后需要一些数据的交互&#xff0c;例如&…

GEE学习笔记 八十四:【GEE之Python版教程十四】矢量数据(ee.feature)

上一节讲了几何图形Geometry&#xff0c;这一节讲矢量数据&#xff08;ee.feature&#xff09;&#xff0c;它的构成也就是几何图形以及属性字典。 1、API定义 首先看一下GEE的python版API&#xff08;Welcome to GEE-Python-API’s documentation! — GEE-Python-API 1.0 do…

【论文】智能隧道检测车的现状及改进策略

本文转载自《智慧城轨》2022年第11期 作者&#xff1a;黄丹樱1,韦强1,朱椰毅2,范骁1,林浩立1 单位&#xff1a;1 浙江师范大学工学院&#xff1b;2 浙江金温铁道开发有限公司 声明&#xff1a;本文仅用于学术分享&#xff0c;不做商业用途&#xff0c;如有侵权&#xff0c;联…

从实现到原理,聊聊Java中的SPI动态扩展

原创&#xff1a;微信公众号 码农参上&#xff0c;欢迎分享&#xff0c;转载请保留出处。 八股文背多了&#xff0c;相信大家都听说过一个词&#xff0c;SPI扩展。 有的面试官就很喜欢问这个问题&#xff0c;SpringBoot的自动装配是如何实现的&#xff1f; 基本上&#xff0c…

Redis第二讲

二、Redis02 2.1 发布和订阅 Redis 发布订阅 (pub/sub) 是一种消息通信模式&#xff1a;发送者 (pub) 发送消息&#xff0c;订阅者 (sub) 接收消息。 Redis 客户端可以订阅任意数量的频道。 发布订阅的实现 1、打开一个客户端订阅channel1 127.0.0.1:6379> subscribe ch…

红黑树的原理+实现

文章目录红黑树定义性质红黑树的插入动态效果演示代码测试红黑树红黑树 定义 红黑树是一个近似平衡的搜索树&#xff0c;关于近似平衡主要体现在最长路径小于最短路径的两倍&#xff08;我认为这是红黑树核心原则&#xff09;&#xff0c;为了达到这个原则&#xff0c;红黑树所…

LeetCode刷题--- 面试题 01.07. 旋转矩阵(原地旋转+翻转替旋转)

&#x1f48c; 所属专栏&#xff1a;【LeetCode题解&#xff08;持续更新中&#xff09;】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;…

【C++之容器篇】二叉搜索树的理论与使用

目录前言一、二叉搜索树的概念二、二叉搜素树的模拟实现&#xff08;增删查非递归实现&#xff09;1. 二叉搜素树的结点2. 二叉搜索树的实现&#xff08;1&#xff09;. 二叉搜索树的基本结构&#xff08;2&#xff09;构造函数&#xff08;3&#xff09;查找函数&#xff08;4…

1225057-68-0,Alkyne PEG4 TAMRA-5,四甲基罗丹明-四聚乙二醇-炔基TAMRA红色荧光染料连接剂

中英文别名&#xff1a;CAS号&#xff1a;1225057-68-0 | 英文名&#xff1a;5-TAMRA-PEG4-Alkyne |中文名&#xff1a;5-四甲基罗丹明-四聚乙二醇-炔基物理参数&#xff1a;CASNumber&#xff1a;1225057-68-0Molecular formula&#xff1a;C36H41N3O8Molecular weight&#x…

P16 激活函数与Loss 的梯度

参考&#xff1a;https://www.ngui.cc/el/507608.html?actiononClick这里面简单回顾一下PyTorch 里面的两个常用的梯度自动计算的APIautoGrad 和 Backward, 最后结合 softmax 简单介绍一下一下应用场景。目录&#xff1a;1 autoGrad2 Backward3 softmax一 autoGrad输入 x输出损…

buu [UTCTF2020]basic-crypto 1

题目描述&#xff1a; 01010101 01101000 00101101 01101111 01101000 00101100 00100000 01101100 01101111 01101111 01101011 01110011 00100000 01101100 01101001 01101011 01100101 00100000 01110111 01100101 00100000 01101000 01100001 01110110 01100101 00100000 0…

【Kubernetes】【七】命令式对象配置和声明式对象配置

命令式对象配置 命令式对象配置就是使用命令配合配置文件一起来操作kubernetes资源。 1&#xff09; 创建一个nginxpod.yaml&#xff0c;内容如下&#xff1a; apiVersion: v1 kind: Namespace metadata:name: dev---apiVersion: v1 kind: Pod metadata:name: nginxpodnames…

调用Windows安全中心实现登录验证

文章目录运行效果用到的运行库代码实现使用日志Win10 Flat风格XP风格总结运行效果 输入用户名和密码点击确定后获取到的信息&#xff1a; 用到的运行库 NuGet搜索安装即可 Kang.ExtendMethodKang.ExtendMethod.Log https://gitee.com/haozekang/kang Vanara.PInvoke https:…

安全算法 - 国密算法

国密即国家密码局认定的国产密码算法。主要有SM1&#xff0c;SM2&#xff0c;SM3&#xff0c;SM4&#xff0c;SM7, SM9。国密算法分类国家标准官方网站如下&#xff1a;http://openstd.samr.gov.cn/bzgk/gb/SM1 为对称加密。其加密强度与AES相当。该算法不公开&#xff0c;调用…

Nacos——配置管理基础应用

目录 一、快速入门 1.1 发布配置 1.2 nacos client远程获取配置 1.2.1 导入坐标 1.2.2 程序代码 二、Nacos配置管理基础应用 2.1 Nacos配置管理模型 2.1.2 配置集(Data Id) 2.1.3 配置项 2.1.4 配置分组 (Group) 2.1.5 命名空间(Namespace) 2.1.6 最佳实践&#xff0…