关联式容器
关联式容器的每个数据都有一个 key 和一个 value,当元素被插入到关联式容器时,容器的内部结构(可能时 RB-tree,也可可能时 hash-table)会根据键值的大小,以某种特定规则将这个数据放置在适当的位置。关联容器没有所谓的头,尾,因此不会有类似于 push_back, push_front, pop_back(), pop_front(), begin, end 之类的函数。一般来说,关联式容器的内部是有个平衡二叉树(包括AVL-tree, RB-tree, AA-tree),其中在STL中使用最多的就是 RB-tree。
1. 数据结构——树
1. 二叉搜索树(BST)
二叉搜索树中任何节点的键值一定大于其左子树的每一个节点的键值,并小于右子树的每一个节点的键值。因此从跟节点一种往左走可以得到最小的键值,一直往右走可以得到最大的键值。
二叉搜索树的插入操作:从跟节点开始,遇键值较大者就向左,遇键值小者就向右,一直到尾端,即为插入点。
二叉搜索树的删除操作:想要删除节点A可以分为两种情况,如果A只有一个子节点,可以直接将A的子节点连接到A的父节点,并将A删除即可;如果A有两个子节点,就用右子树的最小节点(从右子节点开始一直往左走即可获得)取代A即可。
2. 平衡二叉搜索树(balanced binary search tree)
二叉搜索树有一个缺点就是当输入数据不够随机(比如说已经有序),二叉搜索树就可能失去平衡(比如退化成单链表),造成搜索效率低下。这里的平衡的意思就是没有任何一个节点深度过大。不同的平衡条件有不同的搜索效率,AVL-tree、RB-tree、AA-tree都可以实现平衡二叉树,它们一般都比普通的二叉搜索树复杂,因此插入节点喝删除节点的平均时间也比较长,但是可以避免高度不平衡的情况,访问元素的时间平均也比较少。
3. AVL-tree(Adelson-Velskii-Landis tree)
AVL-tree 是一种加上了额外的平衡条件的二叉搜索树,也叫二叉平衡搜索树,平衡条件的建立是为了确保整棵树的深度为 O(logN)。一般来说,最严格的平衡条件就是左右子树有着相同的高度(这也太难以实现了),因此 AVL-tree 退而求其次,要求任何节点的左右子树高度相差最多1。当一个节点插入到 AVL-tree ,使得平衡的状态被打破,就意味着某个节点X的左右子树高度差等于2,因此插入点和X的位置可以分为如下四种关系:
- 插入点位于X的左子节点的左子树——左左
- 插入点位于X的左子节点的右子树——左右
- 插入点位于X的右子节点的左子树——右左
- 插入点位于X的右子节点的右子树——右右
其中,情况1、4互为对称,称为外侧插入,可以使用单旋转操作;情况2、3互为对称,称为内侧插入,可以使用双旋转操作。 - 单旋转
在外侧插入的状态下,也就是出现“左左”或者“右右”的情况下,可以使用单旋转来修正不平衡的状态:
- 双旋转
在内侧插入的状态下,也就是出现“左右”或者“右左”的情况下,可以使用双旋转来修正不平衡的状态,所谓双旋转其实就是由两次单旋转合并而成,只要代码实现好左旋右旋的单旋转即可。
4. RB-tree( 红黑树 )
RB-tree 是另一种平衡二叉搜索树,平衡条件和 AVL-tree 有所不同,牺牲了部分平衡性( RB-tree 某些节点的左右子树高度差值可能大于1),但是同样使用单旋转、双旋转操作来修正树使其满足平衡条件,整体性能优于 AVL-tree。RB-tree 是 STL 中广泛使用的数据结构,除了要满足二叉搜索树的定义之外,还需要满足以下几个规则:
- 每个节点不是红色就是褐色;
- 跟节点是黑色;
- 如果节点是红色,那么其子节点必然是黑色;
- 任一节点到叶节点的任何路径,所含的黑节点数必须相同;
- 每个叶子节点都是黑色的空节点。
根据规则4:新节点必须为红
根据规则3:新节点的父节点必须是黑
有了这些规则的限制,保证了 RB-tree 的自平衡,红黑色从根节点到叶子的最长路径不会超过最短路径的两倍,这是因为最短的路径一定是全黑节点,而最长路径一定是红黑相间的节点,而要保持规则4,那么最长路径就刚好是最短路径的两倍。
RB-tree 的实现代码在 stl_tree.h 中:
- RB-tree 节点设计
1 | enum _Rb_tree_color { _S_red = false, _S_black = true }; // false是红色,true 是黑色 |
- RB-tree 迭代器
迭代器的实现关键在运算符 operator++、operator– 的实现,按照一般意义,在 RB-tree 中,这两个运算符分别用来获取当前节点的后一个节点和前一个节点,也就是当前节点后最小节点以及当前节点前最大节点。
为了实现这两个运算符的重载,STL写了两个查找的函数 increment() 和 decrement()。
1 | void increment() { |
1 | template<typename _Tp> |
- RB-tree 数据结构
为了便于处理 RB-tree 代码中的边界条件,使用两个标记节点 header 和 nil 来充当哨兵,拥有和普通节点一样的属性,header 颜色是红色,父指针指向跟节点,左指针指向最小节点,右指针指向最大节点。之后每次插入新节点不仅要按照 RB-tree 的规则作调整,而且维护 header 节点的正确性。同时所有的叶节点的左右子节点都指向节点 nil,用于在遍历时的结束标记。
header 和 root 互为对方的父节点:
- RB-tree 的构造和内存管理
- RB-tree 元素操作
元素插入操作
insert_equal():插入新值,允许键值重复, 返回一个迭代器,指向新节点
1
2
3
4
5
6
7
8
9template<typename _Arg>
typename _Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::iterator
_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::
_M_insert_equal(_Arg&& __v) {
pair<_Base_ptr, _Base_ptr> __res = _M_get_insert_equal_pos(_KeyOfValue()(__v));
_Alloc_node __an(*this);
return _M_insert_(__res.first, __res.second,
_GLIBCXX_FORWARD(_Arg, __v), __an);
}
- insert_unique():插入新值,不允许重复,返回一个pair,第一个是 RB-tree 迭代器,第二表示插入是否成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
template <class Key, class Value, class KeyOfValue, Class Compare, class Alloc>
pair<typename _Rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::iterator, bool>
_Rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::
_M_insert_uniuque(_Arg&& v)
_M_insert_unique(const Value& v)
{
link_type y = header;
link_type x = root(); // 从跟节点开始
bool comp = true;
while ( x != 0 ) {
y = x;
comp = key_compare(KeyOfValue()(v), key(x)); // v 小于当前节点的键值嘛?
x = comp ? left(x) : right(x); // 遇到大的往左走,遇到小的往右走
}
// 离开while之后,y就是要插入位置的父节点,也就是新值是y的子节点
iterator j = iterator(y);
if ( comp ) { // 如果离开 while 的时候是true,也就是表示遇到大的,应该插入到左变
if ( j == begin() ) // 如果插入点的父节点是最左节点
// 直接插入
return pair<iterator, bool>(__insert(x, y, v), true);
else
--j; // 调整j,继续测试???
}
if ( key_compare(key(j.node), KeyOfVAlue()(v)) ) { // 新键值不与已有键值重复
return pair<interator, bool>(__insert(x, y, v), true);
}
// 至此,树种一定已经有了重复键值,插入失败
return pair<iterator, bool>(j, false);
}
- `__insert()` 真正的插入操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
template <class Key, class Value, class KeyOfValue, Class Compare, class Alloc>
pair<typename _Rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::iterator, bool>
_Rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::
__insert(base_ptr x_, base_ptr y_, const Value& v) {
// x_为新值插入点,y_为新值插入点的父节点,v为新值
link_type x = (link_type)x_;
link_type y = (link_type)y_;
link_type z;
if ( y == header || x != 0 || key_compare(KeyOfValue()(v), key(y)) ) { //插入到左边
z = create_node(v); // 创建一个节点
left(y) = z; // 当y就是header时, leftmost() = z
if ( y == header ) { // 如果 y 就是 header
root() = z; //
rightmost() = z;
}
else if ( y == leftmost() ) // y 是最左节点
leftmost() = z; // 维护 leftmost(), 使其永远指向最左节点
}
else {
z = create_node(v);
right(y) = z; // 令新节点为插入点之父的右节点
if ( y == rightmost() ) // 如果插入点之父已经是最右,那么此时新节点才是最右
rightmost() = z;
}
parent(z) = y;
left(z) = 0;
right(z) = 0;
// 设定颜色并调整
__rb_tree_rebalance(z, header->parent);
++node_count; // 节点数量
return iterator(z);
}
- `__rb_tree_rebalance()` 旋转以及改变颜色操作
每次插入操作之后,都要作出调整,使得树满足 RB-tree 的要求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
inline void __rb_tree_rebalance(__rb_tree_node_base* x, __rb_tree_node_base*& root) {
x->color = __rb_tree_red; // 新节点必然是红色,规则4
while ( x != root && x->parent->color == __rb_tree_red ) { 父节点为红
if ( x->parent == x->parent->parent->left) { // 父节点是祖父节点的左孩子
__rb_tree_node_base *y = x->parent-parent->right; // 令y为父节点的兄弟节点
if ( y && y->color == __rb_tree_red ) { // 伯父节点存在而且是红色
x->parent->color = __rb_tree_black; // 更改父节点为黑色
y->color = __rb_tree_black // 更改伯父节点也为黑色
x->parent->parent->color = __rb_tree_red; // 祖父节点改为红
x = x->parent->parent;
}
else { // 没有伯父节点或者伯父节点是黑色
if ( x == x->parent->right ) { // 新节点是父节点的右子节点
x = x->parent;
__rb_tree_rotate_left(x, root); // 左旋一次
}
x-parent->color = __rb_tree_black;
x->parent->parent->color = __rb_tree_red;
__rb_tree_rotate_right(x->parent->parent, root); // 右旋一次
}
}
else { // 父节点是祖父节点的右孩子
__rb_tree_node_base* y = x->parent->parent->left; // y 为伯父节点
if ( y && y->color == __rb_tree_red ) { // 伯父节点存在而且是红色
x->parent->color = __rb_tree_black;
y->color = __rb_tree_black;
x->parent->parent->color = __rb_tree_red;
x = x->parent->parent; // 继续向上查找
}
else { // 无伯父节点或者伯父节点是黑色
if ( x == x->parent->left ) { // 如果新节点是父节点的左孩子
x = x->parent;
__rb_tree_rotate_right(x, root); // 右旋一次
}
x->parent->color = __rb_tree_black;
x->parent->parent->color = __rb_tree_red;
__rb_tree_rotate_left(x->parent->parent, root);
}
}
} // end while
root->color = __rb_tree_black; // 跟节点是黑色
}
- `__rb_tree_rotate_left()`和`_rb_tree_rotate_right()` 旋转操作
新节点必然是红色节点,如果此时父节点也是红色,那么就违反了 RB-tree 的规则,可能需要做出旋转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// x 为旋转点,root为根
inline void __rb_tree_rotate_left(__rb_tree_node_base* x, __rb_tree_node_base*& root) {
__rb_tree_node_base *y = x->right;
x->right = y->left;
if ( y->left != 0 ) {
y->left->parent = x;
}
y->parent = x->parent;
if ( x == root ) {
root = y;
}
else if ( x == x->parent->left ) {
x->parent->left = y;
}
else {
x->parent->right = y;
}
y->left = x;
x->parent = y;
}
inline void __rb_tree_rotate_right(__rb_tree_node_base *x, __rb_tree_node_base*& root) {
__rb_tree_node_base *y = x->left;
x->left = y->right;
if ( y->right != 0 )
y->right->parent = x;
y->parent = x->parent;
if ( x == root )
root = y;
else if ( x == x->parent->right )
x->parent->right = y;
else
x->parent->left = y;
y->right = x;
x->parent = y;
}
其实到了这里,我仅仅是直到要通过改变颜色,旋转来调整树,而不知道具体是怎么改变颜色的,也不知道具体是怎么旋转的!
- find() 搜索元素
查找元素是 RB-tree 的拿手项目
1
2
3
4
5
6
7
8
9
10
11
12
13
14
iterator find(const Key& k) {
link_type y = header;
link_type x = root();
while ( x != 0 ) {
if ( !key_compare(key(x), k) ) { // x键值大于k,往左走
y = x, x = left(x);
}
else // x键值小于k,往右走
x = right(x);
}
iterator j = iterator(y);
return ( j == end() || key_compare(k, key(j.node))) ? end() : j;
}
2. set
set 中的所有元素会根据键值自动排序,不允许有相同的键值。set的元素值就是其键值,键值关系到 set 元素的排列规则,不可以通过 set 的迭代器改变元素值,在 set 的源代码中,将 set
1 | // set 默认是升序,使用内存分配器 std::allocator |
3. map
map 的所有元素会根据键值自动排序,map 的元素都是 pair,同时有 key 和 value,不允许两个元素有相同的 key。类似于 set, 同样不可以通过迭代器改变键值,因为 RB-tree 是依靠键值开排列元素的,但是却可以通过迭代器改变元素值(value),在 RB-tree 中,每个节点都是一个 key-value,根据 key 组织整棵 RB-tree 。源代码实现在 stl_map.h 中, 900多行的源代码,看看怎么调用 RB-tree 的接口的吧!源码大概如下:
1 | // 默认是升序的map |
4. mutilset
mutilset 的用法和特性和 set 完全相同,唯一不同就是 multiset 允许元素重复,这是因为 multiset 的 insert 操作采用的是 RB-tree 的 insert_equal() 而不是 insert_unique()。
1 | iterator insert(const value_type& __x) { |