00 写在前面
前面已经把序列式容器(sequence)看的差不多了,接下来我们来看看关联式容器(associative)。
就像我们在第一篇中说的一样,关联式容器分为set和map两大类,以及他们对应的衍生体multiset和multimap,还有也可以被看作关联式容器的unordered类(也就是原来非标准的hashtable)
【STL源码剖析】总结笔记(2):容器(containers)概览
01 关联式容器
关联式容器中每个元素都有一个key和一个value,当元素被插入到关联式容器中时,会根据容器内部的规则对元素进行适当位置的放置。关联式容器没有头尾的概念。
对于set和map以及它们的衍生结构来说,底层是依靠RB-tree来实现的。而对于unordered类来说,它们的底层实现是hash-table。
02 从树到红黑树
树
树是一种非常常用的数据结构。由节点和边组成。整棵树的最上端是根节点,其余节点通过边相连。
二叉树
子节点可以存在多个,如果最多只允许两个子节点,那么就是二叉树。
二叉搜索树
二叉搜索树是对二叉树节点的放置制定了规则:任何节点的eky一定大于其左子树节点的每一个key,并且小于其右子树的每一个节点的key。
二叉搜索树的插入比较好理解,就是按照左右顺序比较key然后插入。
我们来说一下二叉搜索树的删除操作(后面会用到这个思想)
-
如果要删除的节点只有一个子节点
那么直接将其子节点连到父节点
-
如果要删除的节点有两个子节点
习惯用有右子树的最小值代替,然后删除。
平衡二叉搜索树
二叉搜索树也有缺陷,如果一直添加比前一个节点小或者大的节点,就会出现非常不平衡的情况。
平衡的原则就是不要有任何一个节点深度过大,导致效率变差。
进一步细分,在平衡二叉搜索树中又有很多分类。
AVL tree
AVL tree在二叉搜索树的基础上附加了额外的平衡条件。由于我们很难保持左右节点的子树都有着相同的高度,所以AVL tree要求任何节点的左右子树高度相差最多1。
而在插入新的节点时,就有可能出现不平衡的情况,那么就需要AVL tree进行旋转操作。旋转操作在平衡二叉树中是很常用的操作。
我们在插入时会遇到以下几种情况:
- 插入点在最深节点的左子节点的左子树
- 插入点在最深节点的右子节点的左子树
- 插入点在最深节点的右子节点的右子树(对应1)
- 插入点在最深节点的左子节点的右子树(对应2)
对应情况只需要改变旋转方向即可,所以我们来看1和2
-
插入点在最深节点的左子节点的左子树
这时候绿色的18是违反平衡原则的节点,可以进行单旋转,同时修改16的指向。
单旋转用来修正外侧插入导致的不平衡。
-
插入点在最深节点的右子节点的左子树
这时候单旋转就不能解决这个内测插入的问题了,因为旋转完还是不平衡。
但是旋转两次即可平衡,所以也称为双旋转。
先将子树做一次左旋转,再做一次右旋转。
在AVL tree中的旋转操作也是红黑树中旋转操作的基础。其实红黑树的修正就是旋转与变色的结合,只是情况较为复杂。
红黑树(RB-tree)
红黑树核心的就是它在二叉搜索树的基础上加入的规则:
- 节点是红色或者黑色
- 根节点是黑色
- 每个叶子节点都是黑色(nil是叶子节点,代表无值)
- 红色节点的两个子节点必须为黑色(也就是从根到叶子的路径上不能有两个连续的红色节点)
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
优点:
红黑树引入颜色的概念简化了平衡的条件。
红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高。
来看一个典型的红黑树
可以看出从根节点出发到叶子节点的路径中黑色节点的数目是一样的。
并且在黑色节点后插入红色节点也不会破坏规则,但是要注意不能插入两个连续的红色节点。
而我们要做的调整就是在插入或删除破坏规则时对红黑树进行“变色”和“旋转”
03 红黑树的操作
红黑树的插入与删除会有很多种情况,删除的情况会更多一些。其实我们可以想到,比如当有两个红色节点时,需要变色调整,但随意变色后可能会出现某条路径多出黑色节点,不满足黑色节点的数量一致。
这时候就要分情况讨论了。
**特别注意:**情况只是局部展示,三角代表下面的子树,有多种情况,但一定保证在插入前红黑树是平衡的。图中的黑色节点数不一致不代表整棵树不一致。
红黑树的插入
情况1
新节点N是根,没有父节点。
这种情况下可以直接变色,使每条路径上的黑色节点数目同时+1
情况2
新节点N的父节点是黑色的。
这种情况下不会打破红黑树的平衡,红色节点不影响。
情况3
新节点N的父亲节点和叔叔节点(祖父节点除了父亲外的另一个孩子)都是红色
这种情况下会出现两个连续的红色节点,违反规则,所以我们先让父节点变为黑色。但是这样左边的路径会多出一个黑色节点,所以我们将叔叔节点也变为黑色。
情况4
新节点N的父节点是红色,叔叔节点是黑色或者没有,且新节点是父节点的右孩子。父节点是祖父节点的左孩子。
这种情况下我们以父节点为轴做一次左旋转,使得N成为新的父节点。这时会进入情况5。
情况5
新节点N的父节点是红色,叔叔节点是黑色或者没有,且新节点是父节点的左孩子,父节点是祖父节点的左孩子。
这种情况下我们以祖父节点为轴做一次右旋转,父节点成为新的祖父节点。
此时黑色节点的数目不一致,需要变色。我们将祖父节点变为黑色,将兄弟节点变为红色。
这时就符合一开始的规则了。
红黑树的删除操作非常繁琐,有兴趣的小伙伴可以自行查阅。
基本原则是首先遵循二叉搜索树的删除规则,再在此基础上对于不符合红黑树要求的部分进行调整。
04 红黑树的结构设计
红黑树的内部结构我们只需要做一些简单的了解即可。
红黑树的迭代器
红黑树提供iterator的++和–操作,可以得到各个节点的排序状态。
但是不应该使用iterator去修改红黑树的值,因为红黑树的元素有严谨的排序状态。
红黑树为set和map服务,set的key不可以改变,map中元素的data可变但是key同样也不可改变。
红黑树的结构
template <class key,class Value,class KeyOfValue,class Compare,class Alloc=alloc>
class rb_tree{
protected:
typedef _rb_tree_node<Value> rb_tree_node;
...
public:
typedef rb_tree_node* link_type;
...
protected:
//三笔资料
size_type node_count;//1
link_type header;//2
Compare ket_compare;//3
...
}
首先看模板定义,这五个参数比较重要。
红黑树需要知道key的类型,value的类型(这里的value是指key和data合在一起),同时还要知道如何从value中取出key,以及如何做比较。
而红黑树实际对外的只有三个参数
- rb_tree的节点数
- 指向rb_tree节点的指针
- key的大小比较的准则
这里的header类似list中的虚头节点,指向父节点。
05 总结
了解红黑树是后面对set和map深入了解的前提。红黑树的各种操作有大概印象即可。
最后补充一下AVL和红黑树的区别:
AVL是高度平衡的,所以在查找方面效率会比较高。
而红黑树在平衡方面不是非常严格,但是其插入和删除等操作因为不需要太多的旋转,所以效率上要比AVL要高。