文章目录
一. 关联式容器简介
所谓关联式容器,观念上类似关联式数据库:每笔数据(每个元素)都有一个键值(key)和一个实值(value)。当元素被插到关联式容器中时,容器内部结构便按照其键值大小,以某种特定规则将这个元素放置于适当位置。
关联式容器没有头尾,所以不会有所谓
push_back()
,pop_back()
,push_front()
,pop_front()
。
首先,关联式容器结构为一个value,value内部为一个键值对构成,也就是 key:data
结构,不过对于set(集合)类型的数据结构来说,它的key就是value本身,对于map(映射表)来说,key只是value中的一小部分。因为这些差异,所以对于关联式容器来说,要有一个从value中取出key的规则,这个将在后续介绍。
PS:从“键值对”这个说法来说,本应是
key:value
结构,python中也采用了这个写法,而STL中value表示数字的全部,键值为key:data
,源码中也全部是这个写法,因此采用与源码相同的说法。
二.使用红黑树作为底层的容器
(1)红黑树 RB-tree
1. 什么是红黑树
二叉搜索树
对于一棵二叉树,我们可以赋予其特殊的规则:对于任意一棵子树,其根节点的值比其左子树的所有制都小,比右子树的所有值都大(因此没有重复值)。经过这样的处理后,数据变得有序了。经过中序遍历,我们便可以得到其排序后的结果。
但是二叉搜索树有其局限性,其非常依赖于插入数据的顺序,因为若数据为升序或者降序(即非随机性质),那根节点会偏小/大,导致“失衡”,即左右子树的高度差距过大,最极端的情况为一条直线,导致查找的效率退化为O(n)。
平衡二叉搜索树 AVL tree
为了解决二叉搜索树面对极端情况下的表现,出现了严格的平衡二叉搜索树,其规定左右子树的高度差距不能大于1,因此若插入一个值后,会导致一系列的重新调整(该操作被称为旋转,将在后面介绍)。
平衡二叉树的规则使得其深度为log(n),搜索效率很高,但是频繁的调整也导致其插入时有许多额外操作,因此插入效率不一定高。
红黑树
红黑树就是节点有红色和黑色两种颜色的二叉搜索树,也被称为“高度平衡的二叉搜索树”。它独特的结构保证了任意节点到尾端的任何路径,黑节点数相同,而红节点的数目小于等于黑节点的数目,因此保证了最深的子树的深度不会大于最小深度的2倍。
因为不是严格的平衡,红黑树在插入时进行的调整比较小,同时也保证了左右子树基本的平衡,所以插入效率和查找效率都很高,被广泛应用于各种数据结构。
红黑树的规则
- 每个节点不是红色就是黑色
- 根节点为黑色
- 若节点为红,其子节点必须为黑——不会有两个相连的红色节点
- 任一节点至叶子节点的任何路径,所含黑节点数必须相同
2. 红黑树的调整——旋转
在AVL tree和红黑树中,面对需要调整的情况,这种时候要使用旋转操作:因为只要保证根节点的左子树比其小,右子树比其大即可,因此可以将其左孩子或者右孩子往上移动,转为父节点,而父节点会响应地变为右孩子/左孩子(左孩子变为父节点,那原父节点比左孩子大,因此是右孩子,另一边同理)
在父节点变为孩子以及孩子变为父节点的过程中,父节点为“认领”孩子多出的那部分子树。比如右旋过程中,左孩子变为父节点,原左孩子的现左孩子为原父节点,那左孩子的原左子树就多出来了,交给原父节点作为其左子树。
3. STL中红黑树的基本结构
节点的结构
红黑树中常常需要向上操作,因此要保存一个指针指向其父节点,因此有三个指针,指向其左孩子、右孩子、父节点。
但是根节点是一个特例——其没有父节点,为了处理这个问题,STL中的红黑树进行了巧妙的设计。
节点中还需要保存其value和颜色两个值。
特殊的header
在红黑树中,设有一个物理意义上的根节点header,它的parent指向逻辑意义上的根节点root,header的left指针指向最左下侧的节点(也就是最小的节点),right指针指向最右下侧的节点(也就是最大的节点)。同时root的parent指针也指向header,即root和header互为父节点。
这样设计的好处是,就像链表的头指针一样,不需要对整个树没有节点时那样进行特别处理(因为至少会有一个header),同时header也维护了最小和最大的元素。
同时在迭代器begin和end的设计中,可以很容易想到,header->right(最小节点)即begin,而header本身作为一个不应该被访问到的节点,即作为end。
4. 红黑树的迭代器
红黑树的迭代器即指向树中的某个节点,这里详细介绍一下其移动操作。
++
假如当前节点有右孩子,那往右移动即可;但是假如没有,那便需要往上查找,查找的方法为:
寻找包含这个节点的最小左子树的根节点。原理为:++即找到当前节点大的最小值,因此需要找到包括其的左子树,而左子树的根节点比左子树的节点都大,这便是下一个节点。
--
同理,若当前节点有左孩子,那直接移动即可,若不存在,则需要找到
包含这个节点的最小右子树的根节点
,原理与++相同。
5. 红黑树的数据结构
刚刚已经介绍了header,header可以以O(1)的复杂度直接访问根节点、begin,本身也是end,因此header已经可以代表一棵二叉树。
成员
- header
- 一个size_t变量,存储树的大小
- 比较器(用于比较节点的key的大小)(本身是一个实例化的仿函数)
构造红黑树需要提供的接口
- 比较器:比较key的大小
- KeyofValue:用于从value中取出key,这在set中有不同的使用
- Alloc:这个一般使用默认的alloc
6. 红黑树的元素操作
插入
insert_unique
不允许插入相同的值:在检测到相同值时说明无法插入
其返回一个pair,为插入/查找位置,后一个值为bool,即插入结果
假如每次对当前节点判断是往左/右,同时还要判断其是否与插入值相等,开销太大了,因此算法会进行上溯——假如它在左子树里,那这个左子树的根节点是否与其相等(这也就是迭代器的
--
操作),相等说明重复了。insert_equal
操作与
insert_unique
相同,但是允许重复的值调整
进行插入后可能会导致红黑树失衡,因此要进行旋转和颜色改变,来使其符合要求。
查找
每次查找都要比较大于/小于/等于,是一笔不小的开销,因此查找也采用了巧妙的方法:其维护了一个值,指向遍历到的最后一个大于等于查找的值的位置,即
!comp(key(x),k)
为true,因此若这个值与查找的值相等,说明找到了。同时查找返回值是一个迭代器,找到则在对应位置,没找到则为end
值得注意的是,RB-tree中的comp(比较器)表示的是
<
操作,即comp(a,b)
=>a<b
,同样,在算法组件中,比较器也表示<
,而没有任何一个接口表示>
,我认为这是为了统一接口,毕竟a>b
就是b<a
,a>=b
就是!a<b
,都可以用<
来完全表示。
(2)set
set的特点
set即集合,内部通过封装红黑树实现,因为是红黑树实现的,所以数据结构是有序的。
同时set不允许重复,因此使用的是红黑树中的unique_insert。
set的值不允许迭代器修改,因为那样会破坏红黑树有序的结构,因此虽然提供迭代器作为遍历的接口,set的迭代器都是const类型,提供的引用也是const类型。
set的API
- insert
- 插入值
- 插入值,同时提供参考位置,这个位置只作为参考,最后还是会进行调整
- 插入一个范围的值(提供两个迭代器)
- 删除 erase
- 删除一个位置
- 删除一个值
- 删除一个范围
- find 非常常用,用于查找值的位置
- count 因为只有1/0个对应的值,所以常用来判断值时候存在
- lower_bound/upper_bound
(3)map
map的特点
- 和set一样,map底层也是红黑树结构,不过map表示的是键值对,因此是
key:data
结构。 - 因为键和value不同(里面还有data),map通常使用pair来实现键值的对应,first为key,second为data。map内部调用了
select1st
作为 KeyofValue的接口 - 因为map的值允许修改,因此map提供的迭代器是
pair<const Key,T>
类型的,迭代器不是const类型,但是其中的key仍然为const
map的API
- insert
- 插入值
- 插入值,以及参考位置
- 插入范围
- 删除 erase
- 删除值
- 删除位置
- 删除范围
- find 常用,用来判断值是否存在(和end比较)
- operator[] : 使用类似访问数组下标的方式,[]内为对应的key
- []不是单纯的查找,其调用红黑树的insert,因此实际上是有“副作用”的
- 因为insert在插入失败后虽然返回false,但是也顺路返回元素所在位置(插入成功自然也返回新插入的位置)。因此[] 内部调用insert插入
key:T的默认构造临时对象 T()
,假如key已存在那插入失败,假如key不存在则插入T() (*((insert(value_type(k,T()))).first).second)
这是其中使用isnert的语句- 这样的好处是平常对其赋值不需要确认值是否存在,比如value是一个vector,那不管其是否存在,可以直接使用语句
map[key].push_back(xx)
对其操作 - 不过取值的时候需要判断值是否存在,毕竟任何情况都有返回值
(4) multiset
- 看名字可以得出,和
set
本质的结构和API都一模一样,区别在于multiset
可以存储相同的值 - 这是通过调用红黑树的equal_insert实现的
(5)multimap
- multimap同理,与map相同,但是key允许重复
- 因为key可能不止一个对应的value,所以multimap不允许使用
[]
运算符(没有进行重载)