关联式容器
1. 红黑树
红黑树不仅是一个二叉搜索树,还必须满足如下五条规则:
- 每个结点要么是红色,要么是黑色。
- 根结点是黑色的。
- 每个叶结点是黑色的。
- 如果一个结点是红色的,那么它的两个子结点必然是黑色的。
- 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。
1. 插入节点
由于必须满足规则5,所以新插入的节点必定是红色的。那么插入时唯一可能破坏的就是第4个规则(如果父节点为红,那么插入一个红色节点就违反规则了)。先为某些特殊节点定义一些代名。以下讨论都将沿用这些代名。假设新节点为X,其父节点为P,祖父节点为G,叔节点为S,曾祖父节点为GG。有了以下四种考虑:
在分类之前,我们可以考虑到一点:当需要旋转的时候,父节点必定是红色的(黑色的就不用旋转了),那么唯二需要考虑的就是插入点在父节点的内侧还是外侧,叔节点是红的还是黑的。
- 叔节点为黑,X为外侧插入,则将父节点置为黑(取代原本为黑的爷节点),爷节点置为红,进行一次单旋后,变为一黑带两红。黑高不变,第5条规则得到保证。
- 叔节点为黑,X为内侧插入,先做一次单旋变成第一种情况,然后按第一种情况处理即可。
- 如果新增节点的父叔都为红(那么爷节点一定是黑的),那么就将父和叔置为黑,爷置为红,如果此时爷的父节点也为红,那么就将爷节点作为新增节点持续向上做,直到不再有父子连续为红的情况。
- 注意!每一轮迭代结束(while()),都会将根置为黑,所以第三点可能爷节点为根节点,被置为黑之后就结束了。
while (z->parent != NIL && z->parent->color == Color->RED) {
if (z->parent == z->parent->parent->left) {
Node y = z->parent->parent->right; // y即叔节点
if (y->color == Color->RED) { //情况3
y->color = Color->BLACK;
z->parent->color = Color->BLACK;
z->parent->parent->color = Color->RED;
z = z->parent->parent;
} else if (z == z->parent->right) { //情况2
z = z->parent;
leftRotate(z);
} else { //情况1
z->parent->color = Color->BLACK;
z->parent->parent->color = Color->RED;
rightRatate(z->parent->parent);
}
} else {
// 父节点为祖父节点右节点时,一样,只是处理时左右反过来。
}
}
root->color = Color->BLACK; //满足红黑树性质2
}
为了避免状况3“父子节点皆为红色”的情况持续向红黑树上层结构发展,形成处理时效上的瓶颈,我们可以施行一个由上而下的程序:假设新增节点为A,那么就延着从根到A的路径,只要看到有某节点X的两个子节点皆为红色,就把X改为红色,并把两个子节点改为黑色。当然如果X的父节点也是红,则需要按上述情况1或2做处理,处理完成之后,A直接插入或插入后做一次单旋就可以了。
2. 迭代器
在将迭代器之前,有必要说明一下红黑树节点之间的关系。首先,每个节点有三个指针,分别指向其父节点、左子节点、右子节点。然后STL专门为根节点创造了一个父节点header
,在树中还没有节点的时候,header的左右指针都指向自己,父节点指向一个空的节点空间,当书中加入节点而有了根节点的时候,header的左指针指向树中最小的节点,右指针指向最大的节点,header和根节点互为对方的父节点。所以在插入节点的过程中,header的左右指针和父节点需要不断的维护。
1. increment()
寻找某个节点 node 的后继节点。
- 如果有右子节点,那么就是右节点
- 如果没有,就沿路径向上遍历node和其父节点 y,直到node不是父节点 y 的右节点为止。
- 如果 node 的右节点不为其父节点,这就分两种情况,但结果都是取node的父节点。
- node是父节点的左节点,那么最初的node的下一个节点自然就是现在的父节点 y。
- 这种情况下,node是 y 的父节点(根节点和header互为父节点)。node 本最开始不是根节点,而是最后一个节点,那么现在node在根节点,y 在header,那么最后一个节点的下一个节点就是 header。
- 如果 node 的右节点为其父节点,则说明 node 最开始就是根节点,且无右子节点(即为最后一个节点),那么再遍历的时候,node 会遍历到header上,y 则遍历到根节点,直接返回这个 node 即可。
- 如果 node 的右节点不为其父节点,这就分两种情况,但结果都是取node的父节点。
2. decrement()
寻找某个节点 node 的前驱节点
- 如果是红色节点,且父节点的父节点则其自己,则说明此节点为 header (根是黑的,所以只能是header),则返回右子节点(即最后一个节点),因为最后一个节点++的时候会到header。
- 如果有左子树,则找到左子树的最后一个节点。
- 如果没有左子树,则向上找到第一个不是父节点的左子节点的节点,返回他的父节点。
- 如果 node 最开始就是这棵树的第一个节点,那么想上找第一个不是父节点的左子节点的节点只能是根节点,那么返回根的父节点 header。
- 如果向上找到了一个是父节点(称为 y)的右子节点的,那么 node 就是 y 右子树的第一个最左面的节点,y 就是最开始的 node 的前一个节点。
3. 元素操作
1. insert_unique()
insert_unique
不允许键值重复,若重复则插入无效
- 通过循环比较找到插入位置 x 及其父节点 y,令一个迭代器 j 指向 y。
- 如果插在 y 的左子节点
- 如果 j 是最左的节点,那么就调用
__insert()
插进去就可以了。 - 如果不是,那么找到 j 的前驱节点
--j
(防止插入重复的值)
- 如果 j 是最左的节点,那么就调用
- 如果(和第二点是可以先后运行的,而不是只能运行一个)节点要插在 j 的右侧,那么就调用
__insert()
插在 x 处。- 如果 j 刚才有 j-- 的操作,那么v的值大于现在 j 的值,小于刚才 j 的值,必然不会重复;
- 如果刚才没 j-- ,那么这个值一定是比 j 后面节点的值都要小的,再加上比现在的 j 大,则没有重复。
- 到现在还没插进去,则说明新值与树中的键值重复,则不插入。
注意了,再第一步比较的时候,是key_compare(v, key(x))
,再第三步的时候,是key_compare(key(j.node), v)
,v 位置的不同防止了v 与节点值相等的情况。相等时会到第四步。
2. insert_equal()
insert_equal
允许键值重复。
- 通过循环比较找到插入位置 x 及其父节点 y,令一个迭代器指向 y。
- 直接调用
__insert
插就可以了
3. __insert()
参数分别为新值插入点 x,x 的父节点 y,新值 v。
- 如果 y 是 header ,或者 x 不为0,或 v 的值比 y 的 value 小,那么:
- 产生一个新节点 z ,赋予 v 的值,并设为 y 的左节点。
- 如果 y 是 header(原本没有节点),将 z 设为根,且 rightmost 设为z。
- 如果 y 是 leftmost ,则将 leftmost 更新为 z。
- 其他情况(v 的值比 y 的 value 大)
- 新建一个节点,设为 y 的右节点,如果 y 原本是
rightmost
,则将rightmost
更新为此节点。
- 新建一个节点,设为 y 的右节点,如果 y 原本是
- 设定新节点的父节点和左右节点,旋转并改变红黑树的部分节点的颜色,节点数+1,返回指向新增节点的迭代器。
4. 单旋
// 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的左变为x的右
y->parent = x->parent;
if(x == root) // y继承x原本的地位
root = y;
else if(x == x->parent->left)
x->parent->left = y;
else
x->parent->right = y;
y->left = x; // 修改x、y之间的关系
x->parent = y;
}
右旋同理
2. set 与 multiset
std::set<int> set;
set
是通过RB Tree
来实现的,其实也是一种适配器。所有set
的操作行为,都是转调用RB Tree
的操作。
set
的特性是:所有元素都会根据元素的键值自动被排序,但set
的key
和value
是一个值,set
不允许两个元素有相同的键值。所以set
的插入操作的底层机制为RB Tree
的insert_unique()
,而multiset
允许两个元素有相同的键值。所以multiset
的插入操作的底层机制为RB Tree
的insert_equal()
。
3. map 与 multimap
# include <map>
std::map<char,int> mymap;
map
是通过RB Tree
来实现的,其实也是一种适配器。所有map
的操作行为,都是转调用RB Tree
的操作。
map
的特性是,所有元素都会根据元素的键值自动被排序。map
的所有元素都是pair
,同时拥有实值( value
)和键值(key
) ,pair
的第一元素被视为键值, pair
第二元素被视为实值。map
不允许两个元素拥有相同的键值。
我们不可以通过迭代器修改map
的键值,但可以通过迭代器修改map
的实值。
同样的,map
的插入操作的底层机制为RB Tree
的insert_unique()
,而multimap
允许两个元素有相同的键值。所以multiset
的插入操作的底层机制为RB Tree
的insert_equal()
。
4. hashtable
哈希表可以对任何有名项进行存取和删除的操作,但通常情况下的利用array
或其他方式来进行操作,要么会带来极大地空间消耗或者时间消耗,为了避免使用一个极大地array
,哈下表使用散列函数来将一个大数映射为小数,这样X%Tablesize
就会是一个0~TableSize-1
的整数,可以作为array
的索引。
为了解决不同的元素被映射到同一位置的问题,哈希表有三种解决方法:
-
线性探测:如果计算出的位置已经被其他元素占用,则一一向下寻找,直到找到条件吻合的位置,至于元素的删除,则采用惰性删除的方式:只删除记号,实际删除操作则等到表格重新整理时进行。但是线性探测会带来主集团的问题。
-
二次探测:相对于线性检测的
H+1,H+2,...H+n
的探寻方式,二次检测则是H+1^2,H+2^2...H+i^2
的方式来依次尝试,其中迭代关系为
H i = H i − 1 + 2 i − 1 ( M O D ( M ) ) H_i=H_{i-1}+2i-1 (MOD(M)) Hi=Hi−1+2i−1(MOD(M))
虽然二次探测可以消除主集团,但是同时可能会造成次集团。 -
开链法:每一个表格元素维护一个
List
,然后在这个List
上进行元素的插入、搜索、删除等动作。使用开链法,表格的负载系数将大于1,以下的所有hashtable
操作都是基于开链法实现的。
哈希表的定义如下:
template<class Value, class Key, class HashFcn, class ExtractKey,
class EqualKey, class Alloc = alloc>
class hashtable;
这些模板参数的意义如下:
Value
:实值型别Key
:键值型别HashFcn
:hash function
的函数型别ExtractKey
:从节点中取出键值的方法EqualKey
:判断键值是否相同的方法Alloc
:空间适配器,缺省为alloc
1. insert_unique
- 调用
resize
判断是否需要重建表格,需要就扩充 - 调用
insert_unique_noresize
插入节点。
2. resize
比较元素的个数和bucket vector
的大小,如果元素个数更多,就重建表格。
-
利用
next_size
找到元素个数的下一个质数 n -
首先新建一个
bucket vector
,大小为 n,命名为tmp
-
对于原
bucket vector
的每一个bucket
,依次遍历它的List
的每一个节点- 调用
bkt_num
计算这个节点的值应该在tmp
的哪一个位置 - 利用指针操作将此节点移动到
tmp
的顶端
// 这个first最开始是bucket每一个序列的第一个指针 while(first){ size_type new_bucket = bkt_num(first->value, n); buckets[bucket] = first->next; // 原来的bucket向下更新 first->next = tmp[new_bucket]; // first插入tmp对应的序列中 tmp[new_bucket] = first; first = buckets[bucket]; // 继续操作 }
- 调用
-
交换这两个
bucket vector
(现在bucket是空的,其中的指针都转移到tmp了,把tmp和bucket对调,tmp这时会变小,bucket会变大),交换完毕后tmp为空,释放tmp
的内存。
3. insert_unique_noresize
- 先利用
bkt_num
计算这个节点的值应该在哪一个位置,找到这个位置的List
头部first
。 - 遍历这个
List
,如果有相同值的节点,就不插入了。 - 新建新的节点,赋值,插入
List
,并更新节点数++num_elements
。
insert_equal_noresize
与insert_unique_noresize
的唯一区别就是在第2步遍历的过程中,如果发现了相同值的节点,就插入在这个节点的后面,并立即返回。
4. clear
- 遍历每一个
bucket
的每一个节点,利用delete_node()
删除这个节点 - 删除完一个
bucket
的最后一个节点的时候,将其内容设置为NULL指针 - 将总结点个数设为0.
clear()
并没有释放掉空间,而是保持了原本的大小。
5. copy_from
- 先调用
clear()
清空这个容器 - 调用
resize
,如果对方的空间更大,就增大自己的空间。 - 利用指针操作将每一个对方的
bucket
及其中的节点移过来。 - 将对方的节点个数赋值过来。
- 如果中间出错了(空间不足等),清除移动过来的所有节点。
6. hash functions
hash_function
是计算元素位置的元素,被之前提到的bkt_num
调用, 来获得一个可以对哈希表进行模运算的值,针对char
,ing
,long
等整数型别,通常什么都没做而返回一个原值,而对于字符字符串const char*
,则设计了一个转换函数。
inline size_t __stl_hash_string(const char* s){
unsigned long h = 0;
for(; *s; ++s)
h = 5 * h + *s;
return size_t(h);
}
这是一个多项式哈希,设最后的映射结果为:
s
t
r
[
0
]
∗
x
n
+
s
t
r
[
1
]
∗
x
n
−
1
+
.
.
.
+
s
t
r
[
n
]
∗
x
0
str[0] * x^n + str[1]*x^{n-1}+...+str[n]*x^0
str[0]∗xn+str[1]∗xn−1+...+str[n]∗x0
可以重写如下:
(
(
(
s
t
r
[
0
]
∗
x
)
+
s
t
r
[
1
]
)
∗
x
+
.
.
.
)
∗
x
+
s
t
r
[
n
]
(((str[0]*x)+str[1])*x+...)*x+str[n]
(((str[0]∗x)+str[1])∗x+...)∗x+str[n]
所以:
h
=
0
h
=
h
∗
x
+
s
t
r
[
1
]
h
=
h
∗
x
+
s
t
r
[
2
]
.
.
.
h
=
h
∗
x
+
s
t
r
[
n
]
h=0 \\ h=h*x+str[1]\\ h=h*x+str[2]\\ ...\\ h=h*x+str[n]
h=0h=h∗x+str[1]h=h∗x+str[2]...h=h∗x+str[n]
在这里x为5,具体为什么取5就不清楚了。
hash_function
无法处理除整数型和const char*
以外的型别的元素,比如string
,double
,float
,如果想处理这些型别,需要用户自己去定义。
5. hash_set / hash_multiset
set / multiset
以红黑树作为底层实现,hash_set / hash_multiset
则以哈希表作为底层实现,所以前者可以实现自动排序功能而后者不可以。后者的所有操作都是哈希表的转调用,哈希表的hash_function
无法处理的型别,hash_set / hash_multiset
同样无法处理。
hash_set
的插入操作是利用底层机制为哈希表的insert_unique()
,而hash_multiset
允许两个元素有相同的键值。所以hash_multiset
的插入操作的底层机制是利用哈希表的insert_equal()
。
6. hash_map / hash_multimap
map / multimap
以红黑树作为底层实现,hash_map / hash_multimap
则以哈希表作为底层实现,所以前者可以实现自动排序功能而后者不可以。后者的所有操作都是哈希表的转调用,哈希表的hash_function
无法处理的型别,hash_map / hash_multimap
同样无法处理。
hash_map
的插入操作是利用底层机制为哈希表的insert_unique()
,而hash_multimap
允许两个元素有相同的键值。所以hash_multimap
的插入操作是利用底层机制为哈希表的insert_equal()
。