索引
关联式容器相关概念介绍
1.什么是关联式容器
之前所学的类似于vector,list,deque等,其底层都是线性序列的数据结构,这些容器统称为序列式容器,容器里面存储的是元素本身。而关联式容器也是用来存储数据的,但与序列式容器不同的是,其里面存储的是<key,value>结构的键值对,在检索数据时,往往通过检索key来找到value的值,效率比序列式容器高。
根据应用场景的不同,STL总共实现了两种不同结构关联式容器——树形结构与哈希结构,关联式容器一共有四种:map,set,multimap.multiset,他们的底层结构都是平衡树准确而言是红黑树,结构按照中序遍历的话默认是升序的。
2.pair介绍
用来表示具有对应关系的一种结构,该结构一般包含两个成员变量key和value,key代表键值,value表示与key对应的信息。在c++中常用pair这个结构体来连接
pair主要是将两个数据组合成一个数据,这两个数据可以是不同的类型,pair将两个值视为一个单元,同时pair是struct,所以可以直接存取pair中的个别值。
我们用pair定义对象还得写出类型
eg
pair<int,int>dir
而make_pair无需写出类型就可以直接生成一个pair对象所以,当有必要对一个接受pair参数的函数传递两个值时,make_pair()就显得很方便,>但是另外一个方面就是pair接受隐式类型转换,这样虽然如果使用make_pair虽然可以不用写类型,但是这也带来了隐患
eg:
make_pair(1,2.2)此时会将second变量转换成double
但是如果是pair<int,double>(1,2.2)就不会发生上述错误
注意 make_pair是一个内联函数
set与map
1.set
Key:set中存放元素的类型,实际在底层存储<value,value>的键值对
Compare:set中元素默认按照小于来比较
Allocator:set中元素空间的管理方式,使用STL提供的空间配置器管理
1.由源码可以看出其默认的仿函数是less根据key的大小排列而成,且如果用迭代器遍历的话默认结果是升序
2.set中的元素不能在容器中修改,但是可以从容器中插入或者删除他们
如图所示,set迭代器无论是const迭代器还是普通迭代器都是由const_iterator转变而成,其指向对象不能被修改
3.set中的元素不能重复,因此使用set可以达到去重的效果
4.在set中查找某个元素时,时间复杂度是Olog n,因为其底层结构是红黑树
5.set的函数介绍一个:
//返回set中值为x的元素的个数
size_type count (const key_type&x) const
6.multiset用法和set几乎一样,只是其可以允许元素的冗余,即在multiset中允许元素的重复
(1)set.erase
d.erase(34)
set<int>::iterator pos = d.find(34);
d.erase(pos);
上述代码已知:d中没有34
如果是erase中的参数是迭代器并且此时需要删除的值不存在,那么编译器会报错,但如果直接是erase(34),34此时不存在编译器就仿佛没看到他一样。
并且通过上述可以知道如果erase中的参数不是迭代器,那么此时返回的是删除此时key的个数,虽然set不允许数据的冗余,但是multi版本允许
map中erase和上述一样
2.map
Key:代表键值
T:代表键值对应的value
Compare:与set一样,默认按照小于比较(key)
Allocator:同上
1.在内部,map中的元素总是按照key进行比较排序,并且key不能改变但是value可以改变
2.map支持下标访问符,即在[]放入key,就可以找到对应的value,其实现的底层与insert有关
3.其底层也是平衡二叉树(红黑树)
(1).map中的insert与operator[]
在map中插入键值对x,并不是插入一个元素,返回值也是一个键值对,如果插入成功,返回值中的iterator表示的是新插入元素的位置,bool = true,如果插入失败,iterator指向map中本身就存在的相同的key,bool = false;
首先用(key,T())构建一个键值对,然后调用Insert()函数将该键值对插入到map中,如果key已经存在,插入失败,并且返回key所在位置的迭代器,此时再用指针解引用,就找到key键值对,再取second就可以找到key对应的value。
总结operator[]:两层意义:
1.查找k对应value
2.修改k对应的value(通过源码可以看出返回值是传引用返回)
注
在元素访问时,当key不存在值,operator[]默认此时key的value是T(),就是此时value的默认构造。
(2)思考为什么map要用pair封装
如果是*it,其调用operator*()
,c++中不支持一个函数返回两个值,其只需要返回结点的数据即可,而map这个容器有两个值,要符合运算符的行为,因此我们要将两个数据打包成一个pair,上述的operator*
只是一个举例,几乎所有函数都是一样的,
(3). multimap
multimap与上述的multiser不一样,multiset除允许数据冗余之外几乎和set的用法一致,multimap也支持数据冗余,但其最大的区别是不支持operator[],因为在multimap中有多个key,而operator[]就是依靠key寻找,此时会有矛盾
(4)一道map简单oj
前K个高频单词
关键:sort是不稳定的排序,并且在vector中最好是map<string,int>的迭代器,如果直接是pair,影响效率
拓展:stable_sort与sort相似,但其是稳定排序
代码题解
AVL树(平衡树)
1.AVL树相关概念
二叉搜索树虽然能缩短查找效率,但是不排除二叉搜索树有时会出现单支树,此时二叉搜索树查询的时间复杂度还是O(N)。
AVL树保证向二叉树插入新结点时,每个结点左右子树的高度差不超过1,且结点的左右子树都是AVL树,我们也将左右高度差称为平衡因子(调整二叉树)
左右子树是取最高的高度
满二叉树才能保证每个子树高度差为0,做不到左右两边完全相等,因为必须要满足性质,左子树上的结点都小于根节点,右子树上的结点都大于根节点,做不到相等,退而求其次,可以调整使左右高度差不超过1;
完全二叉树:最后一层缺一些结点
AVL树:最后两层缺一些结点
性质 如果一颗二叉树是AVL树,其查找的时间复杂度依旧是O(log2N)
2.AVL树核心实现
AVL树结点的定义
struct AVLTreeNode
{
AVLTreeNode<T>* _left;
AVLTreeNode<T>* _right;
AVLTreeNode<T>* _parent;
// 右子树-左子树的高度差
int _bf; // balance factor
T _kv;
AVLTreeNode(const T& kv)
:_kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
};
AVL树并没有规定必须要设计平衡因子只是一个实现的选择,方便控制平衡,设计三叉链条也是为了调整二叉树设计的。
注 AVL树不需要完全掌握代码实现,只需要知道其实现的思路,后面的红黑树也是一样,AVL树和红黑树只模拟实现insert,erase太过复杂
AVL树的插入大致分为两步
1.按照二叉搜索树的方式进行插入
2.调整节点的平衡因子
3.平衡因子调整后的绝对值大于1,调整树
pParent:父节点
p:被插入节点
pcur:插入节点
(1)AVL树的四种旋转
a.右单旋 新节点插入较高左子树的左侧—左左
b.左单旋 新节点插入较高右子树的右侧—右右
c.左右单旋 新节点插入较高左子树的右侧—左右
代码直接复用上述的左单旋,右单旋即可
d.右左单旋 新节点插入较高右子树的左侧—右左
3.AVL分析总结
AVL树插入的时候按照平衡二叉树的规则插入,插入之后只看自己祖先的平衡因子,如果违反规则,直接旋转即可,而且旋转后子树的高度是不变的
AVL树是一颗绝对平衡的二叉树,每个节点左右子树高度差不超过1,这使得查找的时间复杂度为log(N),但是要维持这个结构需要旋转的次数较多,尤其是在删除时,有可能一直旋转持续到根的位置,所以:如果需要一种查询且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树。