C++ map和set

目录

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实现

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值