1. 关联式容器
在初阶阶段,我们已经接触过STL中的部分容器,比如:vector、list、deque、
forward_list(C++11)等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面
存储的是元素本身。那什么是关联式容器?它与序列式容器有什么区别?关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高。
2. 键值对
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然
有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。
SGI-STL中关于键值对的定义:
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
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){}
};
int main()
{
system("pause");
return 0;
}
3. 树形结构的关联式容器
3.1.2 set的使用
1. set的模板参数列表
![](https://i-blog.csdnimg.cn/blog_migrate/1f97e31985d06eb64377eabe662afc05.png)
![](https://i-blog.csdnimg.cn/blog_migrate/f658b9df235e8d1f689ed81f5a6e1438.png)
4. set的容量
5. set修改操作
6. set的使用举例
#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
using namespace std;
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) {}
};
void TestSet()
{
int array[] = {1, 3, 4, 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[0]));
cout << s.size() << endl;
//正向打印set的元素,Con打印结果中可以看出,set 可以去重
for (auto &e : s)
{
cout << e << " ";
}
cout << endl;
for (auto it = s.rbegin(); it != s.rend(); it++)
{
cout << *it << " ";
}
cout << endl;
cout << s.count(3) << endl;
}
int main()
{
TestSet();
system("pause");
return 0;
}
3.2 map
3.2.1 map的介绍
https://cplusplus.com/reference/map/map/?kw=map
3.2.2 map的使用
1. map的模板参数说明
2. map的构造
3. map的迭代器
4. map的容量与元素访问
问题:当key不在map中时,通过operator获取对应value时会发生什么问题?
3.3 multiset
3.3.1 multiset的介绍
cplusplus.com/reference/set/multiset/?kw=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(log_2 N)$
7. multiset的作用:可以对元素进行排序
3.3.2 multiset的使用
此处只简单演示set与multiset的不同,其他接口接口与set相同,同学们可参考set。
#include <iostream>
#include <set>
using namespace std;
void test01()
{
int arr[] = {2, 1, 3, 9, 6, 0, 5, 8, 4, 7};
multiset<int> s(arr, arr + sizeof(arr) / sizeof(arr[0]));
for (auto &e : s)
{
cout << e << " ";
}
cout << endl;
}
int main(int argc, char const *argv[S])
{
test01();
system("pause");
return 0;
}
3.4 multimap
3.4.1 multimap的介绍
官方文档:http://www.cplusplus.com/reference/map/multimap/?kw=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是可以
重复的
3.4.2 multimap的使用
multimap中的接口可以参考map,功能都是类似的。
注意:
1. multimap中的key是可以重复的。
2. multimap中的元素默认将key按照小于来比较
3. multimap中没有重载operator[]操作(同学们可思考下为什么?)。
4. 使用时与map包含的头文件相同
3.5 在OJ中的使用
class compare{
public:
bool operator()(const pair<string,int> & p1,const pair<string,int> & p2){
return p1.second>p2.second||p1.second==p2.second&&p1.first<p2.first;
}
} ;
class Solution {
public:
vector<string> topKFrequent(vector<string>& words, int k) {
// map<string,int> mp;
unordered_map<string,int> mp;
for(int i=0;i<words.size();i++){
mp[words[i]]++;
}
vector<string> ans;
vector<pair<string,int> > v(mp.begin(),mp.end());
sort(v.begin(),v.end(),compare());
for(size_t i=0;i<k;i++){
ans.push_back(v[i].first);
}
return ans;
}
};
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> s1;
unordered_set<int> s2;
vector<int> ans;
for(int i=0;i<nums1.size();i++){
s1.insert(nums1[i]);
}
for(int i=0;i<nums2.size();i++){
s2.insert(nums2[i]);
}
for(auto e:s1){
auto it=s2.find(e);
if(it!=s2.end())
ans.push_back(e);
}
return ans;
}
};
4. 底层结构
前面对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个
共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。
4.1 AVL 树
4.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$)
4.1.2 AVL树节点的定义
#include <iostream>
#include <set>
#include <map>
#include <list>
#include <unordered_map>
using namespace std;
template <class T>
struct AVTreeNode
{
AVTreeNode(const T &data) : _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr), _bf(0)
{
}
AVTreeNode<T> *_pLeft;
AVTreeNode<T> *_pRight;
AVTreeNode<T> *_pParent;
T _data;
int _bf;
};
int main()
{
AVTreeNode<int> t1(1);
// cout<<"Hello"<<endl;
system("pause");
return 0;
}
4.1.3 AVL树的插入
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么 AVL树的插入过程可以分为两步:
1. 按照二叉搜索树的方式插入新节点
class AVLTree
{
public:
bool Insert(const T &data)
{
// 1. 先按照二叉搜索树的规则将节点插入到AVL树中
// ...
// 2. 新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否
破坏了AVL树
// 的平衡性
/*
pCur插入后,pParent的平衡因子一定需要调整,在插入之前,pParent
的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:
1. 如果pCur插入到pParent的左侧,只需给pParent的平衡因子-1即可
2. 如果pCur插入到pParent的右侧,只需给pParent的平衡因子+1即可
此时:pParent的平衡因子可能有三种情况:0,正负1, 正负2
1. 如果pParent的平衡因子为0,说明插入之前pParent的平衡因子为正负1,插入后被调整
成0,此时满足
AVL树的性质,插入成功
2. 如果pParent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更
新成正负1,此
时以pParent为根的树的高度增加,需要继续向上更新
3. 如果pParent的平衡因子为正负2,则pParent的平衡因子违反平衡树的性质,需要对其进
行旋转处理
*/
while (pParent)
{
// 更新双亲的平衡因子
if (pCur == pParent->_pLeft)
pParent->_bf--;
else
pParent->_bf++;
// 更新后检测双亲的平衡因子
if (0 == pParent->_bf)
{
break;
}
else if (1 == pParent->_bf || -1 == pParent->_bf)
{
// 插入前双亲的平衡因子是0,插入后双亲的平衡因为为1 或者 -1 ,说明以双亲
为根的二叉树
// 的高度增加了一层,因此需要继续向上调整
pCur = pParent;
pParent = pCur->_pParent;
}
else
{
// 双亲的平衡因子为正负2,违反了AVL树的平衡性,需要对以pParent
// 为根的树进行旋转处理
if (2 == pParent->_bf)
{
// ...
}
else
{
// ...
}
}
}
return true;
}
};
4.1.4 AVL树的旋转
![](https://i-blog.csdnimg.cn/blog_migrate/4dc6ded9dc3934a2f04258cd8697be3b.png)
实现及情况考虑可参考右单旋。
![](https://i-blog.csdnimg.cn/blog_migrate/3a13d26e4e4510c6257ce6123ac4d15f.png)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
template <class T>
struct AVLTreeNode
{
AVLTreeNode(const T &data) : _pLeft(nullptr), _pRight(nullptr),
_pParent(nullptr), _data(data), _bf(0)
{
}
AVLTreeNode<T> *_pLeft; //该结点在左
AVLTreeNode<T> *_pRight; //节点的右子孩
AVLTreeNode<T> *_pParent; //节点的双亲
T _data;
int _bf; //平衡因子
};
class AVLTree
{
public:
bool Insert(const T &data)
{
// 1. 先按照二叉搜索树的规则将节点插入到AVL树中
// ...
// 2. 新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否
破坏了AVL树
// 的平衡性
/*
pCur插入后,pParent的平衡因子一定需要调整,在插入之前,pParent
的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:
1. 如果pCur插入到pParent的左侧,只需给pParent的平衡因子-1即可
2. 如果pCur插入到pParent的右侧,只需给pParent的平衡因子+1即可
此时:pParent的平衡因子可能有三种情况:0,正负1, 正负2
1. 如果pParent的平衡因子为0,说明插入之前pParent的平衡因子为正负1,插入后被调整
成0,此时满足
AVL树的性质,插入成功
2. 如果pParent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更
新成正负1,此
时以pParent为根的树的高度增加,需要继续向上更新
3. 如果pParent的平衡因子为正负2,则pParent的平衡因子违反平衡树的性质,需要对其进
行旋转处理
*/
while (pParent)
{
// 更新双亲的平衡因子
if (pCur == pParent->_pLeft)
pParent->_bf--;
else
pParent->_bf++;
// 更新后检测双亲的平衡因子
if (0 == pParent->_bf)
{
break;
}
else if (1 == pParent->_bf || -1 == pParent->_bf)
{
// 插入前双亲的平衡因子是0,插入后双亲的平衡因为为1 或者 -1 ,说明以双亲
为根的二叉树
// 的高度增加了一层,因此需要继续向上调整
pCur = pParent;
pParent = pCur->_pParent;
}
else
{
// 双亲的平衡因子为正负2,违反了AVL树的平衡性,需要对以pParent
// 为根的树进行旋转处理
if (2 == pParent->_bf)
{
// ...
}
else
{
// ...
}
}
}
return true;
}
// 旋转之前,60的平衡因子可能是-1/0/1,旋转完成之后,根据情况对其他节点的平衡因子进
// 行调整
void _RotateLR(PNode pParent)
{
PNode pSubL = pParent->_pLeft;
PNode pSubLR = pSubL->_pRight;
// 旋转之前,保存pSubLR的平衡因子,旋转完成之后,需要根据该平衡因子来调整其他节
点的平衡因子
int bf = pSubLR->_bf;
// 先对30进行左单旋
_RotateL(pParent->_pLeft);
// 再对90进行右单旋
_RotateR(pParent);
if (1 == bf)
pSubL->_bf = -1;
else if (-1 == bf)
pParent->_bf = 1;
}
};
int main()
{
system("pause");
return 0;
}
![](https://i-blog.csdnimg.cn/blog_migrate/78638d425dc87f8afd69be240ec5e184.png)
4.1.5 AVL树的验证
![](https://i-blog.csdnimg.cn/blog_migrate/9bc766c81be11656f04cdc19757cf8d4.png)
4.1.6 AVL树的删除(了解)
4.1.7 AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这 样可以保证查询时高效的时间复杂度,即$log_2 (N)$。但是如果要对AVL树做一些结构修改的操 作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时, 有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数 据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
4.2 红黑树
4.2.1 红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路 径会比其他路径长出俩倍,因而是接近平衡的。
4.2.2 红黑树的性质
4.2.3 红黑树节点的定义
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
enum Color
{
RED,
BLACK
};
template <class valueType>
struct RBTreeNode
{
RBTreeNode(const valueType &data = valueType(), Color color = RED) : _pLeft(nullptr), _pRight(nullptr),
_pParent(nullptr), _data(data), _color(color) {}
RBTreeNode<valueType> *_pLeft;
RBTreeNode<valueType> *_pRight;
RBTreeNode<valueType> *_pParent;
valueType _data; //节点的值域
Color _color; //结点的颜色
};
int main()
{
system("pause");
return 0;
}