STL 关联式容器

时间:2022-08-11 20:51:03

1、关联式容器概述

所谓关联式容器,观念上类似于关联式数据库:每笔数据都有一个键值(key)和一个实际值(value)。当元素被插入容器时,内部机制根据键值,按着一定的规则将元素置于特定的位置。关联式容器没有所谓头尾的概念(只有最大元素,最小元素),所以不会有类似push_back(),push_front(),pop_back(),pop_front,begin(),end()这样的操作。

标准的stl关联式容器分为set(集合)和map(映射表)两大类,以及这两大类的衍生体multiset(多键集合)和multimap(多键映射表),这些容器的底层机制均以RB-tree来实现,RB-tree也是一个独立容器,并不开放给外界使用。

此外,SGI STL还提供了一个不在标准之内的关联式容器hash table,以及以此为底层机制而完成的hash_set(散列集合),hash_map(散列映射表),hash_multiset(散列多键集合),hash_multimap(散列多键映射表)。

2、AVL树

AVL树属于二叉查找树,二叉查找树的查找和插入操作在最坏情况下复杂度为O(N),而AVL树最坏时仍然为O(lgN)。

AVL树的插入和删除操作需要借助于节点的旋转来保持树的高度平衡。AVL树平衡被破坏时采用的调整规则:
(1)单旋转:用来修正外侧插入导致的不平衡。
(2)双旋转:用来修正内侧插入导致的不平衡。可以利用两次单旋完成。

节点的插入结果可以分为四种情况。节点插入之后如果AVL树的平衡遭到破坏,那么,令X为平衡状态被破坏的节点中最深(下方)的节点。

 插入点位于X的左子结点的左子树——左左;(外侧插入,单旋)
 插入点位于X的左子结点的右子树——左右;(内测插入,双旋)
 插入点位于X的右子结点的左子树——右左;(内测插入,双旋)
 插入点位于X的右子结点的右子树——右右;(外侧插入,单旋)

3、红黑树(RB-tree)

由红黑树规则知:新增节点必须为红色;新增节点之父节点必须为黑色。

当新增节点根据二叉搜索树的规则到达其插入点,却未能符合红黑树的规则时,就必须调整颜色并旋转树形。

为了更大的弹性,SGI将RB-tree迭代器实现为两层。RB-tree迭代器,但不具备随机定位功能,其提领操作和成员访问操作与list十分近似,较为特殊的是其前进和后退操作。前进操作调用了基层迭代器的increment(),后退操作调用了基层迭代器的decrement()。

RB-tree有定义专属的空间配置器,每次配置一个节点。RB-tree的构造方式有两种,一种是以现有RB-tree复制一个新的RB-tree,另一种是产生一颗空空如也的树。

set和multiset

set的特性:
(1)所有元素都会根据元素的键值自动被排序。
(2)set是集合,它的元素的键值就是实值,实值就是键值,不允许两个元素有相同的值。
(3)不可以通过set的iterator来改变元素的值,因为set的元素值就是键值,改变键值会违反元素排列的规则。
(4)在客户端对set进行插入或删除操作后,之前的迭代器依然有效。当然,被删除的元素的迭代器是个例外。
(5)它的底层机制是RB-tree。几乎所有的操作都只是转调用RB-tree的操作行为而已。

multiset和set几乎一样,唯一的区别是,multiset允许键值重复。因此set使用底层RB-tree的insert_unique()实现插入,而multiset插入采用的是RB-tree的insert_equal()而非insert_unique()。
测试实例:

#include <set>
#include <iostream>

using namespace std;
int main()
{
    int ia[5] = {0,1,2,3,4};
    int n = sizeof(ia)/sizeof(ia[0]);
    cout<<n<<endl;// 5
    set<int> iset(ia, ia+n);
    cout<<"size="<<iset.size() << endl;// size=5
    cout<<iset.count(3)<<endl;// 1
    iset.insert(3);//插入无效,元素不允许重复
    cout<<"size="<<iset.size() << endl;// size=5
    cout<<iset.count(3)<<endl;// 1

    iset.erase(1);
    cout<<iset.count(1)<<endl;// 0

    set<int>::iterator ite1=iset.begin();
    set<int>::iterator ite2=iset.end();
    for(; ite1 != ite2; ++ite1)
        cout<<*ite1<<" ";
    cout<<endl;//0 2 3 4

    //关联式容器,应采用其所提供的find函数搜寻元素,会比使用
    //STL算法find()更优效率!因STL find()只是循序搜寻。
    ite1 = iset.find(3);//使用自身提供的find函数
    if(ite1 != iset.end())
        cout<<"3 found!"<<endl;
    else
        cout<<"3 not found!"<<endl;
    //企图通过迭代器改变set元素,是不被允许的
    //*ite1 = 9;//error !
    return 0;
}

map和multimap

