关联式容器
根据“数据在容器中的排列”特性,容器可分为序列式和关联式容器两种。标准的STL关联式容器可分为set(集合)和map(映射表)。这些容器的底层机制均以RB-tree完成,RB-tree也是一个独立容器,但并不对外开放。
此外,SGI STL还提供了一个不在标准规格之列的关联式容器:hash table(散列表)以及hash table为底层机制完成的hash_set(散列集合)、hash_map(散列映射表)、hash_multiset(散列多键集合)、hash_multimap(散列多键映射表)。
所谓关联式容器,观念上类似关联式数据库:每笔元素都有一个key和一个value。当元素被插入到关联式容器中时,容器内部结构便按照其键值大小,以某种特定规则将这个元素放置于适当位置,关联式容器没有所谓的头尾,只有最大元素和最小元素,所以不会有所谓的push_back(),push_front(),pop_back(),pop_front(),begin(),end()这样的操作行为。
关联式容器的内部结构是一个平衡二叉树,平衡二叉树有很多种类型,包裹AVL-tree、RB-tree、AA-tree。其中最被运用于STL的是RB-tree。
树的概述
树的一般概念如图所示
二叉搜索树
二叉树指任何节点最多只允许两个子节点,分别称为左子节点和右子节点,二叉树是可以为空的,编译器表达式树,哈夫曼编码树都是二叉树,如图
二叉搜索树
所谓二叉搜索树是可提供对数时间的元素插入和访问,其规则是任何节点的键值一定大于其左子树中的每个节点键值,并小于其右子树的每个节点的键值,因此最小元素就是往左一直走知道无路可走的元素,最大元素就是往右一直走,直到无路可走得到的元素。
如果要插入元素就从根节点出发,根节点大就往左走,小就往右走,直到走到叶子结点。
如果要删除某一结点,如果一般取右子树的最左结点或者左子树的最右结点来替换,然后删除。
平衡二叉搜索树
为了防止二叉树输入值不够随机,导致二叉搜索树失去平衡。
AVL tree(Adelson-Velskii-Landis tree)
AVL tree是一个加上了额外平衡条件的二叉搜索树。为了确保整棵树的深度为O(logN),要求左右子树高度差不超过1,。
我们可以把平衡被破坏分成四种情况;
- 插入点位于X的左子节点的左子树-左左
- 插入点位于X的左子节点的右子树-左右
- 插入点位于X的右子节点的左子树-右左
- 插入点位于X的右子节点的右子树-右右
情况1,4彼此对称,称为外侧插入,可以采用单旋转操作来调整,情况2,3彼此对称,称为内侧插入,可以采用双旋转操作来调整。
单旋转
这么做是因为二叉搜索树的规则使我们知道,k2>k1,所以k2必须称为新树形中k1的右子节点,B子树的所有结点的键值都位于k1和k2之间,所以新树形中B子树必须落在k2的左侧。
双旋转
此时单旋转解决不了问题,我们必须以k2位新的根节点,这使得k1必须称为k2的左子节点,k3必须称为k2的右子节点,这种情况称为双旋转,因为可以由两次单旋转来完成。
以上所有调整都只需要将指针稍微做搬移,就可以迅速完成
RB-tree
RB-tree不仅是一颗二叉搜索树,而且必须满足以下规则: - 每个结点不是红色就是黑色
- 根节点为黑色
- 如果结点为红,其子节点必须为黑
- 任一结点至NULL(树尾端)的任何路径,所含之黑结点数必须相同。
根据规则4,新增结点必须为红,根据规则3,新增结点之父节点必须为黑。当新节点根据二叉搜索树的规则到达其插入点,却未能满足上述条件时,就必须调整颜色并旋转树形。
考虑四种破坏条件的情况
此时,经过调整后也可能产生不平衡的状态(高度差超过1),假如图中A和B为null,D或E不为null。但这并无关系,因为RB-tree的平衡性比AVL-tree弱,然而RB-tree通常能够导致良好的平衡关系,RB-tree的搜索平均效率和AVL-tree几乎相等。
为了避免状况4“父子皆为红色”的情况持续向上层发展,形成处理时效上的瓶颈,我们可以实行一个由上到下的程序:假设新增结点为A,那么就沿着A的路径,只要看到有某结点X的两个子节点都为红色,加把X改成红色,并把两个子结点改成黑色。
但是如果A的父节点P也是红色,就得像状况1一样做一次单旋转并改变颜色,或者像状况2一样地做一次双旋转并改变颜色。
RB_tree的结点设计
采用双层结构
//定义颜色区分
typedef bool rb_tree_color_type;
const rb_tree_color_type rb_tree_red = false;//红色为0
const rb_tree_color_type rb_tree_black = true;//黑色为1
//双层红黑树节点结构
struct rb_tree_node_base{
typedef rb_tree_color_type color_type;
typedef rb_tree_node_base* base_ptr;
color_type color;
base_ptr parent;
base_ptr left;
base_ptr right;
//获取最小值
static base_ptr minimun(base_ptr x){
while(x->left)
x = x->left;
return x;
}
//获取最大值
static base_ptr maximun(base_ptr x){
while(x->right)
x = x->right;
return x;
}
};
//红黑树结点第二层机构
template <typename Value>
struct rb_tree_node : public rb_tree_node_base{
//定义出结点类型
typedef rb_tree_node<Value>* link_type;
Value value_field;//结点值
};
RB_tree的迭代器
与结点对应,同样设计双层结构
struct rb_tree_base_iterator{
typedef rb_tree_node_base::base_ptr base_ptr;
typedef bidirectional_iterator_tag iterator_category;
typedef ptrdiff_t difference_type;
base_ptr node;//用来与容器之间产生一个连接的关系
//按照中序遍历的顺序,左子树的最右结点->根节点->右子树的最左结点,找出下一结点
void increment(){
if(node->right != 0){
//状况1:右子结点存在,则当前可以看成是根节点,则找出右子树的最左结点
node = node->right;
while(node->left != 0)
node = node->left;
}else{
//状况2:没有右子节点,则当前可以看成是左子树的最右结点了,则找出根节点
base_ptr y = node->parent;
while(y->right == node){
node = y;
y = y->parent;
}
if(node->right != y)
//状况3:如果当前的node不是根节点,则y即为所求
node = y;
//状况4:如果当前的node是根节点,则node为所求
}
}
//按照中序遍历的顺序,左子树的最右结点->根节点->右子树的最左结点,
// 找出上一结点,只需要处理后两者,后两者才有前驱结点
void decrement(){
if(node->color == rb_tree_red && node->parent->parent == node)
//状况1:如果当前结点是header结点,那么其前驱应该是mostright结点,也就是其右子结点
node = node->right;
else if(node->left != 0){
//状况2:也就是当前是根节点,那么其前驱应该是左子树中最右结点
base_ptr y = node->left;
while(y->right != 0)
y = y->right;
node = y;
}else{
//状况3:如果当前不是根节点,也没有左子节点,那么当前处于右子树的最左结点,要去找根节点
base_ptr y = node->parent;
while(y->left == node){
node = y;
y = y->parent;
}
node = y;
//如果当前处于root结点,y则为header结点,那么当前必定只有root一个结点
//则可以得到node = header,y = root
//最终得到node = y,不需要特殊处理
}
}
};
template <typename Value,typename Ref,typename Ptr>
struct rb_tree_iterator:public rb_tree_base_iterator{
typedef Value value_type;
typedef Ref reference;
typedef Ptr pointer;
typedef rb_tree_iterator<Value,Value&,Value*> iterator;
typedef rb_tree_iterator<Value,const Value&,const Vallue*> const_iterator;
typedef rb_tree_iterator<Value,Ref,Ptr> self;
typedef rb_tree_node<Value>* link_type;
//构造函数
rb_tree_iterator(){}
rb_tree_iterator(link_type x){node = x};
rb_tree_iterator(const iterator& it){node = it.node};
//运算符重载
reference operator*() const{
return link_type(node)->value_field;
}
pointer operator->() const{
return &(operator*());
}
self& operator++(){
increment();
return *this;
}
self operator++(int){
self tmp = *this;
increment();
return tmp;
}
self& operator--(){
decrement();
return *this;
}
self operator--(){
self tmp = *this;
decrement();
return tmp;
}
};
关键方法insert_unique/insert_equal
//将x插入到红黑树,并且保持key独一无二
std::pair<iterator,bool> insert_unique(const value_type& v){
link_type y = header;
link_type x = root();
bool comp = true;
while(x != 0){
y = x;
comp = key_compare(KeyOfValue()(v),key(x));
x = comp ? left(x):right(x);//小就往左,大或等就往右
}
iterator j = iterator(y);//父节点的迭代器
if(comp)
//如果comp为真表示遇到小
if(j == begin())
//如果插入点的父节点是最左结点
return std::pair<iterator,bool>{_insert(x,y,v),true};
else
//否则就找出比当前结点的前驱结点
--j;
//判断前驱结点与插入值的大小
//比前驱结点大说明可以插入
//比前驱结点小或等于则说明一定是等于,不可能是小
//因为小的话一定是先找到的这个前驱结点
//所以返回false
if(key_compare(key(j.node),KeyOfValue()(v)))
return std::pair<iterator,bool>{_insert(x,y,v),true};
return std::pair<iterator,bool>{j,false};
}
//允许key值重复
iterator insert_equal(const value_type& v){
link_type y = header;
link_type x = root();
//从根节点开始往下寻找
while(x != 0){
y = x;
x = key_compare(KeyOfValue()(v),key(x)) ? left(x) : right(x);
}
return _insert(x,y,v);
}
可以看到:
对于insert_equal,不管有没有相同结点,都是走一遍红黑树,找出一个空结点,然后插入进去.
对于insert_unique,则
走一遍红黑树,找到空结点
1.假如插入值比其父节点小,comp为真
1.1判断父节点是否为最左结点,是则直接插入不是则找到其前驱结点–>然后判断其前驱结点值是否比插入值小,小则说明不是相等结点,可以直接插入,否则说明已经有该结点了,插入失败
2.假如插入值比其父节点大,comp为假
直接判断其父节点与插入值的大小,这种情况判断父节点与插入点之间的大小,一定是插入点大,因此可以直接插入
那么为什么会这样呢?
因为我们在遍历红黑树的时候,如果比当前结点小就往左走,大于等于就往右走,那么如果当前是找到的相等的值,我们就会往与该结点相等的右子树走,然后继续往其左边走,最后会来到该结点的右子树的最左结点处,所以要判断与其前驱结点的关系(前驱结点自然就是与其相等的那个结点了),如果是前驱结点小,那就可以插入,那就是正常的小于的情况,如果是false,只能是相等的情况,因为在其右边一定是大于等于前驱结点的,那就说明找到相等结点,插入失败了。
而如果最后comp是false,也就是找到的是大于其父节点的值,这个是否是可以直接插入的,因为相等的情况会像上面那样走的!