所谓关联式容器,观念上类似于关联式数据库:每个元素都有一个键值(key)和一个实值(value)。当元素被插入到关联式容器中时,容器内部结构便依照其键值大小,以某种特定规则将这个元素放置到适当位置。关联式容器没有所谓的头尾(只有最大或最小元素),所以不会有所谓push_back()、push_front()等操作行为。
关联式容器set、map、multiset、multimap的内部结构是一个AVLTree,为了获得良好的搜索效率,还分为AVL-Tree、RB-Tree、AA-Tree。其中最广泛运用STL的就是RB-Tree。
不再标准规格之列的关联式容器:hash_set、hash_map、hash_multiset、hash_multimap的内部结果是hash_table。
一、RB-Tree(红黑树)
(1)RB-Tree的定义
红黑树,是一种二叉查找树,在每一个节点上增加一个存储位表示节点的颜色(红或黑),然后通过一些限制(它的性质)保证没有一条路径会比其他路径长出两倍,因此是接近平衡的。
(2)RB-Tree的数据结构
typedef bool __rb_tree_color_type;
const __rb_tree_color_type __rb_tree_red = false;
const __rb_tree_color_type __rb_tree_black = true;
template
struct __rb_tree_node
{
private:
typedef __rb_tree_color_type color_type;
typedef __rb_tree_node_base* base_ptr;
typedef __rb_tree_node* link_type;
color_type color;//节点颜色,红或黑
base_ptr parent;//父节点
base_ptr left;//左孩子
base_ptr right;//右孩子
Value value_field;//节点值
public:
......
}
(3)RB-Tree的性质
1、每个节点不是红色就是黑色。
2、根节点为黑色。
3、每个叶节点都是黑色。
4、如果节点为红,其子节点必须为黑。
5、任一节点至NULL(树尾端)的任何路径,所含之黑节点数必须相同。
(4)RB-Tree的调整
类似于AVLTree进行插入和删除操作会破坏树的平衡需要重新调整,对红黑树进行插入和删除等操作时,对树做了修改可能会破坏红黑树的性质。为了继续保持红黑树的性质,可以通过对结点进行重新着色,以及对树进行相关的旋转操作,即通过修改树中某些结点的颜色及指针结构,来达到对红黑树进行插入或删除结点等操作后继续保持它的性质或平衡的目的。
RB-Tree相对BSTree和AVLTree的优点:
红黑树是牺牲了严格的高度平衡的优越条件为代价,它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。红黑树能够以O(logn)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。
相比于BST,因为红黑树可以能确保树的最长路径不大于两倍的最短路径的长度,所以可以看出它的查找效果是有最低保证的。在最坏的情况下也可以保证O(logN)的,这是要好于二叉查找树的。因为二叉查找树最坏情况可以让查找达到O(N)。
红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高,所以在插入和删除中所做的后期维护操作肯定会比红黑树要耗时好多,但是他们的查找效率都是O(logN),所以红黑树应用还是高于AVL树的. 实际上插入 AVL 树和红黑树的速度取决于你所插入的数据.如果你的数据分布较好,则比较宜于采用 AVL树(例如随机产生系列数),但是如果你想处理比较杂乱的情况,则红黑树比较快。
二、set
(1)定义
set的特性是所有的元素都会根据元素的键值自动被排序,set的元素不像map那样可以同时拥有实值(value)和键值(key),set元素的键值就是实值,实值就是键值。set不允许两个元素有相同的键值。
(2)set的数据结构
class set
{
public:
//实值就是键值,键值就是实值
typedef Key key_type;
typedef Key value_type;
//可以看到key_compare和value_compare采用同一个比较函数
typedef Compare key_compare;
typedef Compare value_compare;
private:
......
rep_type t;//底层采用红黑树
public:
......
}
(3)元素操作
迭代器相关:
iterator begin() const;//返回第一个元素的指针iterator end() const;//返回最后一个元素的指针
bool empty() const;//set集合是否为空
插入和移除元素:
pair insert(const value_type& elem);//插入新元素,返回插入后该元素指针
iterator insert(iterator position, const value_type& elem);//插入新元素,返回插入后该元素指针,pos用来指明从哪里开始搜索
void insert(const value_type* first, const value_type* last);//插入在begin和end之间的元素,无返回
size_type count(const key_type& elem);//查找set集合中elem的数量,不是0就是1,multiset可能有多个
size_type size() const;//返回set中元素的数量
void erase(iterator position);//移走指定位置的元素,返回下一个元素的指针
void erase(iterator first, iterator last);//移走所有[first, last]之间的元素,返回下一个元素的指针
iterator find(const key_type& elem);//在set集合查找某个elem并返回其指针
void clear();//移除所有元素
(4)set的迭代器
set的迭代器被定义为底层RB-Tree的constant-iterator,杜绝写入操作,也就是说set的迭代器是一种常量迭代器。试想一下,set的底层是采用的红黑树(也是一种二叉查找树),修改某个值,会关系到set元素的排列规则,如果任意改变set元素值,会严重破坏set组织。
set拥有和list相同的某些性质:对它进行元素的增加操作(insert)或删除操作(erase)时,操作之前的所有迭代器,在操作完成之后都依然有效,被删除的那个元素迭代器例外。
(5)特殊的set——multiset
multiset的特性以及用法和set完全相同,唯一的差别在于它允许键值重复,因为他采用的插入操作是采用底层机制RB-Tree的insert_equal()而不是insert_unique()。
三、map
(1)map的定义
map的特性是,所有元素都会根据元素的键值自动被排序。map的所有元素都是二元组的集合(pair),同时拥有实值(value)和键值(key)。其中key值是在排序或搜索时使用,它的值可以在容器中重新获取,实值则是该元素关联的数值。map不允许两个元素拥有相同的键值。
(2)map的数据结构
class map
{
public:
typedef Key key_type;//键值型别
typedef T data_type;//实值型别
typedef T mapped_type;//?
typedef pair value_type;//map的所有元素都是pair,同时有键值和实值
typedef Compare key_compare;//只对键值进行比较
private:
......
rep_type t;//底层采用红黑树
public:
......
}
(3)元素操作
迭代器相关:
iterator begin();//返回第一个元素的指针(指向pair类型元素)
iterator end();//返回最后一个元素的指针(指向pair类型元素)
bool empty() const;//set集合是否为空
插入和移除元素:
pair insert(const value_type& elem);//插入新元素,返回插入后该元素指针
size_type count(const key_type& key);//查找map集合中键值为key的数量,不是0就是1,multimap可能有多个
size_type size() const;//返回set中元素的数量
void erase(iterator position);//移走指定位置的元素,返回下一个元素的指针
void erase(iterator first, iterator last);//移走所有[first, last]之间的元素,返回下一个元素的指针
size_type erase(const key_type& key);//通过关键字删除元素
iterator find(const key_type& key);//在map集合查找键值为key的元素 并返回其指针
void clear();//移除所有元素
(4)map的迭代器
通过map的迭代器不能都改map的键值,原因和set类似,因为修改map的键值在红黑树中是排列好的,如果修改键值会影响到红黑树的排列规则,但是通过map的迭代器可以修改map的实值,因为map元素的实值不影响map元素(键值)的排列规则。map的迭代器既不是constant-iterator,也不是一种mutable iterators。
STL中迭代器失效处理——erase的使用
关联性容器::(map和set比较常用)
erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器,
正确方法为::
for( iter = c.begin(); iter != c.end(); )
c.erase(iter++);
(5)特殊的map——multimap
multimap的特性以及用法和map完全相同,唯一的差别在于它允许键值重复,因为他采用的插入操作是采用底层机制RB-Tree的insert_equal()而不是insert_unique()。
后来插入的键值相同的节点默认比先前的节点要大,所以用迭代器遍历时,键值相同的节点是按先后顺序排列的。
四、hashtable
(1)hashtable的定义
哈希表又叫散列表,是实现字典操作的一种有效数据结构。哈希表的查询效率极高,在没有冲突(后面会介绍)的情况下不需经过任何比较,一次存取便能得到所查记录,理想情况下,查找一个元素的平均时间为常数时间。哈希表就是描述key—value对的映射问题的数据结构,使用一个下标范围比较大的数组(哈希表)来存储元素。在记录的存储位置和它的关键字之间建立一个确定的对应关系f(哈希函数),使每个关键字与哈希表中唯一一个存储位置相对应。
也可以简单的理解为,按照关键字为每一个元素“分类”,然后将这个元素存储在相应“类”所对应的地方,称为桶。
(2)hashtable的数据结构
template
struct __hashtable_node
{
__hashtable_node* next;//使用hash计算得到hash值的元素用链表连起来(放在同一个桶中)
Value val;
};
struct __hashtable_iterator
{
__hashtable_node* cur;
hashtable* ht;
}
class hashtable
{
public:
typedef Key key_type;//节点的键值型别
typedef Value value_type;//节点的实值型别
typedef HashFcn hasher;//hash function的函数型别
typedef EqualKey key_equal;//从节点中取出键值的方法
private:
hasher hash;
key_equal equals;
ExtractKey get_key;
typedef __hashtable_node node;
typedef simple_alloc node_allocator;
vector buckets;//用vector来存放多个桶
size_type num_elements;//桶中元素的个数
public:
......
}
(3)hashtable的插入和取值过程
插入过程:
1>得到key
2>通过hash函数得到hash值
3>得到桶号(一般都为hash值对桶数求模)
4>存放key和value在桶内。
取值过程:
1>得到key
2>通过hash函数得到hash值
3>得到桶号(一般都为hash值对桶数求模)
4>比较桶的内部元素是否与key相等,若都不相等,则没有找到。
5>取出相等的记录的value。
(4)hashtable寻址方法
直接寻址表:
当关键字的全域U比较小时,直接寻址是一种简单而有效的技术,它的哈希函数很简单:f(key) = key,即关键字大小直接与元素所在的位置序号相等。另外,如果关键字不是自然数,我们需要通过某种手段将其转换为自然数,比如可以将字符关键字转化为其在字母表中的序号作为关键字。直接寻址法不会出现两个关键字对应到同一个地址的情况,既不会出现f(key1) = f(key2)的情况,因此不用处理冲突,这便是其优点所在。
散列表:
直接寻址的缺点非常明显,如果全域U很大,则在一台标准的计算机可用内存容量中,要存储大小为U的一张表也许不太实际,而且,实际需要存储的关键字集合K可能相对U来说很小,这时散列表需要的存储空间要比直接表少很多。散列表通过散列函数f计算出关键字key在槽的位置。散列函数f将关键字域U映射到散列表T[0...m-1]的槽位上。但是这里会存在一个问题:若干个关键字可能映射到了表的同一个位置处,我们称这种情形为冲突。当然理想的方法是尽量避免冲突,我们可以尽可能第将关键字通过f随即地映射到散列表的每个位置上。
(5)冲突处理
链接法:
链接法的思路很简单:如果多个关键字映射到了哈希表的同一个位置处,则将这些关键字记录在同一个线性链表中,挂在该位置处。
开放定址法:
如果出现冲突,有多种方法,线性探测再散列、二次探测再散列、随机探测再散列。总之就是发生冲突时,将关键字应该放入的位置向前或向后移动若干位置。
五、hash_set
hash_set、hash_multiset的用法和set、multiset是一样的,
提供了 insert,size,count等操作,只是底层是使用hashtable实现的。
六、hashmap
hash_map、hash_multimap的用法和map、multimap是一样的,提供了 insert,size,count等操作,并且里面的元素也是以pair类型来存贮的。虽然对外部提供的函数和数据类型是一致的,但是其底层实现是完全不同的,map底层的数据结构是rb_tree而,hash_map是用
RB-Tree与hashtable的特点:
权衡三个因素: 查找速度,数据量, 内存使用,可扩展性。
总体来说,hashtable查找速度会比RB-Tree快,而且查找速度基本和数据量大小无关,属于常数级别;而RB-Tree的查找速度是O(logn)级别。并不一定常数就比log(n) 小,hashtable还有hash函数的耗时,在元素达到一定数量级时,考虑hashtable,但如果内存使用特别严格,希望程序尽可能少消耗内存,用RB-Tree。 hashtable对空间的要求要比RB-Tree高很多,所以是以空间换时间的方法。
RB-Tree并不适应所有应用树的领域。如果数据基本上是静态的,那么让它们待在能够插入并且不影响平衡的地方会具有更好的性能。如果数据完全是静态的,用hashtable可能会更好一些。
参考:
http://blog.csdn.net/silangquan/article/details/18655795
http://blog.chinaunix.net/uid-20773165-id-1847756.html
http://blog.csdn.net/xocoder/article/details/8533849
http://blog.csdn.net/cws1214/article/details/9842679
http://www.xuebuyuan.com/2073109.html
《STL源码剖析》