map的特性:
(1)所有元素都会根据元素的键值自动被排序。
(2)map的所有元素都是pair,第一个值是键值,第二个是实值。
(3)map不允许两个元素拥有相同的键值。
(4)可以通过map的迭代器来改变元素的实值,但不可以改变键值,那样会违反元素的排列规则。
(5)在客户端对map进行插入或删除操作后,之前的迭代器依然有效。当然,被删除的元素的迭代器是个例外。
(6)它的底层机制是RB-tree。几乎所有的操作都只是转调用RB-tree的操作行为而已。

multimap和map几乎一样,唯一的区别是,multimap允许键值重复。因此map使用底层RB-tree的insert_unique()实现插入,而multimap插入采用的是RB-tree的insert_equal()而非insert_unique()。

hashtable

1、hashtable概述

hashtable可以提供对任意有名项的存取和删除操作,这种结构的用意在于提供常数时间的的基本操作,而不依赖于插入元素的随机性,是以统计为基础的。

散列函数(hash function):负责将某一元素映射为一个”大小可接受之索引”。简而言之,就是将大数映射为小数。

使用hash function带来的问题:可能有不同元素映射到相同的位置(相同索引)。这便是碰撞或冲突问题。解决碰撞问题的方法:线性探测(linear probing),二次探测(quadratic probing),开链(separate chaining)等。stl hashtable采用的hash方式是开链法。

(1)线性探测:当hash function计算出某个元素的插入位置,而该位置空间不再可用时,怎么做?最简单的办法就是循序往下一一寻找,知道找到一个可用空间为止。

需要两个假设:a.表格足够大。b.每个元素都能够独立。

线性探测会造成主集团(primary clustering)问题:平均插入成本的成长幅度,远高于负载系数的成长幅度。

(2)二次探测:主要用来解决主集团问题。解决碰撞的方程式为F(i) = i^2。如果hash function计算出新元素的位置为H,而该位置实际上已被使用,那么就依次尝试H+1^2,H+2^2,H+3^2,H+4^2,….,H+i^2,而不像线性探测尝试的是H+1,H+2,H+3,H+4,….,H+i。

二次探测可以消除主集团,却可能造成次集团(secondary clustering):两个元素经hash function计算出来的位置若相同,则插入时所探测的位置也相同,形成某种浪费。消除次集团的方法如复式散列。

(3)开链:这种做法是在每一个表格元素中维护一个list。hash function为我们分配某一个list,然后我们在哪个list身上执行元素的插入、搜寻、删除等操作。若list够短,速度还是够快。

使用开链法,表格的负载系数将大于1。SGI STL hashtable便采用的是开链法。

hashtable结构

hashtable表格内的每个单元,涵盖的不只是个节点(元素),而可能是一桶节点,因此称为bucket。
SGI STL中hash table使用的是开链法进行的冲突处理,其结构如图所示:
STL 关联式容器
bucket所维护的linked list不采用STL的list或者slist,而是自行维护hash table node。而至于buckets聚合体,则使用vector来完成,以便有动态扩容能力。

STL中的hash迭代器,是一种forward迭代器,只能+。有指向当前节点的指针和指向对应的vector的指针,没有后退操作,也就是没有所谓的逆向迭代器。

hashtable以质数来设计表格大小,预先计算好了28个质数,以备随时访问,大约都是两倍的关系递增,同时提供一个函数,查询28个质数中,“最接近某数且大于某数”的质数作为vector的长度,如果需要重新分配,则分配下一个质数长度的vector。

stl hash table扩张表格的触发条件是:当元素的数目大于或等于表格的大小。(这个条件应该是为了保证常数操作时间,在统计基础上得出的)。

insert分为insert_unique和insert_equal操作,前者保证插入的数不能有重复,后者可以插入键值相同的数。可以先用unique之后再用equal。insert_unique:先调用resize函数,看是否需要增大vector,然后插入,vector的索引通过取余得到。resize:如果已有元素的个数大于vector的size,需要根据得到的最新质数,分配新的空间,将在旧空间的元素,重新计算hash,复制到新的空间,最后旧空间与新空间swap一下即可。insert_equal:也是先调用resize,遍历找到和他相同的节点,在该节点的前面插入。

hashtable有一些无法处理的型别,比如string,double,float。除非用户为那些型别写了相应的hash function。

4、hash_set,hash_map,hash_multiset,hash_multimap

这些容器和前面介绍的一一对应,只不过这些都是以hash_tabel为底层实现机制的。

底层机制决定了这两组容器的区别:

RB-tree组对元素实现排序,而hashtable组没有;
RB-tree组的查找时间复杂度为lg(n),而hashtable组为常数时间;
RB-tree组在空间利用上,不会浪费结点,而hashtable组可能会有一些空置桶。
hash_multiset和hash_multimap插入使用的是inset_equal。其它操作与multiset和multimap相同。