标准关联容器分为set和map两大类,包括multiset和multimap,这些容器的底层机制都是RB-tree.标准之外的关联容器有hashtable 以及以此hash table为底层机制而完成的hash_set(散列集合) hash_map(散列映射表) hash_multiset hash_multimap.
序列和关联容器各自的内部关系是内含的,例如heap内含一个vector,priority_quehe内含一个heap,stack和queue都内含一个deque,set/map/multiset/multimap都内含一个RB-tree,hash_x都内含一个hashtable
当元素被插入到关联式容器中时,容器内部结构便依照其键值大小,以某种特定规则将这个元素放置于适当位置。关联式容器没有所谓头尾,只有最大元素和最小元素。
二叉搜素树规定:任何节点的键值一定大于其左子树中的每一个节点的键值,并小于其右子树中的每一个节点的键值。
AVL-tree,二叉平衡树调整的规则,如果某一个子树平衡被打破,那么根据新插入的节点的位置可以分为以下几种:(X是被打破平衡的那个子树的根节点)
插入点位于X的左子节点的左子树—左左
插入点位于X的左子节点的右子树—左右
插入点位于X的右子节点的左子树—右左
插入点位于X的右子节点的右子树—右右
对于1、4彼此对称,成为外侧插入,可以采用单旋转操作调整;对于2、3,称为内侧插入,可以采用双旋转操作。
关于红黑树,我们来看几个问题,首先对于红黑树有几点规则,任何一个节点非红即黑;跟节点为黑;任一节点到NULL(树尾端)的任何路径,所含黑节点数必须相同;如果节点为红,其子节点必须为黑。
根据上面的几点规则,那么规定我们在插入新的节点时必须为红,假设插入节点为X,父亲为P,祖父为G,曾祖父为GG,伯父节点为S。
插入的节点X必须为叶节点,如果X的父亲为黑,那么不用调整,如果X的父亲为红,那么必须调整。如果P为红,那么G必定是黑。这个时候只需要根据X的位置和GG和S的颜色来决定下一步如何调整了。
这么一来就明白了在红黑树的调整中为何以S节点作为前提来调整的原因了。一定要清楚此事P一定是红色的。那么知道了在调整时是以S来作为调整规则就好办了,判断S是红还是为黑。
如果S为红,同时P为红,G为黑,那么只需看X是外侧插入还是内侧插入,这两种情况比较简单。
如果S为黑,同时P为红,G为黑,这个时候就需要看GG是红还是黑,这两种情况比较复杂,尤其是GG是红的时候。
在需要调整的前提下(P为红)可根据S分为两种情况,同时每种情况下又可以分为两种情况。这么一来就说明了在红黑树的插入时为何要以S的节点颜色分情况的原因。
(注释:在红黑树中插入一个新节点X,节点颜色必须为红,那么为何以S节点来判断呢。首先明白和X相关的都有什么节点,有P G GG S,如果P为黑,那么不用调整,如果P为红,那么必须调整,同时G为黑,那么剩下相关的就只有S和GG了,这个时候就需要根据S来判断下一步怎么调整了。)虽说在理论分析的时候是这么说的,但是程序实现的时候可能就不按照理论来写成程序了,可能需要先判断父亲节点是左节点还是右节点。
在STL中对于红黑树的插入操作有比较新颖的方法,这里假设插入新的节点为A,那么就沿着A的路径向上走,只要看到有某节点X的两个子节点均为红色,那么就把X改为红色,并把两个子节点改为黑色。这么调整以后,如果A的父节点P也为红色(注意,此时S绝不可能为红,为什么呢,因为如果在经过上面的调整以后P仍然为红,同时S也为红,那么根据调整的规则不久把这一对子节点给调整了么),做一次单旋转并改变颜色,或者双旋转闭关改变颜色。再次插入节点就简单了,要么直接插入,要么插入后再一次单旋转。这么处理的过程和平时常见的红黑树的插入操作还是有所不同的。
Set的特性是,所有元素都会根据元素的键值自动被排序。Set的元素不像map那样可以同时拥有实值和键值,set元素的键值就是实值,实值就是键值。Set不允许两个元素又相同的键值。
同时不能通过set的迭代器改变set的元素值,这一点在EffectiveSTL中也有介绍,因为set元素值就是其键值,关系到set元素的排列规则。如果任意改变set元素值,会严重破坏set组织,set<T>::iterator被定义为底层RB-tree的const_iterator,杜绝写入操作。
面对关联式容器,应该使用其本身提供的find函数来搜寻元素,会比使用STL算法find()更有效率,因为STL算法find()只是循序搜寻
Map特性是所有元素根据元素的键值自动排序,map的所有元素都是pair,同时拥有实值和键值,pair的第一元素被视为键值,第二元素被视为实值。Map不允许两个元素拥有相同的键值。是否可以通过map的迭代器改变Map的元素内容呢?如果想要修正元素的键值是不可的,因为Map元素的键值关系到map元素的排序规则。修改map元素键值会破坏map组织,如果想要修改元素的实值,是可以的。
Map和set都有自己的find()函数。至于multiset和multimap,都是使用了RB-tree中的insert_equal()函数,表明可以重复的键值插入。
至于hashtable,内部使用vector作为桶,每个桶内的链表是自己维护的。对于hashtable模板的参数相当多。Hashtable需要注意的一点就是自定义了几个现成的hash functions,全都是仿函数。Hashtable有一些无法处理的型别(除非用户为那些型别自定义hash funcitons),凡是hashtable无法处理者,hash_set也无法处理。对于使用hashtable作为底层机制的关联容器都自己定义了swap函数。也有自己的find()函数。
对于hash_sethash_multiset,模板参数最多使用三个,一个值,一个函数,一个判断相等函数。Hash_set 使用的方法和set相似。Hash_multiset的特性与multiset相同,关键底层实现不一样,hash_multiset的元素并不会被自动排序。
对于hash_maphash_multimap模板参数自定义最多有四个,一个键值,一个值,一个Hash函数,一个判断相等函数。因为hash_map存储的是pair,和map的使用方法相似。
总结:关联容器分为两类,一类是set/map/multiset/multimap,一类是hash_x/hash_multi*,前者是依照红黑树作为底层机制,后者是依照hashtable作为底层机制。所以前者天生就会自动排序,后者就不具有排序功能。同时要明白关联容器内部实现了一些函数,在和算法库中的某些算法有冲突是尽量使用内部实现的函数。同时还有通过迭代器来修改容器内部元素的问题,set和map还是有区别的。