第五章 关联式容器 associative containers

  1. 容器,置物之所也,第4章一开始曾对此做了一些描述。所谓STL容器,即是将最常被运用的一些数据结构(datastructures)实现出来,其涵盖种类有可能在每五年召开一次的C++标准委员会议中不断增订。
  2. 根据“数据在容器中的排列“特性,容器可概分为序列式(sequence)和关联式(associative)两种,如下图所示。第4章已经探讨过序列式容器,本章将探讨关联式容器。
  3. 标准的STL关联式容器分为set(集合)和map(映射表)两大类,以及这两大类的衍生休multiset (多键集合)和multimap(多键映射表)。这些容器的 底层机制均以RB-tree(红黑树)完成。RB-tree也是一个独立容器,但并不开放给外界使用.
  4. SGL STL还提供了一个不在标准规格之列的关联式容器:hash table(散列表, 以及以此hashtable为底层机制而完成的hash_set(散列集合)、 hash_rnap (散列映射表)、hash_multiset(散列多键集合),hasti_multimap(散列多键映射表)。

在这里插入图片描述

关联式容器( associative containers)

  1. 关联式容器,观念上类似关联式数据库(实际上则简单许多):每笔数据 (每个元素)都有一个键值(key)和一个实值(value) (set的键值就是实值,map的键值可以和实值分开,并形成一种映射关系,所以map被称为映射表或字典)。
  2. 当元素被插入到关联式 容器中时,容器内部结构(可能是RB-tree, 也可能是hash-table)便依照其键 值大小,以某种特定规则将这个元素放置千适当位置。
  3. 联式容器没有所谓头尾(只有最大元素和最小元素),所以不会有所谓push_back ()、 push_front ()、 pop_back ()、 pop_front ()、 begin ()、 end()这样的操作行为。

关联式容器的内部结构是个balanced binary tree(平衡二插树),以便获得良好的搜索效率。balanced bianary tree有许多类型,包括AVL-tree/RB-tree/AA-tree,其中最广泛运用STL的是RB-tree(红黑树),为了探讨STL关联式容器,必须了解RB-tree。

进入RB-tree之前,先了解tree的概念。以下讨论都和最终目标RB-tree有密切关联。

1.树的导览

  1. 树(tree),是一种十分基础的数据结构。几乎所有操作系统都将文件存放在树状结构里;几乎所有的编译器都需要实现一个表达式树(expression tree) ;
  2. 文件压缩所用的哈夫曼算法(Huffman’s Algorithm)需要用到树状结构;数据库所使用的B-tree则是一种相当复杂的树状结构。
  3. 本章所要介绍的RB-tree(红黑树)是一种被广泛运用、可提供良好搜寻效率 的树状结构。
  4. 树由节点(nodes)和边(edges)构成,如下图所示。整棵树有一个最上端节点,称为根节点(root)。每个节点可以拥有具方向性的边(directed edges) 用来和其它节点相连。相连节点之中,在上者称为父节点(parent), 在下者称为子节点(child)。无子节点者称为叶节点(leaf)。
  5. 子节点可以存在多个,如果最 多只允许两个子节点,即所谓二叉树(binary tree)。
  6. 不同的节点如果拥有相同的父节点,则彼此互为兄弟节点(siblings)。
  7. 根节点至任何节点之间有唯一路径(path), 路径所经过的边数,称为路径长度(length)。
  8. 根节点至任一节点的路径长度,即所谓该节点的深度(depth)。根节点的深度永远是0。
  9. 节点至其最深子节点(叶节点)的路径长度,称为该节点的高度(height)。
  10. 整棵树的高度,便以根节点的高度来代表。节点A➔B之间如果存在(唯一)一条路径,那么A称为B的祖代(ancestor),B称为A的子代(descendant)。如何节点的大小(size)是指其所有子代(包括自己)的节点数。

在这里插入图片描述
1.1 二叉搜索树(binary search tree)

  1. 所谓二叉树(binary tree) , 其意义是: “任何节点最多只允许两个子节点 。 这两个子节点称为左子节点和右子节点。
  2. 如果以递归方式来定义二叉树,我们可以说:“一个二叉树如果不为空,便是由一个根节点和左右两子树构成:左右子树都可能为空”。
    二叉树运用极广,先前提到的编译器表达树(exression tree)和哈夫曼编码树(Huffman coding tree)都是二叉树,如下图所示:
    在这里插入图片描述

所谓二叉搜索树(binary search tree) , 可提供对数时间(logarithmic time) 的元素插入和访问。 二叉搜索树的节点放置规则是:任何节点的键值一定大于其左子树中的每一个节点键值,并小于其右子树的每一个节点的键值。因此,从根节点一直往前左走,直至无左路可走,即得最小元素;从根节点一直往右走,直至无右路可走,即最大元素,如下图所示的就是一棵二叉搜索树。
在这里插入图片描述

要在一棵二叉搜索树中找出最大元素或最小元素,是一件极简单的事:就像上述所言,一直往左走或一直往右走即是。
比较麻烦的是元素的插入和移除。 下图是二叉搜索树的元素插人操作图解。 插入新元素时, 可从根节点开始, 遇键值较大 者就向左, 遇键值较小者就向右, 一直到尾端,即为插入点。
在这里插入图片描述

下两个图是二叉搜索树的元素移除操作图解。 欲删除旧节点A, 情况可分两种:

  1. 如果A只有一个子节点, 我们就直接将A的子节点连至A的父节点, 并将A删除。
  2. 如果A有两个子节点,我们就以右子树内的最小节点取代A。 注意,右子树的最小节点极易获得:从右子节点开始(视为右子树的根节点), 一直向左走至底即是。

①. 只有一个字节点:
在这里插入图片描述
②. 有两个子节点:
在这里插入图片描述

1.2 平衡二叉搜索树(balanced binary search tree)

也许因为输入值不够随机,也许因为经过某些插入或删除操作,二叉搜索树可能会失去平衡, 造成搜寻效率低落的情况, 如下图所示。
在这里插入图片描述

  1. 所谓树形平衡与否,并没有一个绝对的测量标准。
  2. “平衡” 的大致意义是:没有任何一个节点过深(深度过大)。
  3. 不同的平衡条件,造就出不同的效率表现,以及不同的实现复杂度。
  4. 有数种特殊结构如AVL-tree、RB-tree、AA-tree, 均可实现出平衡二叉搜索树, 它们都比一般的(无法绝对维持平衡的)二叉搜索树复杂, 因此,插入节点和删除节点的平均时间也比较长,但是它们可以避免极难应付的最坏(高度不平衡)情况,而且由于它们总是保持某种程度的平衡,所以元素的访问 (搜寻)时间平均而言也就比较少。 一般而言其搜寻时间可节省25%左右。

1.3 AVL tree(Adelson-Velskii-Landis tree)

AVL tree是一个 “加上了额外平衡条件” 的二叉搜索树。 其平衡条件的建立是为了确保整棵树的深度为O(logN)。直观上的最佳平衡条件是每个节点的左右子 树有着相同的高度,但这未免太过严苛,我们很难插入新元素而又保持这样的平衡条件。 AVL tree于是退而求其次, 要求任何节点的左右子树高度相差最多1。这是 一个较弱的条件, 但仍能够保证 ”对数深度(logarithmic depth) "平衡状态。
下图左侧所示的是一个AVL tree,插入了节点11之后(图右),灰色节点违法ATL tree的平衡条件。由于只有“插入点至根节点”路径上的个节点可能改变平衡状态,因此,只有调整其中最深的哪个节点,便可使整棵树重新获得平衡。
在这里插入图片描述
前面说过,只要调整“插入点至根节点”路径上,平衡状态被破坏之各节点中最深的哪一个,便可使整棵树重新获得平衡。假设该最深节点为X,由于节点最多拥有两个子节点,而所谓“平衡被破坏”意味着X的左右两棵树的高度相差2,因此可以轻易将情况分为如下四种:

  1. 插入点位于X的左子节点的左子树:左左。
  2. 插入点位于X的左子节点的右子树:左右。
  3. 插入点位于X的右子节点的左子树:右左。
  4. 插入点位于X的右子节点的右子树:右右。

情况1, 4彼此对称,称为外侧(outside)插入,可以采用单旋转操作(single rotation)调整解决。情况2,3彼此对称,称为内侧(inside)插入,可以采用双旋转操作(double rotation)调整解决。

AVL-tree的四种“平衡破坏”的情况:

在这里插入图片描述

1.4 单旋转(Single Rotation)

在外侧插入状态中,k2“插入前平衡,插入后不平衡”的唯一情况如下图左侧所示,A子树成长了一层,致使它比C子树的深度多2。 B子树不可能和A子树位于同一层,否则k2在插入前就处于不平衡状态了。B子树叶不可能和C子树位于同一层,否则第一个违法平衡条件的将是K1而不是k2。

在这里插入图片描述

  1. 为了调整平衡状态,我们希望将A子树提高一层,并将C子树下降一层,这已经比AVL-tree所要求的平衡条件更进一步了。图右侧即是调整后的情况。
  2. 我们可以这么想象,把k1向上提起,使k2自然下滑,并将B子树挂到k2的左侧。 这么做是因为,二叉搜索树的规则使我们知道,k2>k1, 所以k2必须成为新树形中的k1的右子节点。
  3. 二叉搜索树的规则也告诉我们,B子树的所有节点的键值都在kl和k2之间,所以新树形中的B子树必须落在k2的左侧。
  4. 以上所有调整操作都只需要将指针稍做搬移,就可迅速达成。完成后的新树形 符合AVL-tree的平衡条件,不需再做调整。上图所显示的是“左左“外侧插入。至于“右右“外侧插入,情况如出一辙。

1.5 双螺旋(Double Rotation)

  1. 下图左侧为内侧插入所造成的不平衡状态。单旋转无法解决这种情况。
  2. 第一,我们不能再以k3为根节点,其次,我们不能将k3和k1做一次单旋转,因为 旋转之后还是不平衡(你不妨自行画图试试)。
  3. 唯一的可能是以k2为新的根节点, 这使得(根据二叉搜索树的规则)k1必须成为k2的左子节点,k3必须成为k2的 右子节点,而这么一来也就完全决定了四个子树的位置。
  4. 新的树形满足AVL-tree 的平衡条件,并且,就像单旋转的情况一样,它恢复了节点插入之前的高度,因此保证不再需要任何调整。

以双螺旋修正内侧插入而导致的不平衡,本图显示的是 “左右 “ 内侧插入。 至于"右左 “ 内侧插入, 情况如出 一 辙:
在这里插入图片描述

为什么称这种调整为双旋转呢?因为它可以利用两次单旋转完成,如下图。
双旋转(如上图)可由两次单旋转合并而成。这对编程带来不少方便。
在这里插入图片描述

以上所有调整操作都只需要将指针稍做搬移,就可迅速达成。完成之后的新树 形符合AVL-tree的平衡条件,不需再做调整。

2.RB-tree(红黑树)

AVL-tree之外,另一个颇具历史并被广泛运用的平衡二叉搜索树是RB-tree(红黑树)。所谓RB-tree,不仅是一个二叉搜索树,而且必须满足以下规则:

  1. 每个节点不是红色就是黑色(图中深色底纹代表黑色,浅色底纹代表红色, 下同)。
  2. 根节点为黑色。
  3. 如果节点为红, 其子节点必须为黑。
  4. 任一节点至NULL(树尾端)的任何路径,所含之黑节点数必须相同。

根据规则4, 新增节点必须为红;根据规则3, 新增节点之父节点必须为黑。 当新节点根据二叉搜索树的规则到达其插入点,却未能符合上述条件时, 就必须调整颜色并旋转树形。 如下图说明。
在这里插入图片描述

2.1 插入节点

延续上图状态,插入一些新节点,会产生声明变化。举出四种不同的典型。以上图的RB-tree分别插入3, 8, 35, 75, 根据二叉搜索树的规则, 这匹个新节点的落脚处应该如下图所示, 它们都破坏了RB-tree的 规则, 因此我们必须调整树形, 也就是旋转树形并改变节点颜色。

下图为RB-tree插人四个新节点:3,8,35, 75。新增节点必为红色,暂以空心粗框表示。不论插入3,8,35,75之中的哪一个节点,都会破坏RB-tree的规则,使我们必须旋转树形并调整节点的颜色。

在这里插入图片描述

  1. 为了方便讨论,为某些特性节点定一些代名,以下讨论都将沿用这些代名。
  2. 假设新节点为X,其父节点为P,祖父节点为G,伯父节点(父节点之兄弟节点)为S,曾祖父节点为GG。
  3. 现在,根据二叉搜索树的规则,新节点X必为叶节点。根据红黑树规则4,X必为红。若P亦为红(这就违反了规则3,必须调整树形),则G必为黑(因为原为RB-tree,必须遵循规则3)。
  4. 于是,根据X的插入位置以及外围节点(S和GG)的颜色,有了以下四种考虑。

①.状况1:S为黑且为外侧插入,对此情况,先对P,G做一次单旋转,再做更改P,G颜色,即可重新满足红黑树的规则3,如下图所示:

S为黑且X为外侧插入。先对P,G做一次单旋转,再更改P,G颜色,即可重新满足红黑树规则3。
在这里插入图片描述
注意,此时可能产生不平衡状态(高度相差1以上)。例如图中的A和B为null, D或E不为null。这倒没关系,因为RB-tree的平衡性本来就比AVL-tree弱。 然而RB-tree通常能够导致良好的平衡状态。是的,经验告诉我们,RB-tree的搜寻平均效率和AVL-tree几乎相等。

②.状况2:S为黑且X为内侧插入。对此情况,我们必须先对P,X做一次单旋转 并更改G,X颜色,再将结果对G做一次单旋转,即可再次满足红黑树规则3。如下图所示:

函数所示为SGI<stl_tree.h>所提供的函数,用于左旋或右旋。
在这里插入图片描述

③.状况3: S为红且X为外侧插入 对此情况, 先对P和G做 一次单旋转,并改变X的颜色。此时如果GG为黑,一切搞定,如下图所示,但如果GG为红,则问题比较大,见状况④。
RB-tree元素插入状况3:
在这里插入图片描述

④.状况4:S为红且X为外侧插入。对此情况,先对P和G做一次单旋转,并改变X的颜色。此时如果GG为红,还得持续往上做,直到不再有父子连续为红的情况。
RB-tree元素插入状况4:
在这里插入图片描述

2.2 —个由上而下的程序

为了避免状况4 "父子节点皆为红色”的情况持续向RB-tree的上层结构发展,形成处理时效上的瓶颈,我们可以施行一个由上而下的程序(top-down procedure) : 假设新增节点为A,那么就延着A的路径,只要看到有某节点X的两 个子节点皆为红色,就把X改为红色,并把两个子节点改为黑色,如下图所示。
沿着X的路径,由上而下修正节点颜色。
在这里插入图片描述

但是如果A的父节点P亦为红色(注意,此时S绝不可能为红),就得像状况1一样地做一次单旋转并改变颜色,或是像状况2一样地做一次双旋转并改变颜色。
在此之后,节点35的插入就很单纯了:要么直接插入,要么插入后再一次单旋转即可,如图所示:
延续上图右侧状态,对G,P做一次右旋转,并改变颜色。
在这里插入图片描述
2.3 RB-tree的节点设计

RB-tree有红黑二色,并且拥有左右子节点,我们很容易就可以勾勒出其结构风貌。下面是SGISTL的实现源代码。为了有更大的弹性,节点分为两层,稍后下面的图将显示节点双层结构和迭代器双层结构的关系。

从以下的minimum()和maximum()函数可清楚看出,RB-tree作为一个二叉搜索树,其极值是多么容易找到。由于RB-tree的各种操作时常需要上溯其父节点,所以特别在数据结构中安排了一个parent指针。

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; //RB树的许多操作,必须知道父节点
base_ptr left; //指向左节点
base_ptr right; //指向右节点

static base_ptr minimum(base_ptr x)
{
while(x->left !=0) x=x->left;//一直向左走,就会找到最小值
return x;//这是二叉搜索树的特性
}

static base_ptr maximum(base_ptr x){
while(x->right !=0)x = x->right;//一直向右走,就会找到最大值
return x;//这是二叉搜索树的特性
}
};

template <class Value>
struct __rb_tree_node : public __rb_tree_node_base
{
typedef __rb_tree_node<Value>* link_type;
Value value_field;//节点值
};

下面是RB-tree的节点图标,其中将__rb_tree_node::value_field填为10:
在这里插入图片描述

2.4 RB-tree的迭代器

要成功地将RB-tree实现为一个泛型容器,迭代器的设计是一个关键。首先我们要考虑它的类别(category), 然后要考虑它的前进(increment)、后退 (decrement)、提领(dereference)、成员访问(member access)等操作。

为了更大的弹性,SGI将RB-tree迭代器实现为两层,这种设计理念和前面的slist类似。下图所示的便是双层节点结构和双层迭代器结构之间的关系,其中主要意义是:_rb_tree_node继承自_rb_tree_node_base , _rb_tree_iterator继承自_rb_tree_base_iterator。有了这样的认识,就可以将迭代器稍做转型,然后解开RB-tree的所有奥秘,追踪其一切状态。

RB-tree的节点和迭代器之间的关系,这种双层架构和slist极其相似。
在这里插入图片描述

  1. RB-tree迭代器属于双向迭代器,但不具备随机定位能力,其提领操作和成员访间操作与list十分近似,较为特殊的是其前进和后退操作。
  2. RB-tree迭代器的前进操作operator++()调用了基层迭代器的increment()。
  3. RB-tree迭代器的后退操作operator–()则调用了基础迭代器的decrement()。
  4. 前进或后退的举止行为完全依据二叉搜索树的节点排列法则,再加上实现上的某些特殊技巧。
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;//用来与容器之间产生一个连结关系(make a reference)
//以下其实可以实现于operator--内,因为再无他处会调用此函数了
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(node == y->right){//如果现行节点本身是个右子节点
node = y;//一直上溯,直到“不为右子节点”止
y = y->parent;
}
if(node -> right !=y)//若此时的右子节点不等于此时的父节点
node = y;//状况(3)此时的父节点即为解答,否则此时的node为解答,状况(4)
}

//注意,以上判断“若此时的右子节点不等于此时的父节点”,是为了应付一种
//特殊情况:我们欲寻找根节点的下一节点,而恰巧根节点无右子节点
//当然,以上特殊做法必须配合RB-tree根节点与特殊之header之间的
//特殊关系

//以下其实可实现于operator--内,因为再无他处会调用此函数了
 void decrement () 
 {
 if(node->color == __rb_tree_red && node->parent->parent == node)//如果是红节点,且父节点的父节点等于自己
 node = node->right;//状况(1)右子节点即为解答

//以上情况发生于node为header时(亦即node为end()时)
//注意,header之右子节点即mostright,指向整棵树的max节点
else if(node->left !=0){//如果有左子节点,状况(2)
base_ptr y=node->left;//令y指向左子节点
while(y->right !=0)//当y有右子节点时
y = y->right;//一直往右走到底
node = y;//最后即为答案
}
else{//既非根节点,无左子节点
base_ptr y = node->parent;//状况(3)找出父节点
while(node==y->left){//当现行节点身为左子节点
node = y;//一直交替往上走,直到现行节点
y = y->parent;//不为左子节点
}
node  = y;//此时之父节点即为答案
}
}
};

//RB-tree的正规迭代器
template <class Value,class Ref,class 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 Value*>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;}
#ifndef __SGI_STL_NO_ARROW_OPERATOR
pointer operator->()const {return &(operator*());}
#endif /* __SGI_STL_NO_ARROW_OPERATOR*/

self& operator++(){increment();return *this;}
self operator(int){
self tmp = *this;
increment();
return tmp;
}

self& operator--(){decrement();return *this;}
self operator--(int){
self tmp = *this;
decrement();
return tmp;
}
};

在_rb_tree_iterator_base的increment () 和decrement()两个函数中,较令人费解的是前者的状况4和后者的状况1(见源代码注释标示), 它们分别发生于下图所展示的状态下。
increment ()和decrement ()两函数中较令人费解的状况4和状况1。 其中的header是实现上的特殊技巧, 见稍后说明。
在这里插入图片描述

2.5 RB-tree的数据结构

下面是rb-tree的定义。 你可以看到其中定义有专属的空间配置器, 每次用来配置一个节点大小,也可以看到各种型别定义,用来维护整棵RB-tree的三笔数据(其中有个仿函数,functor, 用来表现节点的大小比较方式),以及一些member function的定义或声明。

template <class Key, class Value, class KeyOfValue, class Compare, 
class Alloc = alloc> 
class rb_tree { 
protected: 
typedef void* void_pointer;
typedef __rb_tree_node_base* base__ptr; 
typedef _rb_tree_node<Value> rb_tree_node;
typedef simple_alloc<rb_tree_node, Alloc> rb_tree_node_allocator;
typedef _rb_tree_color_type  color_type;
public:
//注意, 没有定义iterator (不, 定义在后面!)
typedef Key key_type; 
typedef Value value_type; 
typedef value_type* pointer; 
typedef const value_type* const__pointer;
typedef value_type& reference; 
typedef const value_type& const_reference; 
typedef rb_tree_node* link_type; 
typedef size_t size_type; 
typedef ptrdiff_t difference_type; 
protected:
link_type get_node(){return rb_tree_allocator::allocate();}
void put_node(link_type p){rb_tree_node_allocator::deallocate(p);}

link_type create_node(const value_type& x){
link_type tmp = get_node();//配置空间
__STL_TRY{
construct(&tmp->value_filed,x);//构造内容
}
__STL_UNWIND(put_node(tmp));
return tmp;
}
link_type clone_node(link_type x) { //复制一个节点(的值和色)
link_typetmp=create_node(x->value_field); 
tmp->color = x->color; 
tmp->left=O; 
tmp->right = O; 
return tmp; 
}

void destroy_node(link_type p){
destroy(&p->value_filed);//析构内容
put_node(p);//释放内存
}

protected:
// RB-tree只以三笔数据表现
size_type node_count; //追踪记录树的大小(节点数量)
link_type header; //这是实现上的个技巧
Compare key_compare; //节点间的键值大小比较准则。应该会是个function object

//以下三个函数用来方便取得header的成员
link_type& root()const{return (link_type&)header->parent; } 
link_type& leftmost()const { return (link_type&) header->left; } 
link_type& rightmost()const { return (link_type&) header->right; } 
//以下六个函数用来方便取得节点x的成员
static link_type& left(link_type x)
{return (link_type&)(x->left);}
static link_type right(link_type x)
{return (link_type&)(x->right);}
static link_type& parent(link_type x)
{return (link_type&)(x->parent);}
static link_type value(link_type x)
{return x->value_filend;}
static color_type& color(link_type x)
{return (color_type&)(x->color);}

//以下六个函数用来方便取得节点x的成员
static link_type& left(base_ptr x)
{return (link_type&)(x->left);}
static link_type& right(base_ptr x)
{return (link_type&)(x->right);}
static link_type& parent(base_ptr x}
{return (link_type&)(x->parent);}
static reference value(base_ptr x)
{return ((link_type&)x)->value_filed;}
static const Key& key(base_ptr x}
{return KeyOfValue()(value(link_type(x)));}
static color_type& color(base_ptr x)
{return (color_type&) (link_type(x}->color); }

//求取极大值和极小值。node class有实现此功能, 交给它们完成即可 
static link_type minimum(link_type x) { 
return (link_type) __rb_tree_node_base::minimum(x); 
}
static link_type maximum(link_type x) {
return (link_type) _rb_tree_node_base::maximum(x);
}


public:
typedef __rb_tree_iterator<value_type,reference,pointer> iterator;
private:
iterator __insert(base_ptr x,base_ptr y,const value_type& v);
link_type __copy(link_type x,link_type p);
void __erase(link_type x);
void init(){
header = get_node();
color(header) = __rb_tree_red;//令header为红色,用来区分header和root,在iterator.operator++之中

root() = 0;
leftmost() = header;//令header的左子节点为自己
rightmost() = header;//令header的右子节点为自己
}
public:			//allocation/deallocation
rb_tree(const Compare& comp = Compare()):node_count(0),key_compare(comp){init();}
~rb_tree(){
clear();
put_node(header);
}
rb_tree<Key,Value,KeyOfValue,Compare,Alloc>&operator=(const rb_tree<Key, Value, KeyOfValue, Compare, Alloc>& x); public: 
Compare key_comp() const { return key_compare; } 
iterator begin() { return leftmost();} //RB树的起头为最左(最小)节点处 
iterator end () { return header; } //RB树的终点为header所指处
bool empty() const { return node_count== 0; } 
size_type size() const{ return node_count; } 
size_type max_size() const { return size_type(-1);}
public:
					//insert/erase
//将 x插入到RB-tree中(保持节点值独一无二)
pair<iterator,bool> insert_unique(const value_type& x); 
//将 x插入到RB-ree中(允许节点值重复)。
iterator insert_equal(const value_type x);
...
};

2.6 RB-tree的构造与内存管理

下面是RB-tree所定义的专属空间配置器rb_tree_node_allocator, 每次可恰恰配置一个节点。 它所使用的sirnple_alloc<>定义于第二章:

template <class Key, class Value, class KeyOfValue, class Compare,class Alloc=alloc>
class rb_tree{
protected:
typedef __rb_tree_node<Value> rb_tree_node;
typedef simple_alloc<rb_tree_node,Alloc> rb_tree_node_allocator;
...};

先前所列的程序片段也显示了数个节点相关函数,如get_node (), put_node () create_node(), clone_node(), destroy_node()。

RB-tree的构造方式有两种,一种是以现有的RB-tree复制应该新的RB-tree,另一种是产生一棵空空如也的树,如下所示:

rb_tree<int ,int,identity<int>,less<int> >itree;

这行程序代码分别指定了节点的键值、实值、大小比较标准…然后调用RB-tree的default constructor:

rb_tree(const Compare& comp = Compare()):node_count(0),key_compare(comp){init();}

其中的init()是实现技巧上的一个关键字:

private:
void init(){
header = get_node();//产生一个节点空间,令header指向它
color(header) = rb_tree_red;//令header为红色,用来区分header和root(在iterator.operator++中)
root() = 0;
leftmost() = header;//令header的左子节点为自己
rightmost() = header;//令header的右子节点为自己
}

树状结构的各种操作,最需注意的就是边界情况的发生,也就是走到根节点时要有特殊的处理。为了简化处理,SGI STL特别为根节点再设计个父节点,名为header,并令其初始状态如下图所示。
左图是RB-tree的初始状态,图右为加入第一个节点后的状态。
在这里插入图片描述
接下来,每当插入新节点时,不但要依照RB-tree的规则来调整,并且维护header的正确性,使其父节点指向根节点,左子节点指向最小节点,右子节点指向最大节点。节点的插入所带来的影响,是下一小节的描述重点。

2.7 RB-tree的元素操作

  1. 介绍元素(节点)的插入和搜寻,RB-tree提供两种插人操作:insert_unique ()和insert_equal ()。
  2. 前者表示被插入节点的键值(key)在整棵树中必须独一无二(因此, 如果树中已存在相同的键值,插入操作就不会真正进行)。
  3. 后者表示被插入节点的键值在整棵树中可以重复, 因此,无论如何插入都会 成功(除非空间不足导致配置失败)。
  4. 这两个函数都有数个版本, 以下以最简单的 版本(单一参数, 用以表现将被插人的节点实值(value))作为说明对象。
  5. 注意, 虽然只指定实值,但RB-tree 一开始即要求用户必须明确设定所谓的KeyOfValue仿函数, 因此, 从实值(value)中取出键值(key)是毫无问题的。

①元素插入操作insert_equal()

//插入新值;节点键值允许重复
//注意 , 返问值是一个RB-tree迭代器, 指向新增节点
template <class Key, class Value, class KeyOfValue, class Compare, class Alloc>
typename rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::iterator 
rb_tree<Key, value, KeyOfValue, Compare, Alloc>::insert_equal(const Value& v) 
{
link_type y = header;
linke_type x =root();
while(x !=0){
y = x;
x = key_compare(KeyOfValue()(v),key(x)) ? left(x) : right(x);
//以上,遇“大”则往左,遇“小或等于”则往右
}

②.元素插入操作insert_unique()

//插人新值:节点键值不允许重复,若重复则插人无效
//注意,返回值是个pair,第一元素是个RB-tree迭代器,指向新增节点, 
//第二叉素表示插入成功与否
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_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所指即插入点之父节点(此时的它必为叶节点)
iterator j = iterator(y) ; //令迭代器j指向插入点之父节点y
if (comp) //如果离开while循环时comp为真(表示遇 ”大” ,将插入于左侧)
if(j==begin())//如果插入点之父节点为最左节点
return pair<iterator,bool>(_insert(x, y, v), true); 
//以上, x为插入点,y为插人点之父节点,v 为新值
else //否则(插入点之父节点不为最左节点)
--j;//调整j,回头准备测试...
if(key_compare(key(j.node),KeyOfValue()(v)))
//小于新值(表示遇“小",将插入于右侧)
return pair<iterator,bool>(_insert(x,y, v),true); 
//以上,x为新值插人点,y为插入点之父节点,v为新值

//进行至此,表示新值一定与树中键值重复,那么就不该插人新值 
return pair<iterator,bool>(j,false); 
}

③.真正的插入执行程序__insert()

template<class Key, class Value, class KeyOfValue, 
class Compare, class Alloc> 
typename rb_tree<Key,Value,KeyOfValue,Compare, Alloc>::iterator
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; 
// key_compare是键值大小比较准则。应该会是个function object 
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) { 
root() = z;
rightmost() = z;
}
else if(y == leftmost())//如果y为最左节点
leftmost() = z;//维护leftmost() , 使它永远指向最左节点
}
else{
z = create_node(v) ;//产生一个新节点
right(y) = z; //令新节点成为插人点之父节点y的右子节点
if(y == rightmost())
rightmost() = z;//维护rightmost(),使它永远指向最右节点
}
parent(z) = y;//设定新节点的父节点
left(z) = 0;//设定新节点的左子节点
right(z) = 0;//设定新节点的右子节点
			 //新节点的颜色将在__rb_tree_rebalance()设定(并调整) 
__rb_tree_rebalance(z, header->parent);//参数一为新增节点,参数二为root 
++node_count; //节点数累加
return iterator(z); //返回一个迭代器,指向新增节点
}

④.调整RB-tree(旋转及改变颜色)

任何插人操作,于节点插入完毕后,都要做一次调整操作,将树的状态调整到符合RB-tree的要求。_rb_tree_rebalance()是具备如此能力的一个全局函数:

//全局函数
//重新令树形平衡(改变颜色及旋转树形)
//参数为新增节点,参数二为root
inline void 
__rb_tree_rebalance(__rb_tree_node_base* x,__rb_tree_node_base*& root)
{
x->color = _rb_tree_red;//新节点必为红
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);//第一参数为左旋点
}
}
}//while结束
root->color=__rb_tree_black;//根节点永远为黑
}

这个树形调整操作,就是5.2节所说的那个“由上而下的程序"。从源代码清楚可见,某些时候只需调整节点颜色,某些时候要做单旋转,某些时候要做双旋转(两次单旋转);某些时候要左旋,某些时候要右旋。下面是左旋函数和右旋函数:

//全局函数
//新节点必为红节点。如果插入处之父节点亦为红节点,就违反红黑树规则,此时必须
 //做树形旋转(及颜色改变,在程序它处)
inline void 
_rb_tree_rotate_left(__rb_tree_node_base* x, __rb_tree_node_base*& root)
{
// X为旋转点
_rb_tree_node_base* y = x->right; //令y为旋转点的右子节点 
x->right=y->left;
if(y->left! =0) 
y->left->parent = x; //别忘了回马枪设定父节点
y->parent = x->parent;
//令y完全顶替x的地位(必须将x对其父节点的关系完全接收过来)
if (x == root)// X为根节点
root= y; 
else if (x == x->parent->left) // X为其父节点的左子节点
x->parent->left = y; 
else // X为其父节点的右子节点
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)
{
// X为旋转点
__rb_tree_node_base*y = x->left; // y为旋转点的左子节点 
x->left = y->right; 
if (y->right != 0) 
y->right->parent = x; //别忘了回马枪设定父节点
y->parent = x->parent;
//令y完整替代x的位置(必须将x对其父节点的关系完全接收过来)
if(x == root)//x为根节点
root = y;
else if (x == x->parent->right)// X为其父节点的右子节点
x->parent->right = y; 
else // X为其父节点的左子节点
x->parent->left = y;
y->right = x; 
x->parent=y; 
}

下面是客户端程序连续插入数个元素到RB-tree中并加以测试的过程:

// file:5rbtree-test.cpp
rb_tree<int, int, identiity<int>,less<int> > itree;
cout<<itree.size() << endl; // 0

//以下注释中所标示的函数名称, 是修改<stl_tree.h> , 令三个函数 
//打印出函数名称而后得

itree.insert_unique(10);//__rb_tree_rebalance
itree.insert_unique(7);//_rb_tree_rebalance
itree.insert_unique(8);//_rb_tree_rebalance
					   //__b_tree_rotate_left
					   //__rb_tree_rotate_right
itree.insert_unique(15);//__rb_tree_rebalance
itree.insert_unique(5); //__rb_tree_rebalance
itree.insert_unique(6);  //__rb_tree_rebalance 
						//__rb_tree_rotate_left
						//__rb_tree_rotate_right
itree.insert_unique(11); //__rb_tree_rebalance
						//__rb_tree_rotate_right
						//__rb_tree_rotate_left
itree.insert_unique(13); 
itree.insert_unique(12); // __rb_tree_rebalance
cout << itree.size() << endl; // 9 
for(; ite1 != ite2; ++ite1) 
cout<< *ite1<<' ';//5 6 7 8 10 11 12 13 15
cout<<endl;
//测试颜色和operator++ (亦即__rb_tree_iterator_base::increment)
rb_tree<int, int, identity<int>, less<int> >::iterator
ite1=itree.begin(); 
rb_tree<int, int, identity<int>, less<int> >::iterator
ite2=itree.end(); 
__rb_tree_base_iterator rbtite;
for(; ite1 != ite2; ++ite1) {
rbtite = _rb_tree_base_iterator(ite1);
//以上,向上转型up-casting, 永远没问题。 
cout<< *ite1 << '(' << rbtite.node->color << ") "; 
}
cout << endl; 
//结果: 5(0) 6(1) 7(0) 8(1) 10(1) 11(0) 12(0) 13(1) 15(0)

下图是上述程序操作的完整图标, 一步一 步展现RB-tree的成长与调整。
在这里插入图片描述
在这里插入图片描述
⑤.元素的搜寻

RB-tree是一个二叉搜索树,元素的搜寻正是其拿手项目。以下是RB-tree提供的find函数:

//寻找RB树中是否有键值为K的节点
template<class Key, class Value, class KeyOfValue, class Compare, class Alloc> 
typename rb_tree<Key,Value,KeyOfValue,Compare,Alloc>::iterator
rb_tree<Key,Value,KeyOfValue,Compare,Alloc>::find(const Key& k){
link_type y = header; // Last node which is not less thank. 
link_type x =root(); // Current node. 
while (x != 0) 
//以下,key_compare是节点键值大小比较准则。应该会是个function object
 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;
}

3.set

  1. set的特性是,所有元素都会根据元素的键值自动被排序。
  2. set的元素不像map那样可以同时拥有实值(value)和键值(key), set元素的键值就是实值, 实值就是键值。
  3. set不允许两个元素有相同的键值。
  4. 我们可以通过set的迭代器改变set的元素值吗?不行,因为set元素值就是其键值,关系到set元素的排列规则。
  5. 如果任意改变set元素值,会严重破坏 set组织。
  6. 稍后你会在set源代码之中看到,set:: iterator被定义为底层RB-tree的const_iterator,杜绝写入操作。换句话说,set iterators是一种constant iterators (相对于mutable iterators)。
  7. set拥有与list相同的某些性质:当客户端对它进行元素新增操作 (insert)或删除操作(erase)时,操作之前的所有迭代器,在操作完成之后都依然有效。当然,被删除的那个元素的迭代器必然是个例外。
  8. STL特别提供了一组set/multiset相关算法,包括交集 set_intersection、联集set_union、差集set_difference、对称差集 set_syrnmetric_difference。
  9. 由于RB-tree是一种平衡二叉搜索树,自动排序的效果很不错,所以标准的STL set即以RB-tree为底层机制(以hash-table为底层机制的set,称为hsh_set)。又由于set所开放的各种操作接口,RB-tree也 都提供了,所以几乎所有的set操作行为,都只是转调用RB-tree的操作行为而已。

下面是set的源代码摘录,其中的注释几乎说明了一切,本节不再另做文字解释。

template<class Rey,class Compare = less<Key>,class Alloc = alloc>
class set{
public:
//typedefs:
typedef Key Key_type;
typedef Key value_type;
//注意,以下key_compare和value_compare使用同一个比较函数
typedef Compare key_compare;
typedef Compare value_compare;
private:
//注意,以下的identity定义于<stl_function.h>,参见第7章,其定义为:
/*
template <class T> 
struct identity : public unary_function<T,T> {
const T& operator() (const T& x) const { return x; }
};
*/
typedef rb_tree<key_type, value_type, identity<value_type>,key_compare,Alloc> rep_type;
rep_type t;//采用红黑树(RB-tree)来表现
public:
typedef typename rep_type: :const_pointer pointer; 
typedef typename rep_type::const_pointer const_pointer; 
typedef typename rep_type::const_reference reference; 
typedef typename rep_type::const_reference const_reference; 
typedef typename rep_type:: const_iterator iterator; 
//注意上一行,iterator定义为RB-tree的const_iterator,这表示set的 
//迭代器无法执行写入操作。这是因为set的元素有一定次序安排
//不允许用户在任意处进行写人操作
typedef typename rep_type::const_iterator const_iterator; 
typedef typename rep_type::const_reverse_iterator reverse_iterator; 
typedef typename rep_type::const_reverse_iterator const_reverse_iterator;
typedef typename rep_type::size_type size_type; 
typedef typename rep_type::difference_type difference_type;

// allocation/deallocation 
//注意,set一定使用RB-tree的insert_unique()而非insert_equal() 
// multiset才使用RB-tree的insert_equal() 
//因为set不允许相同键值存在,multiset才允许相同键值存在
set():t(Compare()){} 
explicit set(const Compare& comp) : t(comp) {} 
template <class InputIterator>
set(Inputiterator first, Inputlterator last):t(Compare()){t.insert_unique(first,last);}
template<class InputIterator>
set(InputIterator fist,Inputiterator last,const Compare& comp):t(comp){t.insert_unique(first,last);}

set(const set<Key, Compare, Alloc>& x) : t(x.t) {} 
set<Key,Compare, Alloc>& operator=(const set<Key,Compare, Alloc>& x) 
{ t=x.t;
return *this;
}

//以下所有的set操作行为,RB-tree都已提供,所以set只要传递调用即可
// accessors: 
key_compare key_comp() const{return t.key_comp(); } 
//以下注意,set的value_comp()事实上为RB-tree的key_comp () 
value_compare value_comp() const { return t.key_comp();} 
iterator begin() const{return t.begin(); } 
iterator end() const { return t.end();} 
reverse_iterator rbegin() const { return t.rbegin(); } 
reverse_iterator rend() const { return t.rend(); } 
bool empty() const { return t.empty();} 
size_type size() const {return t.size()}
size_type max_size() const {return t.max_size();}
void swap(set<Key,Compare,Alloc>& x){t.swap(x.t);}

//insert/erase 
typedef pair<iterator, bool> pair_iterator_bool;
pair<iterator,bool>insert(const value_type& x) { 
pair<typename rep_type::iterator,bool> p =t.insert_unique(x); 
return pair<iterator, bool>(p.first,p.second); 
}
iterator insert(iterator positon,const value_type& x) {
typedef typename rep_type::iterator rep_iterator;
return t.insert_unique((rep_iterator&)position, x);
 }
template <class InputIterator>
void insert(InputIterator first,InputIterator lst){
t.insert_unique(first,last);
}
void erase(iterator position){
typedef typename rep_type::iterator rep_iterator; 
t.erase((rep_iterator&)position);
}
szie_type erase(const key_type& x){
return t.erase(x);
}
void erase(iterator first,iterator last){
typedef typename rep_type::iterator rep_iterator;
t.erase((rep_iterator&)first,(rep_iterator&)last);
}
void clear() { t. clear() ;} 
// set operations: 
iterator find(const key_type& x) const{returnt.find(x); } 
size_type count(const key_type&x) const{returnt. count (x); } 
iterator lower_bound(const key_type&x) const{
return t.lower_bound(x);
}
iterator upper_bound(const key_type& x)const{
return t.upper_bound(x);
}
pair<iterator,iterator>equal_range(const key_type& x)const{
return t.equal_rang(x);
}

//以下的_STL_NULL_TMPL_ARGS 被定义为<>
friend bool operator== _STL_NULL_TMPL_ARGS (const set&, const set&); 
friend bool operator< _STL_NULL_TMPL_ARGS (const set&, const set&); 
};
template <class Key, class Compare, class Alloc> 
inline bool operator==(const set<Key, Compare, Alloc>& x,const set<Key,Compare,Alloc>& y){
return x.t == y.t;
}
template <class Key, class Compare, class Alloc> 
inline bool operator<(const set<Key, Compare, Alloc>& x,const set<Key,Compare,Alloc>& y){
return x.t<y.t;
}

下面是个set测试程序:

#include<set>
#include<iostream>
using namespace std;
int main()
{
int i;
int ia[5] = {0,1,2,3,4};
set<int>iset(ia,ia+5);

cout<<"size="<<iset.size()<<endl;//size=5
cout<<" 3 count="<<iset.count(3)<<endl;//3 count=1
iset.insert(3);
cout<<"size="<<iset.size()<<endl;//size=5
cout<<"3 count="<<iset.count(3)<<endl;//3 count=1
iset.insert(5);
cout<<"size="<<iset.size()<<endl;//size=6
cout<<"3 count="<<iset.count(3)<<endl;//3 count=1
iset.insert(1);
cout<<"size="<<iset.size()<<endl;//size=5
cout<<"3 count="<<iset.count(3)<<endl;//3 count=1
cout<<"1 count="<<iset.count(1)<<endl;//1 count=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 5

//使用STL算法find()来搜寻元素,可以有效运作,但不是好办法
ite1 = find(iset.begin(), iset.end(), 3); 
if (ite1 != iset.end()) 
cout<<"3 found"<<endl;//3 found

ite1 = find(iset.begin(),iset.end(), 1); 
if (ite1==iset. end()) 
cout<<"1 not found"<<endl;//1 not found

//面对关联式容器,应该使用其所提供的find函数来搜寻元素,会比使用STL算法find()更有效率。因为STL算法find()只是循序搜寻
ite1 = iset.find(3); 
if (itel != iset.end()) 
cout<<"3 found"<<endl;//3 found
 
ite1 = iset.find(1); 
if (ite1 == iset.end())
cout<<"1 not found"<<endl;//1 not found

//企图通过迭代器来改变set元素,是不被允许的
*ite1 = 9; // error, assignment of read-only location
}

4.map

map的特性是,所有元素都会根据元素的键值自动被排序。map的所有元素都是pair,同时拥有实值(value)和键值(key)。pair的第一元素被视为键值,第二元素被视为实值。map不允许两个元素拥有相同的键值。下面是<stl_pair.h>
中的pair定义:

template<class T1,calss T2>
struct pair{
typedef T1 first_type;
typedef T2 second_type;
T1 first;//注意,它是public
T2 second;//注意,它是public
pair():first(T1()),second(T2()){}
pair(const T1& a,const T2& b):first(a),second(b){}
};

可以通过map的迭代器改变map的元素内容吗?

  1. 如果想要修正元素的键值,答案是不行,因为map元素的键值关系到map元素的排列规则。任意改变map 元素键值将会严重破坏map组织。
  2. 但如果想要修正元素的实值,答案是可以,因为map元索的实值并不影响map元素的排列规则。因此,map iterators既不是一种 constant iterators, 也不是一种mutable iterators。

map拥有和list相同的某些性质:当客户端对它进行元素新增操作 (insert)或删除操作(erase)时,操作之前的所有迭代器,在操作完成之后都依然有效当然,被删除的那个元素的迭代器必然是个例外。

由于RB-tree是 一种平衡二叉搜索树,自动排序的效果很不错,所以标准的STL map即以RB-tree为底层机制(提供hash table为底层机制map 称为hash_map)。又由于map所开放的各种操作接口,RB-tree也 都提供了,所以几乎所有的map操作行为,都只是转调用RB-tree的操作行为而已。

下图说明map的架构,下页是map源代码摘录,其中注释说明一切。

SGI STL map以红黑树为底层机制, 每个节点的内容是一个pair,pair的第一元素被视为键值(key),第二元素被视为实值(value):
在这里插入图片描述

//注意,以下Key为键值(key)型别,T为实值(value)型别
template <class Key,class T,
class Compare = less<Key> ,//缺省采用递增排序
class Alloc =alloc>
class map{
public:
//typedefs: 
typedef Key key_type; //键值型别
typedef T data_type;//数据(实值)型别
typedef T mapped_type;
typedef pair<const Key, T> value_type; //元素型别(键值/实值) 
typedef Compare key_compare; //键值比较函数

//以下定义 一个functor, 其作用就是调用 “元素比较函数”
class value_compare 
: public binary_function<value_type, value_type, bool> {
friend class map<Key, T, Compare, Alloc>;
protected: 
Compare comp; 
value_compare(Compare c) : comp(c) { } 
public: 
bool operator() (const value_type& x, const value_type& y) const {
return comp(x.first, y.first);
}
};

private:
//以下定义表述型别(representation type)。 以map元素型别( 一个pair) 
//的第一型别,作为RB-tree节点的键值型别
typedef rb_tree<key_type, value_type, select lst<value_type>, key_cornpare, Alloc> rep_type;
rep_type t;//以红黑树(RB-tree)表现map
public:
typedef typenarne rep_type::pointer pointer; 
typedef typenarne rep_type::const_pointer const_pointer; 
typedef typenarne rep_type::reference reference; 
typedef typenarne rep_type::const_reference const_reference; 
typedef typenarne rep_type::iterator iterator; 
//注意上一行, map并不像set 一样将iterator定义为RB-tree的
// const_iterator。 因为它允许用户通过其迭代器修改元素的实值(value)
typedef typenarne rep_type::const_iterator const_iterator; 
typedef typenarne rep_type::reverse_iterator reverse_iterator;
typedef typenarne rep_type::const_reverse_iterator const_reverse_irerator;
typedef typenarne rep_type::size_type size_type; 
typedef typenarne rep_type::difference_type difference_type; 
// allocation/deallocation
//注意,map一定使用底层RB-tree的insert_unique()而非insert_equal()
// multimap才使用insert_equal() 
//因为map不允许相同键值存在,multimap才允许相同键值存在
map(): t(Compare()) {} 
explicit map(const Compare& comp):t(comp){} 
template<class InputIterator>
map(InputIterator first, Inputiterator last):t(Compare()){t.insert_unique(first,last);}

template<class InputIterator>
map(InputIterator first,InputIterator last, const Compare& comp):t(comp){t.insert_unique(first,last);}

map(const map<Key,T, Compare, Alloc>& x) : t(x.t) {} 
map<Key, T, Compare, Alloc>& operator=(const map<Key,T, Compare, Alloc>&x) 
{
t = x.t;
return *this;
}

// accessors: 
//以下所有的map操作行为,RB-tree都已提供,map只要转调用即可

key_compare key_comp() const{return t.key_comp(); } 
value_compare value_comp() const{return value_compare(t.key_comp());} 
iterator begin() { return t.begin(); } 
const_iterator begin() const{returnt.begin();} 
iterator end() { return t.end();} 
const_iterator end() const { return t.end();} 
reverse_iteratorr begin() { return t.rbegin();} 
const_reverse_iterator rbegin() const{return t.rbegin();} 
reverse_iterator rend() { return t.rend(); } 
const_reverse_iterator rend() canst { return t.rend();} 
bool empty() const{return t.empty();} 
size_type size() const{ return t.size();} 
size_type max_size() const{ return t.max_size();} 
//注意以下  下标(subscript)操作符
T& operator[] (const key_type& k) { 
return (*(insert(value_type(k,T())).first)).second;
}
void swap(map<Key,T,Compare,Alloc>& x){t.swap(x.t);}
//insert/erase
//注意以下insert操作返回的型别
pair<iterator,bool>insert(const value_type& x) {
return t.insert_unique(x);}
iterator insert(iterator positon,const value_type& x){
return t.insert_unique(position,x);
}
template <class InterIterator>
void insert(InputIterator first,InputIterator last){
t.insert_unique(first,last);
}
void erase(iterator position) { t.erase(position); } 
size_type erase(const key_type& x) { return t.erase(x) ; } 
void erase{iterator first, iterator last) {t.erase(first, last); J void clear() { t.clear(); } 

// map operations: 
iterator find(const key_type& x) { return t.find(x); } 
const_iterator find(const key_type& x) const { return t.find(x); } 
size_type count(const key5;_type& x) const{ return t.count(x); } 
iterator lower_bound(const key_type& x) {return t. lower_bound (x) ; } 
const_iterator lower_bound(const key_type& x) const { 
return t.lower_bound(x);
}
iterator upper_bound(const key_type& x) {return t.upper_bound(x);} 
const_iterator upper_bound(const key_type& x) const { 
return t.upper_bound(x);
}
pair<iterator,iterator>equal_range(const key_type& x){
return t.equal_range(x);
}

pair<const_iterator,const_iterator>equal_range(const key_type& x)const
{
return t.equal_range(x);
}
friend bool operator==__STL_NULL_TMPL_ARGS (const map&, const map&); 
friend bool operator<__STL_NULL_TMPL_ARGS (const map&, const map&); 
};
template <class Key,class T,class Compare,class Alloc>
inline bool operator==(const map<Key,T,Compare,Alloc>& x,const map<Key,T,Compare,Alloc>& y){
return x.t == y.t;
}
template <class Key,class T,class Compare,class Alloc>
inline bool operator<(const map<Key,T,Compare,Alloc>& x,const map<Key,T,Compare,Alloc>& y){
return x.t < y.t;
}

测试:

#include<map>
#include<istream>
#include<string>
using namespace std;
int main()
{
map<string,int>simap;//以string为键值,以int为实值
simap[string("jjhou")]=1;//第1对内容是("jjhou", 1)
simap[string("jerry")] =2;//第2对内容是("jerry",2)
simap[string("jason")] =3;//第3对内容是("jason",3)
simap[string("jimmy")] =4;//第4对内容是("jimmy",4) 

pair<string, int>value(string("david"),5); 
simap.insert(value); 

map<string, int>::iterator simap_iter=simap.begin();
 for(; simap_iter!= simap.end(); ++simap_iter) 
cout<<simap_iter->first<<' '<<simap_iter->second<<endl;
										//david 5
										//Jason 3
										//jerry 2
										//jimmy 4
										//jjhou 1
										
int number = simap[string("jjhou")];
cout<<number<<endl;//1
map<string,int>iterator ite1;
//面对关联式容器,应该使用其所提供的find函数来搜寻元素,会比
//使用STL算法find()更有效率。因为STL算法find()只是循序搜寻 
ite1=simap.find(string("mchen")) ; 
if(ite1==simap.end()) 
cout<<"mchen not found"<<endl;//machen not found

ite1 = simap.find(string(jerry"));
if(ite1 !=simp.end())
cout<<"jerry found"<<endl;//jerry found

ite1->second = 9;//可以通过map迭代器修改"value"(not key)
int number2 = simap[string("jerry")];//9
cout<<number2<<endl;
}

针对其中使用的insert()函数及subscript(下标)操作符做一些说明。
首先是insert()函数:
//注意以下insert操作返回的型别:

pair<iterator,bool>insert(const value_type& x)
{return t.insert_unique(x);}
  1. 此式将工作直接转给底层机制RB-tree的insert_unique()去执行,原因不必多说。
  2. 要注意的是其返回值型别是一个pair,由一个迭代器和一个bool值 组成,后者表示插入成功与否,成功的话前者即指向被插入的那个元素。

至于subscript(下标)操作符,用法有两种,可能作为左值运(内容可被修改),也可能作为右值运用(内容不可被修改),例如:

map<string,int>simap;//以string为键值,以int为实值
simap[string("jjhou")] = 1;//左值运用
...
int number = simap[string("jjhou")];//右值引用

左值或右值都适用的关键在于,返回值采用by reference传递形式。

无论如何,subscript操作符的工作,都得先根据键值找出其实值,在做打算。下面是其实际操作:

template<class Key,class T,class Compare = less<Key>,class Alloc  =alloc>
class map{
public:
//typedefs:
typedef Key key_type;//键值型别
typedef pair<const Key,T>value_type;//元素型别(键值/实值)
...
public:
T& operator[](const key_type& k){
return (*((insert(value_type(k,T()))).first)).second;//(A)
}
...
};

上述(A)式比较复杂,首先根据键值和实值做出一个元素,由于实值未知,所以产生一个与实值型别相同的暂时对象替代:

value_type(k,T())

再将该元素插入到map里:

insert(value_type(k T()))
  1. 插入操作返回一个pair, 其第一元素是个迭代器, 指向插入妥当的新元素, 或指向插入失败点(键值重复)的旧元素。
  2. 注意,如果下标操作符作为左值运用(通常表示要填加新元素), 我们正好以此 “实值待填" 的元素将位置卡好;
  3. 如果下 标操作符作为右值运用(通常表示要根据键值取其实值), 此时的插人操作所返回 的pa计的第一元素(是个迭代器)恰指向键值符合的旧元素。

现在我们取插入操作所返回的pair的第一元素:

(insert(value_type(k,T()))).first

这第一元素是个迭代器, 指向被插人的元素。 现在, 提领该迭代器:

*((insert(value_type(k,T()))).first)

获得一个map元素, 是一个由键值和实值组成的pair。 取其第二元素, 即为 实值:

(*((insert(value_type(k, T()))).first)).second;

注意, 这个实值以by reference方式传递, 所以它作为左值或右值都可以。 这便是(A)式的最后形式。

5.multiset

multiset的特性以及用法和set完全相同,唯一的差别在于它允许键值重复,因此它的插人操作采用的是底层机制RB-tree的insert_equal()而非insert_unique ()。下面是multiset的源代码提要,只列出了与set不同之处:

template<class Key, class Compare=less<Key>, class Alloc=alloc> 
class multiset { 
public: 
//typedefs:
...(与set相同)
//allocation/deallocation
//注意,multiset一定使用insert_equal()而不是使用insert_unique()
//set才使用insert_unique()

template<class InputIterator>
multiset<InputIterator, first,InputIterator last):t(Compare()){t.insert_equal(first,last);}
template <class InputIterator>
multieet(InputIterator first, InputIterator last, const Compare& comp):t(comp){t.insert_equal(first,last);}
...(其它与set相同)

//insert/erase
iterator insert(iterator position,const value_type& x){
typedef typename rep_type::iterator rep_iterator;
return t.insert_equal((rep_iterator&)position,x);
}
template<class InputIterator>
void insert(InputIterator first,InputIterator last){
t.insert_equal(first,last);
}
...(其它与set相同)

6.multimap

multimap的特性以及用法与map完全相同,唯一的差别在于它允许键值重复,因此它的插入操作采用的是底层机制RB-tree的insert_equal()而非 insert_unique ()。下面是multimap的源代码提要,只列出了与map不同之处:

template<class Key, class T, class Compare= less<Key>, class Alloc = alloc>
class multimap { 
public: 
//typedefs:
...(与set相同)
//allocation/deallocation
//注意,multimap一定使用insert_equal()而不是使用insert_unique()
//map才使用insert_uniuqe()

template<class InputIterator>
multimap(Inputiterator first, InputIterator last):t(Compare()){t.insert_equal(first,last);}

template <class Inputlterator> 
multimap(InputIterator first, InputIterator last, const Compare& comp) 
: t(comp) { t.insert_equal(first, last); } 
... (其它与map相同)

// insert/erase 
iterator insert(constvalue_type& x) { return t.insert_equal(x); } 
iterator insert(iterator position,const value_type&x ) {
return t.insert_equal(positon,x);
}
template<class InputIterator>
void insert(InputIterator,first,InputIterator last){
t.insert_equal(first,last);
}
...(其它与map相同) 

7.hashtable

  1. 1.1节介绍了二叉搜索树,1.2节介绍了平衡二叉搜索树,第2 节详细地介绍了一种被广泛运用的平衡二叉搜索树:RB-tree (红黑树)。 RB-tree不仅在树形的平衡上表现不错,在效率表现和实现复杂度上也保持相当的“平衡”, 所以运用甚广,也因此成为STL set和map的标准底层机制。
  2. 二叉搜索树具有对数平均时间(logarithmic average time)的表现,但这样的表现构造在一个假设上:输入数据有足够的随机性。这一节要介绍一种名为hash table (散列表)的数据结构,这种结构在插人、删除、搜寻等操作上也具有“常数平均时间”的表现,而且这种表现是以统计为基础,不需仰赖输入元素的随机性。

7.1 hashtable概述

  1. hash table可提供对任何有名项(named item)的存取操作和删除操作。
  2. 由于操作对象是有名项,所以hash table也可被视为一种字典结构(dictionary) 这种结构的用意在于提供常数时间之基本操作,就像stack或queue那样。
  3. 乍听之下这几乎是不可能的任务,因为约束制条件如此之少,而元素个数增加,搜寻操作必定耗费更多时间。

举个例子,如果所有的元素都是16-bits且不带正负号的整数,范围0-65535, 那么简单地运用一个array就可以满足上述期望。

  1. 首先配置一个array A, 拥有 65536个元素,索引号码0-65535,初值全部为0,如下图所示。
  2. 每一个元素值代表 相应元素的出现次数。如果插入元素i,我们就执行A[i]++,如果删除元素i,就执行A[i]–。
  3. 如果搜寻元素i,就检查A[i]是否为0。
  4. 以上的每一个操 作都是常数时间.这种解法的额外负担(overhead)是array的空间和初始化时间。

如果所有的元素都是16-bits且不带正负号的整数, 我们可以 一个拥有 65536个元素的array A, 初值全部为0, 每个元素值代表相应元素的出现次数。于是, 不论是插入、 删除、 搜寻, 每个操作都在常数时间内完成。
在这里插入图片描述
这个解法存在两个现实问题。

  1. 第一 , 如果元素是32-bits而非16-bits, 我们所 准备的array A 的大小就必须是2 ³²= 4GB, 这就大得不切实际了。 第二,如果元素 型态是字符串(或其它)而非整数, 将无法被拿来作为array的索引。
  2. 第二个问题(关于索引)不难解决。就像数值1234是由阿拉伯数字1,2,3,4构成一样,字符串 ,'jjhou" 是由字符 ‘j’,‘j’,‘h’,‘o’,‘u’ 构成。 那么,既然数值1234是1 *10³ +2 * 10² +3 * 10¹+4 *10º , 我们也可以把字符编码, 每个字符 以一个7-bits数值来表示(也就是ASCII编码),从而将字符串 ,'j jhou"表现为:
'j'*128²*² + 'j'*128³ + 'h'*128²+'0'*128¹ + 'u'*128º

于是先前的array实现就可适用于“元素型别为字符串”的情况了。但并不实用,因为,这会产生出非常巨大的数值。“jjhuo”的索引值将是:

106*128²*²+ 106*1283³+ 104*128² + 111*128¹ + 117*128° = 28678174709

这太不切实际了。更长的字符串会导致更大的索引值!这就回归到第一个问题: array的大小。

如何避免使用一个大得荒谬的array呢?

  • 办法之一 就是使用某种映射函数,将大数映射为小数。
    • 负责将某一元素映射为一 个 “大小可接受之索引 ” ,这样的函数称为hash function (散列函数)。
    • 例如,假设x是任意整数,TableSize是array大小,则X%TableSize会得到一个整数,范围在0-TableSize-1之间,恰可作为表格(也就是array)的索引。

使用hash function会带来一个问题:

  1. 可能有不同的元素被映射到相同的位置(亦即有相同的索引)。这无法避免,因为元素个数大于array容量。这便是所谓的"碰撞(collision)"问题。
  2. 解决碰撞间题的方法有许多种,包括线性探测(linear probing)、二次探测(quadraticprobing)、开链(separatechaining)…等做法。
  3. 每一种方法都很容易,导出的效率各不相同一与array的填满程度有很大的关连。

①.线性探测Clinear probing) 。

首先让我们认识一个名词:负载系数(loading factor) , 意指元素个数除以 表格大小。负载系数永远在0-1之间一除非采用开链(separatechaining)策略, 后述。

当hash function计算出某个元素的插入位置,而该位置上的空间已不再可用 时,我们应该怎么办?

  1. 最简单的办法就是循序往下一一寻找(如果到达尾端,就绕到头部继续寻找),直到找到一个可用空间为止。

  2. 只要表格(亦即array)足够大, 总是能够找到一个安身立命的空间,但是要花多少时间就很难说了。

  3. 进行元素搜寻 操作时,道理也相同,如果hash function计算出来的位置上的元素值与我们的搜寻 目标不符,就循序往下一一寻找,直到找到吻合者,或直到遇上空格元素。

  4. 至于元素的删除,必须采用惰性删除(lazy deletion) , 也就是只标记删除记号,实际删 除操作则待表格重新整理(rehashing)时再进行。这是因为hash table中的每一 个元素不仅表述它自己,也关系到其它元素的排列。

例:线性探测,依序插入5个元素,array的变化。
在这里插入图片描述
欲分析线性探测的表现,需要两个假设:
(1)表格足够大;
(2)每个元素都够独立。
在此假设之下,最坏的情况是线性巡访整个表格,平均情况则是巡访一半表格,这已经和我们所期望的常数时间天差地远了,而实际情况犹更糟糕。问题出在上述第二个假设太过于天真。

拿实际例子来说:

  1. 接续上图的最后状态,除非新元素经过hash function的计算之后直接落在位置#4-#7,否则位置#4-#7永远不可能被运用,因为位置 #3永远是第一考虑。
  2. 换句话说,新元素不论是8,9,0,1,2,3中的哪一个,都会落在位置#3上。
  3. 新元素如果是4或5,或6,或7,才会各自落在位置#4,或#5, 或#6,#7上。
  4. 这很清楚地突显了个问题:平均插入成本的成长幅度,远高于负载系数的成长幅度。这样的现象在hashing过程中称为主集团(primary clustering)。
  5. 此时的我们手上有的是一大团已被用过的方格,插入操作极有可能 在主集团所形成的泥汗中奋力爬行,不断解决碰撞问题,最后才射门得分,但是却又助长了主集团的泥汗面积。

②.二次探测(quadratic probing)

  1. 二次探测主要用来解决主集团(primaryclustering)的问题。
  2. 其命名由来是因为解决碰撞问题的方程式F(i) =i²是个二次方程式。
  3. 明确地说,如果hash function计算出新元素的位置为H,而该位置实际上已被使用,那么我们就依序尝试H+1²,H+2² H+3² H+4² … , H+i ², 而不是像线性探测那样依序尝试H+1,H+2, H+3, H+4, … , H+i。

下图是个二次探测实例:依序插入5个元素,array变化。
在这里插入图片描述
二次探测带来一些疑问:

  1. 线性探测法每次探测的都必然是一个不同的位置,二次探测法是否能够保证如此?二次探测法是否能够保证如果表格之中没有x,那么我们插入x一定能成功吗?
  2. 线性探测法的运算过程极其简单,二次探测法则显然复杂得多。这是否会在执行效率上带来太多的负面影响?
  3. 不论线性探测或二次探测,当负载系数过高时,表格是否能够动态成长?

幸运的是,如果我们假设表格大小为质数(prime), 而且永远保持负载系数 在0.5以下(也就是说超过0.5就重新配置并重新整理表格),那么就可以确定每插入一个新元素所需要的探测次数不多于2。

至于实现复杂度的问题,一般总是这样考虑:

  1. 赚的比花的多,才值得去做。
  2. 受累增加了探测次数,所获得的利益好歹总得多过一次函数计算所多花的时间, 不能得不偿失。
  3. 线性探测所需要的是一个加法(加1)、一个测试(看 是否需要绕转回头),以及一个偶需为之的减法(用以绕转回头)。
  4. 二次探测需的则要是一个加法(从i-1到i)、一个乘法(计算i²), 另一个加法,以及一个 mod运算。看起来很有得不偿失之赚。然而这中间却有一些小技巧,可以去除耗 时的乘法和除法:
Hi = Ho+(mod M) 
Hi-1 = Ho+(i-1) ² (mod M) 

整理可得:

Hi-Hi-1 =-(i-1)²(mod M)
Hi = Hi-1 + 2i-1 (mod M) 
  1. 因此,如果我们能够以前一个H值来计算下一个H值,就不需要执行二次方 所需要的乘法了。
  2. 虽然还是需要一个乘法,但那是乘以2,可以位移位(bit shift) 快速完成。至于mod运算,也可证明并非真有需要(本处略)。

最后一个问题是array的成长。

欲扩充表格,首先必须找出下一个新的而且够大(大约两倍)的质数,然后必须考虑表格重建(rehashing)的成本:是的, 不可能只是原封不动地拷贝而已,我们必须检验旧表格中的每一个元素,计算其在 新表格中的位置,然后再插入到新表格中。
二次探测可以消除主集团(primary clustering) , 却可能造成次集团(secondary clustering) : 两个元素经hash function计算出来的位置若相同,则插入时所探测 的位置也相同,形成某种浪费。消除次集团的办法当然也有,例如复式散列(double hashing)。

虽然目前还没有对探测有数学上的分析,不过,以上所有考虑加加减减起来,总体而言,二次探测仍然值得投资。

③.开链(separate chaining)

另一种与二次探测分庭抗礼的,是所谓的开链(separate chaining)法。这种做法是在每一个表格元素中维护 一 个list : hash function 为我们分配某一个list, 然后我们在那个list身上执行元素的插入、搜寻、删除等操作。虽然针对list而进行的搜寻只能是种线性操作,但如果list够短, 速度还是够快。 使用开链手法,表格的负载系数将大于1。SGI STL的hash table 便是采用这种做法。

7.2 hash table 的桶子(buckets)与节点(nodes)

下图是以开链法(separate chaining)完成hash table的图形表述。 为了解说SGI STL源代码,遵循SGI的命名, 称hash table表格内的元素为桶 子(bucket) , 此名称的大约意义是,表格内的每个单元,涵盖的不只是个节点(元 素), 甚且可能是一 "桶 ” 节点。

以开链(separate chaining)法完成的hash table。SGI即采此法。
在这里插入图片描述

下面是hash table的节点定义:

teplate<class Value>
struct  __hashtable_node
{
__hashtable_node* next;
Value val;
};

在这里插入图片描述
注意,backet所维护的linked list,并不采用STL的list或slist,而是自行维护上述的hash table node。至于backets聚合体,则以vector完成,以便有动态扩充能力。

7.3 hashtable的迭代器
以下是hash table迭代器的定义:

template<class Value, class Key, class HashFcn, 
class ExtractKey, class EqualKey, class Alloc> 
struct _hashtable_iterator{ 
typedef hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc> 
hashtable; 
typedef _hashtable_iterator<Value,Key, HashFcn, xtractKey, EqualKey, Alloc>
iterator;
typedef _haashtable_const_iterator<Value,Key,HashFcu,ExtractKey,EqualKey,Alloc>
const_iterator;

typedef _hashtable_node<Value> node;
typedef forward_iterator_tag iterator_category;
typedef Value value_type; 
typedef ptrdiff_t difference_type; 
typedef size_t size_type; 
typedef Value& reference; 
typedef Value* pointer; 

node* cur; //迭代器目前所指之节点
hashtable* ht; //保持对容器的连结关系(因为可能需要从bucket跳到bucket)
_hashtable_iterator(node*n, hashtable* tab) : cur(n), ht(tab) {} 
_hashtable_iterator() {} 
reference operator*() const { return cur->val;} 
pointer operator->() const { return &(operator*()); } 
iterator&operator++(); 
iterator operator++(int);
bool operator==(const iterator& it)const{return cur == it.cur;}
bool operator!=(const iterator& it)const{return cur !=t.cur;}
};
  1. hast table迭代器必须永远维系着与整个"bucket svector"的关系, 并记录目前所指的节点。
  2. 其前进操作是首先尝试从目前所指的节点出发,前进一个 位置(节点),由于节点被安置于list内,所以利用节点的next指针即可轻易达成前进操作。
  3. 如果目前节点正巧是list的尾端,就跳至下一个bucket身上,那 正是指向下一个list的头部节点。
template <Class V, class K, class HF, class ExK, class EqK, class A> 
_hashtable_iterator<V,K, HF, ExK, EqK, A>&
_hashtable_iterator<V,K,HF,ExK,EqK,A>::operator++()
{
const node* old= cur; 
cur= cur->next;//如果存在,就是它。否则进入以下if流程 
if (!cur) { 
//根据元素值,定位出下一个bucket。其起头处就是我们的目的地
size_type bucket= ht->bkt_num(old->val); 
while (!cur && ++bucket< ht->buckets.size()) //注意,operator++
cur = ht->buckets[bucket];
}
return *this;
}

template <Class V, class K, class HF, class ExK, class EqK, class A> 
inline _hashtable_iterator<V, K, HF, ExK, EqK, A> 
_hashtable_iterator<V,K,HF,ExK,A>::operator++(int)
{
iterator tmp = *this;
++*this;//调用operator++()
return tmp;
}

请注意,hash table的迭代器没有后退操作(operator–()) , hash table 也没有定义所谓的逆向迭代器(reverseiterator)。

7.4 hashtable的数据结构

下面是hashtable的定义摘要, 其中可见buckets聚合体以vector完成, 以利动态扩充:

template <class Value, class Key, class HashFcn,class ExtractKey, class Equal Key, class Alloc=alloc>

class hashtable; 
// ... 
template <class Value, class Key, class HashFcn, 
class ExtractKey, class EqualKey, 
class Alloc> //先前声明时, 已给予Alloc默认值alloc 
class hashtable { 
public: 
typedef HashFcn hasher; //为template型别参数重新定义一个名称
typedef EqualKey key_equal;//为template型别参数重新定义一个名称
typedef size_t size_type;

private:
//以下三者都是function objects. <stl_hash_fun.h> 如中定义有数个
//标准型别(如int,c-style string等)的hasher
hasher hash; 
key_equal equals;
ExtractKey get_key; 
typedef _hashtable_node<Value> node; 
typedef simple_alloc<node, Alloc> node_allocator; 
vector<node*,Alloc> buckets; //以vector完成
size_type num_elements;
public: 
// bucket个数即buckets vector的大小
size_type bucket_count() const { return buckets.size(); } 
... 
} ; 

hashtable的模板参数相当多, 包括:

  1. Value: 节点的实值型别。
  2. Key: 节点的键值型别。
  3. HashFcn: hash function的函数型别。
  4. ExtractKey: 从节点中取出键值的方法(函数或仿函数)。
  5. EqualKey: 判断键值相同与否的方法(函数或仿函数)。
  6. Alloc: 空间配置器,缺省使用std::alloc。

如果对STL的运用缺乏深厚的功力,不太容易给出正确的参数。

稍后以一个小小的测试程序告诉大家如何在客户端应用程序中直接使用hashtable。
7.7 节的<stl_hash_fun.h>定义有数个现成的hash functions, 全都是仿函数。

先前谈及的是hash function是计算元素位置(如今应该说是bucket位置)的函数,SGI将这项任务赋予bkt_num(),由它调用hash function取得一个可以执行modulus(取模运算的值)。

稍后执行这项”计算元素位置”的任务时,会再提醒你一次。

虽然开链法(separate chaining)并不要求表格大小必须为质数,但SGI STL仍 然以质数来设计表格大小,并且先将28个质数(逐渐呈现大约两倍的关系)计算好,以备随时访间,同时提供一个函数,用来查询在这28个质数之中,“最接近某数并大于某数”的质数:

//注意:假设long至少有32bits
static const int _stl_num_primes= 28; 
static const unsigned long _stl_prime_list[__stl_num_primes]={
53,97,193,389,769,
1543,3079,6151,12289,24593,
49157,98317,196613,393241,786433,
1572869,3145739,6291469,12582917,25165843,
50331653,100663319,201326611,4022653189,805306457,
1610612741,3221225473u1,429496729lu1
};
//以下找出上述28个质数之中,最接近并大于n的那个质数
inline unsigned long _stl_next_prime(unsigned long n) 
{
const unsigned long* first=__stl_prime_list;
const unsigned long* last=__stl_prime_list + __stl_num_primes; 
const unsigned long* pos = lower_bound(first, last, n); 
//以上,lower_bound()是泛型算法
//使用lower_bound(),序列需先排序。没问题,上述数组已排序
return pos == last ? * (last -1) : *pos; 
}
//总共可以有多少buckets.以下是hast_table
size_type max_bucket_count()const
{return __stl_prime_list[__stl_num_primes -1];}
//其值将为4294967291

7.5 hashtable的构造与内存管理

上一节hashtable定义式中展现了一个专属的节点配置器:

typedef simple_alloc<node, Alloc> node_allocator; 

下面是节点配置函数与节点释放函数:

node* new_node(const value_type& obj) 
{
 node* n =node_alloc::allocate();
 n->next = 0;
 __STL_TRY{
 construct(&n->val,obj);
 return n;
 }
 __STL_UNWIND(node_allocator::deallocate(n));
 }

void delete_node(node* n)
{
destroy(&n->val);
node_allocator::deallocate(n);
}

当我们初始化一个拥有50个节点的hash table如下:

// <value, key, hash-func, extract-key,equal-key, allocator> 
//注意:hash table没有供应default constructor
hashtable<int, int, hash<int>, identity<int>, equal_to<int>, alloc>
iht(50, hash<int>{), equal_to<int>());
cout<<iht.size() << endl; //0
cout<< iht.bucket_count() << endl;//53 STL提供第一个质数

上述定义调用以下构造函数:

hashtable(size_type n,const HashFcn& hf,const EqualKey& eql):hash(hf),equal(eql),get_key(ExtractKey()),num_elements(0)
{
initialize_backets(n);
}
void initialize_buckets(size_type n)
{
const size_type n_buckets = next_size(n);
//举例:传入50,返回53,以下首先保留53个元素空间,然后将其全部填0
buckets.reserve(n_buckets);
buckets.insert(buckets.end(),n_buckets,(node*)0);
num_elements = 0;
}

其中的next_size()返回最接近n并大于n的质数:

size_type next_size(size_type n)const {return __stl_next_prime(n);}

然后为buckets vector保留空间,设定所有buckets的初值为0(或null指针)。

①.插入操作(insert)与表格重整(resize)

当客户端开始插人元素(节点)时:

iht.insert_unique(59); 
iht.insert_unique(63); 
iht.insert_unique(108);

hash table内将会进行以下操作:

//插人元素, 不允许重复
pair<iterator, bool> insert_unique(const value_type& obj)
{
resize(num_elements+1);//判断是否需要重建表格, 如需要就扩充
return insert_uniqe_noresize(obj);
}

//以下函数判断是否需要重建表格。如果不需要立刻回返。 如果需要,就动手…
template <class v, class K, class HF, class Ex, class Eq, class A> 
void hashtable<V, K, HF, Ex, Eq, A>::resize(size_type nurn_elernents_hint)
{
//以下, ”表格重建与否" 的判断原则颇为奇特, 是拿元素个数(把新增元素计入后)和bucket vector的大小来比。 如果前者大于后者,就重建表格
//由此可判知, 每个bucket (list)的最大容量和buckets vector的大小相同
const size_type old_n = buckets.size(); 
if (nurn_elernents_hint > old_n) {//确定真的需要重新配置
const size_type old_n = buckets.size(num_elements_hit);//找出下一个质数
if(n>old_n){
vector<node*,A>tmp(n,(node*) 0);//设立新的buckets
__STL_TRY{
//以下处理每一个旧的bucket
for(size_type bucket = 0;bucket<old_n;++bucket){
node* first = buckets[bucket]; 
//指向节点所对应之串行的起始节点
//以下处理每一个旧bucket所含(串行)的每一个节点
while (first) { //串行还没结束时
//以下找出节点落在哪一个新bucket内
size_type new_bucket=bkt_nurn(first->val,n); 
//以下四个操作颇为微妙
//(1)令旧bucket指向其所对应之串的下一个节点(以便迭代处理)
buckets[bucket] = first->next;
//(2) (3)将当前节点插入到新bucket内,成为其对应串行的第一个节点 
first->next = tmp[new_bucket]; 
tmp[new_bucket] =first;
//(4)回到旧bucket所指的待处理串行,准备处理下一个节点
first= buckets[bucket]; 
}
}
buckets.swap(tmp); // vector::swap。新旧两个buckets对调
//注意,对涸两方如果大小不同,大的会变小,小的会变大
//离开时释放local_tmp的内存
}
}
}
}
//在不需重建表格的情况下插入新节点。键值不允许重复
template <class V, class K, class HF, class Ex, class Eq, class A> 
pair<typename hashtable<V, K, HF, Ex, Eq, A>::iterator,bool> 
hashtable<V, K, HF, Ex, Eq, A >::insert_unique_noresize(const value_type& obj)
{
const size_type n = bkt_num(obj) ; //决定obj应位于#nbucket 
node* first = buckets [n];//令first指向bucket对应之串行头部
//如果buckets[n]已被占用,此时first将不为o,于是进入以下循环, 
//走过bucket所对应的整个链表
for (node* cur= first; cur; cur= cur->next) 
if (equals(get_key(cur->val), get_key(obj)))
//如果发现与链表中的某键值相同,就不插入,立刻返回
return pair<iterator, bool>(iterator(cur,this), false);
//离开以上循环(或根本未进入循环)时,first指向bucket所指链表的头部节点 
node* tmp = new_node (obj); //产生新节点
tmp->next=first; 
buckets[n] = tmp; //令新节点成为链表的第一个节点
++num_elements; //节点个数累加1
return pair<iterator,bool>(iterator(tmp,this), true); 
}

前述的resize()函数中,如有必要,就得做表格重建工作。操作分解如下, 并示下图中。

// (1) l令旧bucket指向其所对应之链表的下一节点(以迭代处理)
buckets[bucket] = firs->next;
//(2)(3)将当前节点插入到新bucket内,成为其对应链表的第一个节点
first->next = tmp[new_bucket];
tmp[new_bucket] =first;
// (4)回到旧bucket所指的待处理链表,准备处理下一个节点
first = buckets[buckte];

图表格重建操作分解。本图所表现的是hashtable::resize ()内的行为,图左的buckets[ ]是旧有的buckets,图右的tmp[]是新建的buckets。最后会将新旧两个buckets对调,使buckets[ ]有了新风貌而tmp[]还诸系统。
在这里插入图片描述
如果客户端执行的是另一种节点插入行为(不再是insert_unique,而是insert_equal):

iht.insert_equal(59);
iht.insert_equal(59);

进行的操作如下:

//插入元素, 允许重复
iterator ineert_equal(const value_type& obj) 
{
resize(num_elements+1);//判断是否需要重建表格,如需要就扩充
return insert_equal_noresize(obj);
}
//在不需重建表格的情况下插入新节点。键值允许重复
template <class V, class K, class HF, class Ex, class Eq, class A> 
typename hashtable<V, K, HF, Ex, Eq, A>::iterator 
hashtable<V, K, HF, Ex, Eq, A>::insert_equal_noresize(const value_type& obj)
{
const size_type n = bkt_num(obj); //决定obj应位于#n bucket
 node* first = buckets[n]; //令first指向bucket对应之链表头部
//如果buckets[n]已被占用, 此时first将不为0, 于是进入以下循环,
 //走过bucket所对应的整个链表
for (node* cur = first; cur; cur = cur->next)
if(equals(get_key(cur->val),get_key(obj))){
//如果发现与链表中的某键值相同, 就马上插入, 然后返回
node* tmp = new_node(obj); //产生新节点
tmp->next = cur->next; //将新节点插人于目前位置
 cur->next = tmp; 
++num_elements; //节点个数累加1
}
//进行至此, 表示没有发现重复的键值 
node* tmp = new_node(obj); //产生新节点
tmp->next = first; //将新节点插入于链表头部
buckets(n] = tmp; 
++num_elements; //节点个数累加1
return iterator (tmp, this); //返回一个迭代器,指向新增节点
}

②.判知元素的落脚处(bkt_num)

本节程序代码在许多地方都需要知道某个元素值落脚于哪一个buckt之内。
这本来是hash function的责任,SGI把这个任务包装了一层,先交给bkt_num()函数,再由此函数调用hash function,取得一个可以执行modulus(取模)运算的数值。为什么要这么做?因为有些元素型别无法直接拿来对hashtable的大小进行模运算,例如字符字符串const char*,这时需要做一些转换。下面是bkt_num()函数,7.7 列有SGI内建的所有hash functions。

//版本1:接受实值(value)和buckets个数
size_type bkt_num(const value_type& obj, size_t n) const
{
return bkt_num_key(get_key (obj), n);//调用版本4
}
//版本2:只接受实值(value)
size_type bkt_num(const value_type& obj) const
{
return bkt_num_key(get_key(obj);//调用版本3
}
//版本3:只接受键值
size_type bkt_num_key(const key_type& key) const
{
return bkt_num_key(key,buckets.size());//调用版本4
}
//版本4: 接受键值和buckets个数
size_type bkt_num_key(const key_type& key, size_t n) const
{
return hash(key) % n;//SGI的所有内建的hash()列于7.7节
}

③.复制(copy_from)和整体删除(clear)

由于整个hash table由vector和linked-list组合而成,因此,复制和整体
删除,都需要特别注意内存的释放问题。下面是hash table提供的两个相关函数:

template <Class V, class K, class HF, class Ex, class Eq, class A> void hashtable<V, K, HF, Ex, Eq, A>: :clear() 
{
//针对每一个bucket
for(size_type i =0;i<buckets.size();++i){
node* cur = bucket[i];
//将bucket_list中每一个节点删除
while(cur !=0){
node* nexr=cur->next;
delete_node(cur); 
cur = next;
}
buckets[i] =0;//令bucket内容为null指针
}
num_elements = 0;//令总节点个数为0
//注意buckets vector并未释放掉空间,仍保有原来大小
}
ternplate <class v, class K, class HF, class Ex, class Eq, class A>
void hashtable<V,K,HF,Ex,Eq,A>::copy_from(const hashtable& ht){
//先清除己方的bucketsvector. 这操作是调用vector::clear. 造成所有元素为0
buckets.clear();
//为己方的buckets vector保留空间, 使与对方相同
//如果己方空间大于对方,就不动,如果已方空间小于对方,就会增大
buckets.reserve(ht.buckets.size());
//从己方的buckets vector尾端开始,插入n个元素,其值为null指针
//注意,此时buckets vector为空,所以所谓尾端,就是起头处
buckets.insert(buckets.end(), ht.buckets.size(),(node*) 0); 
__STL_TRY{
//针对buckets vector
for(size_type i =0;i<ht.buckets.size();++i){
//复制vector的每一个元素(是指针,指向hash table节点)
 if (const node* cur=ht.buckets[i]) { 
 node* copy= new_node(cur->val); 
 buckets[i] = copy; 
//针对同一个bucket list,复制每一个节点
for (node* next=cur->next; next;cur= next, nex七=cur->next) {
copy->next = new_node(next->val);
copy = copy->next;
}
}
}
num_elements = ht.num_elements;//重新登录节点个数(hashtable的大小)
}
__STL_UNWIND(clear());
}

7.6 hashtable运用实例

先前说明hash table的实现源代码时,已经零零散散地示范客户端程序,下面是一个完整的实例:


//注意:客户端程序不能直接含入<stl_hashtable.h>,应该含入有用到hashtable
 //的容器头文件,例如<hash_set.h>或<hash_map.h>
#include <hash_set>// for hashtable
#include <iostream> 
using namespace std; 
int main()
{
//hash-table
// <value, key, hash-func, extract-key,equal-key, allocator> 
// note: hash-table has no default ctor 
hashtable<int, int,hash<int>,identity<int>,equal_to<int>,alloc>
iht<50,hash<int>(),equal_to<int>());//指定保留50个buckets

cout<<iht.size()<<endl;//0
cout<<iht.bucket_count()<<endl;//53. 这是STL供应的第一个原数
cout<< iht.max_bucket_count()<<endl;// 429496729,这是STL供应的最后一个质数

iht.insert_unique(59);
iht.insert_unique(63); 
iht.insert_unique(108); 
iht.insert_unique(2); 
iht.insert_unique(53) ; 
iht.insert_unique(55); 
cout<<iht.size() << endl; //6. 此即hashtable<T>::num_elements
//以下声明一个hashtable迭代器 
hashtable<int,int,hash<int>,identity<int>,equal_to<int>,alloc>::iterator ite = iht.begin();

//以下迭代器遍历hashtable,将所有节点的值打印出来
for(int i=0;i<iht.size();++i,++ite)
cout<<*ite<<' ';//53 55 2 108 59 63
cout<<endl;

//遍历所有buckets,如果其节点个数不为0,就打印出节点个数
 for{int i=O; i< iht.bucket_count(); ++i){
 int n=iht.elems_in_bucket(i);
 if(n !=0)
 cout<<"bucket["<<i<<"]has<<n<<"elem."<<endl;
 }
// bucket[OJ has 1 elems.
 // bucket[2] has 3 elems. 
 // bucket[6] has 1 elems.
  // bucket[lO] has 1 elems.

//为了验证"bucket(list)的容量就是buckets vector的大小"(这是从
// hashtable<T>::resize ()得知的结果),我刻意将元素加到54个,
//看看是否发生”表格重建(re-hashing)" 
for(int i=O; i<=47; i++) 
iht.insert_equal(i);
cout<< iht.size() << endl; // 54. 元素(节点)个数 
cout<< iht.bucket_count() << endl; // 97. buckets个数
//遍历所有buckets,如果其节点个数不为0,就打印出节点个数
for(int i=O; i< iht.bucket_count();++i){
int n = iht.elems_in_bucket(i);
if( n!=0)
cout<<"bucket["<<i<<"]has<<n<<"elem."<<endl;
}
//打印结果:bucket[2]和bucket[ll]的节点个数为2,
//其余的bucket[O]~bucket[47]的节点个数均为1
//此外,bucket[53],[55], [59], [63]的节点个数均为1
//以迭代器遍历hash table, 将所有节点的值打印出来
ite = iht.hegin(); 
for(int i=O; i< iht.size(); ++i, ++ite) 
cout << *ite << '';  
cout << endl; 
// O 1 2 2 3 4 5 6 7 8 9 10 11 108 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 44 45 46 47 53 55 59 63 
cout << *(iht.find(2)) << endl; // 2 
cout << iht.count(2) << endl; // 2 
}

这个程序详细测试出hash table的节点排列状态与表格重整结果。一开始我保留50个节点,由于最接近的STL质数为53,所以buckets vector保留的是53个buckets,每个buckets(指针,指向一个hash table节点)的初始值为0.接下来,循序加入6个元素:53,55,2,108,59,63,于是hash table变成如下图所示:

在这里插入图片描述
接下来,我再插入48个元素,使总元素达到54个,超过当时的bucket svector的大小,符合表格重建条件(这是从hashtable::resize ()函数中得知的),于是hash table变成了下图所示的模样:hash table重整结果。注意,bucket#2和bucket#11的节点个数都是2,其余的灰色buckets,节点个数都是1。白色buckets表示节点个数为0。灰 色和白色只是为了方便表示,因为如果把节点全画出来,画面过挤摆不下。图中的 bucket#47和bucket#48应该连续,也是因为画面的关系,分为两段显示。

在这里插入图片描述
程序最后分别使用了hash table提供的find和count函数,搜寻键值为2的元素,以及计算键值为2的元素个数。请注意,键值相同的元素,一定落在同一个bucket list之中。下面是find()和count()的源代码:

iterator find(const key_type& key)
{
size_type n = bkt_num_key(kry);//首先选找落在哪一个bucket内
node* first;
//以下,从bucket list的头开始,一一比对每个元素的键值。比对成功就跳出 
for (first= buckets[n]; 
first && !equals(get_key(first->val), key); 
first=first->next)
{}
return iterator(first,this);
}

size_type count(const key_type& key) const
{
const size_type n = bkt_num_key(key); //首先寻找落在哪一个bucket内
size_type result= O; 
//以下,从bucket lsit的头开始,一一比对每个元素的键值。比对成功就累计1.
for(const node* cur= buckets[n];cur;cur = cur->next)
if(equals(get_key(cur->val),key))
++result;
return result;
}

7.7 hash function

  1. <stl_hash_fun.h>定义有数个现成的hash functions, 全都是仿函数。
  2. 先前谈及概念时,我们说hash function是计算元素位置的函数,SGI将这项任务赋予了先前提过的bkt_num(),再由它来调用这里提供的hash function , 取得 一个可以对hash table进行模运算的值。
  3. 针对char,int, long等整数型别, 这里大部分的hash functions什么也没做,只是忠实返回原值。但对于字符字符串(const char*),就设计了一个转换函数如下:
//以下定义于<stl_hash_fun.h>
template<class Key> struct hash{ }; 
inline size_t _stl_hash_string(const char*s) 
{	
	unsigned long h =0;
	for( ;*s;++s)
	h = 5*h+*s;
	return size_t(h);
}
//以下所有的_STL_TEMPLATE_NULL,在<stl_config.h>中皆被定义为 template<>
__STL_TEMPLATE_NULL struct hash<char*> 
{
size_t operator()(const char* s) const{ return __stl_hash_string(s);}
};

__STL_TEMPLATE_NULL struct hash<const char*>
{
size_t operator()(const char* s)const{return __stl_hash_string(s);}
} ;

__STL_TEMPLATE_NULL struct hash<char>{
size_t operator()(char x) const { return x; } 
};

__STL_TEMPLATE_NULL struct hash<unsigned char> { 
size_t operator() (unsigned char x) const (return x; }
};

__STL_TEMPLATE_NULL struct hash<signed char> {
size_t operator() (unsigned char x) const{return x; }
};

__STL_TEMPLATE _NULL struct hash<short> { 
size_t operator()(short x) const{return x;}
};
__STL_TEMPLATE_NULL struct hash<unsigned short> {
size_t operator()(unsigned short x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<int>{
size_t operator()(int x) const { return x; } 
};
__STL_TEMPLATE_NULL struct hash<unsigned int> { 
size_t operator() (unsigned int x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<long>{
size_t operator () (long x) const (return x; }
};

__STL_TEMPLATE_NULL struct hash<unsigned long> { 
size_t operator() (unsigned long x) const { return x; }
};

由此观之,SGI hashtable无法处理上述所列各项型别以外的元素,例如 string, double, float。欲处理这些型别,用户必须自行为它们定义hash function。 下面是直接以SGI hashtable处理string所获得的错误现象:

#include <hash_set> // for hashtable and hash_set
#include <iostream> 
#include <string> 
using namespace std; 
int main() 
{
 // hash-table 
// <value, key, hash-func, extract-key,equal-key, allocator>
// note: hashtable has no default ctor 
hashtable<string, string, hash<string>, identity<string>, equal_to<string>,alloc>
iht(50,hash<string>(),equal_to<string>());

cout<<iht.size() << endl; //0
cout<< iht.bucket_count() << endl;//53 
iht.insert_unique(string("jjhou"));//error

// hashtable无法处理的型别,hash_set当然也无法处理
hash_set<string> shs; 
hash_set<double> dhs; 

shs.insert(string("jjhou"));// error 
dhs.insert(15.0);//error
}

8.hash_set

  1. 虽然STL只规范复杂度与接口,并不规范实现方法,但STL set多半以 RB-tree为底层机制。
  2. SGI则是在STL标准规格之外另又提供了一个所谓的 hash_set, 以hashtable为底层机制。
  3. 由于hash_set所供应的操作接口, hashtable都提供了,所以几乎所有的hash_set操作行为,都只是转调用 hashtable的操作行为而已。
  4. 运用set, 为的是能够快速搜寻元素。 这一点, 不论其底层是RB-tree 或是hashtable , 都可以达成任务。但是请注意,RB-tree有自动排序功能而hashtable 没有, 反应出来的结果就是, set的元素有自动排序功能而hash_set没有。
  5. set的元素不像map那样可以同时拥有实值(value)和键值(key) , set元 素的键值就是实值, 实值就是键值。 这一点在hash_set中也是一样的。
  6. hash_set的使用方式,与set完全相同。

下面是hash_set的源代码摘录, 其中的注释几乎说明了 一 切, 本节不再另 做文字解释。
请注意,7.5节最后谈到,hashtable有 一些无法处理的型别(除非用户为 那些型别撰写hash function)。 凡是hashtable无法处理者,hash_set也无法 处理。

template <class Value,class HashFcn = hash<Value>, class EqualKey = equal_to<Value>,class Alloc = alloc>
class hash_set{
private:
//以下使用的过entity<>定义于<stl_funct:ion.h>中
typedef hashtable<Value, Value, HashFcn, identity<Value>, 
EqualKey, Alloc> ht; 
ht rep;//底层机制以hash table完成

public:
typedef typename ht::key_type key_type; 
typedef typename ht::value_typevalue_type; 
typedef typename ht::hasher hasher; 
typedef typename ht:: key _equal key _equal; 

typedef typename ht::size_type size_type; 
typedef typename ht::difference_type difference_type; 
typedef typename ht::const_pointer pointer;
typedef typename ht::const_pointer const_pointer; 
typedef typename ht::const_reference reference; 
typedef typename ht::const_reference const_reference;
typedef typename ht::const_iterator iterator;
typedef typename ht::const_iterator const_iterator;

hasher hash funct() const { return rep.hash_funct(); }
key_equal key_eq() const { return rep.key_eq(); } 
public:
//缺省使用大小为100的表格。将被hash table调整为最接近且较大之质数
hash_set() : rep(lOO, hasher(), key_equal()) {} 
explicit hash_set(size_type n):rep(n, hasher(), key_equal()) {} 
hash_set(size_type n, const hasher& hf) : rep(n, hf, key_equal()){} hash_set(size_type n, const hasher& hf, const keyequal& eql):rep(n,hf,eql){}
//以下,插人操作全部使用insert_unique(),不允许键值重复
template<class InpuyIterator>
hash set (InputIterator f, InputIterator l) : rep(lOO, hasher(), key_equal()) { rep.insert_unique(f, l);}

template <class Inputiterator> 
hash_set(InputIterator f, Inputlterator l, size_typen) : rep(n, hasher(), key_equal()) { rep.insert_unique(f, l) ; }

template <class Inputiterator> 
hash_set(Inputiterator f, Inputlterator l, size_type n, 
canst hasher& hf) : rep(n, hf, key_equal()) { rep.insert_unique(f, l);}

template <class Inputiterator> 
hash_set (Inputiterator f, Inputlterator l, size_type n, const hasher& hf,const key_equal& eql):rep(n,hf,eql){rep.insert_unique(f,l);}

public:
//所有操作几乎都有hash table对应版本。传递调用就行
size_type size() const{return rep.size();} 
size type max_size() const{return rep.max_size();}
bool empty()const { return rep.empty(); } 

void swap(hash_set& hs) (rep.swap(hs.rep); } 
friend bool operator== _STL_NULL_TMPL_ARGS (const hash_set&,const hash_Set&);
iterator begin() const { return rep.begin(); } 
iterator end() const { return rep.end(); } 
public:
pair<iterator,bool>insert(const value_type& obj)
{
pair<typename ht::iterator,bool> p = rep.insert_unique(obj);
return pair<iterator,bool>(p.first,p.second);
}
template <class InputIterator>
void insert(Inputiterator f, InputIterator l) { rep. insert_unique (f, 1) ; }
pair<iterator,bool> insert_nores1ze(const value_type& obj) 
{
pair<typename ht::iterator,bool> p = p.insert_unique_noresize(obj); return pair<iterator, bool>(p.first, p.second); 
}
iterator find(const key_type& key) const{return rep.find(key); } 
size_type count(const key_type& key) const{return rep.count (key);}
pair<iterator, iterator> equal_range(const key_type& key) const {return rep.equal_range(key);}

size_type erase(const key_type& key) {return rep.erase(key); } 
void erase(iteratorit) { rep.erase(it);}
void erase(iterator f, iterator l) { rep.erase(f, l); }
void clear() { rep.clear(); } 
public:
void resize(size_type hint) {rep.resize(hint); } 
size_type bucket count() const{return rep.bucket_count();}
size_type max_bucket_count() const{return rep.max_bucket_count();} size_type elems_in_bucket(size_type n) const {return rep.elems_in_bucket(n);}
};
template<class Value, class HashFcn, class EqualKey, class Alloc>
inline bool operator==(const hash_set<Value,HashFcn, EqualKey, Alloc>& hs1,const hash_set<Value,HashFcn,EqualKey,Alloc>& hs2)
{
return hs1.rep == hs2.rep;
}

下面这个例子,在程序最后新加一段遍历操作,为的是验证hash_set内的元素并无任何特定排序,但是这样的安排不尽合理。程序中对于hash_set的EqualKey必须有特别的设计,不能沿用缺省的equal_to,因为此例之中的元素是C字符串(C style charactors string) , 而C字符串的相等与否,必须一个字符一个字符地比较(可使 用C标准函数strcmp()), 不能直接以const char*做比较。

#include <iotream>
#include <hash_set> 
#include <cstring> 
using namespace std; 
struct eqstr
{
bool operator()(const char* s1,const char* s2)const
{
return strcmp(s1,s2)==0;
}
};
void lookup(const hash_set<const char*,hash<const char*>, eqstr>& Set,const char* word)
{
hash_set<const char*, hash<const char*>, eqstr>::const_iterator
it= Set.find(word);
cout << "• << word << ": " << (it != Set.end () ? "present" : "not present") << endl; 
}

int main()
{
hash_set<const char*, hash<const char*>, eqstr> Set;
Set.insert("kiwi"); 
Set.insert ("plum"); 
Set.insert ("apple"); 
Set.insert ("mango"); 
Set.insert ("apricot");
Set. insert ("banana") ; 
lookup (Set, "mango") ;//mango: present
lookup(Set, "apple"); //apple: present
lookup(Set,"durian");//durian: not present
 hash_set<const char*, hash<const char*>, eqstr>::iterator 
ite1 = Set.begin();
hash_set<const char*, hash<const char*>, eqstr>::iterator 
ite2= Set.end();
for(; ite1 != ite2; ++ite1)
cout<<*ite1<<' ';// banana plum mango apple kiwi apricot
}

最后执行结果虽然显示hash_set内的字符串并没有排序, 但这其实不是一个良好的测试, 因为即使有排序, 也是以元素型别const char* 为排序对象, 而非对const char*所代表的字符串进行排序。 要测试hash_set是否排序, 最好 是以int作为元素型别, 如下:

hash_set<int> Set; 
Set. insert (59) ; 
Set.insert(63); 
Set.insert(108);
Set.insert(2);
Set.insert(53) ; 
Set.insert(55); 
hash_set<int>::iterator ite1 = Set.begin(); 
hash_set<int>::iterator ite2 = Set.end();
for(; ite1 != ite2; ++ite1) 
cout<<*ite1<<' ';//2 53 55 59 63 108
cout<<endl;
  • 为什么也有排序呢? hash_set 的底层不就是一 个hashtable 吗?先前7.6节的例子也是以相同的次序将相同的6个整数插入到hashtable内,获得的结果为什么和这里不同?
    • 这是一个令人迷惑的问题,7.6节的hashtable大小被指定为 50 (根据SGI的设计,采用质数53) , 而这里所使用的hash_set 缺省情况下指定 hashtable的大小为100 (根据SGI的设计,采用质数193) , 由于buckets够多, 才造成排序假象。 如果以下面这样的次序输入这些数值:
hash_set<int> Set;//底层hashtable缺省大小为100
Set.insert (3) ;//实际大小为193
Set.insert(196); 
Set.insert(1); 
Set .insert(389); 
Set.insert(194);
Set.insert(3 87) ; 
hash_set<int>::iterator ite1 =Set.begin(); 
hash_set<int>::iterator ite2=Set.end(); 
for(lite1 !=ite2;++ite1)
cout<<*ite1<<' ';//387 194 1 389 196 3

就呈现出未排序的状态了。此时底层的hashtable构造如下:
在这里插入图片描述

9.hash_map

  1. SGI在STL标准规格之外,另提供了一个所谓的hash_map,以hashtable 为底层机制。
  2. 由于hash_map所供应的操作接口,hashtable都提供了,所以几乎所有的hash_map操作行为,都只是转调用hashtable的操作行为而已。
  3. 运用map,为的是能够根据键值快速搜寻元素。这一点,不论其底层是RB-tree 或是hashtable,都可以达成任务。但是请注意,RB-tree有自动排序功能而 hashtable没有,反应出来的结果就是,map的元素有自动排序功能而hash_map 没有。
  4. map的特性是,每一个元素都同时拥有一个实值(value)和一个键值(key)。这一点在hash_map中也是一样的。hash_map的使用方式,和map完全相同。

下面是hash_map的源代码摘录,请注意,7.5节最后谈到,hashtable有一些无法处理的型别(除非用户为 那些型别撰写hash function)。凡是hashtable无法处理者,hash_map也无法 处理。

//以下的hash<>是个function object, 定义于<stl_hash_fun.h>中
//例:hash<int>::operator()(int x) const { return x; }
 template <class Key, class T, class HashFcn = hash<Key>, class EqualKey = equal_to<Key>, class Alloc=alloc> 
class hash_map{
private:
 //以下使用的selectlst<>定义于<stl_function.h>中
 typedef hashtable<pair<const Key,T>,Key,HashFcn,selectlst<pair<const Key, T> >, EqualKey, Alloc> ht;
ht rep;//底层机制以hash table完成
public:

typedef typename ht::key_type key_type;
typedef T data_type;
typedef T mapped_typd;
typedef typename ht::value_type value_type;
typedef typename ht::hasher hasher;
typedef typename ht::key_equal key_equal;

typedef typename ht::size_type size_type; 
typedef typename ht::difference_type difference_type; 
typedef typename ht::pointer pointer;
typedef typename ht::const__pointer const__pointer; 
typedef typename ht::reference reference; 
typedef typename ht::const_reference const_reference; 
typedef typename ht::iterator iterator;
typedeftypename ht::const_iterator const_iterator; 
hasher hash_funct() const { return rep.hash_funct(); }
 key_equal key_eq() const{return rep.key_eq(); } 
public:
//缺省使用大小为100的表格。将由hash table调整为最接近且较大之质数
hash_map():rep(lOO, hasher(), key_equal()) {} 
explicit hash_map(size_type n) : rep(n, hasher(), key_equal()) {} 
hash_map(size_type n, const hasher& hf):rep(n, hf, key_equal()) {} 
hash_map(size_type n, canst hasher& hf, const key_equal& eql) :rep(n,hf,eql){}
//以下,插人操作全部使用insert_unique() , 不允许键值重复
template <class Inputiterator>
hash_map (InputIterator f, InputIteratar l) : rep(lOO, hasher(), key_equal()) { rep.insert_unique(f, l); } 
template <class Inputiterator> 
hash_map(InputIterator f, Inputiterator l, size_type n) : rep(n, hasher(), key_equal()) { rep.insert_unique(f, l) ; } 
template<class Inputiterator>
hash_map (InpuIterator f, InputIterator l, size_type n, 
const hasher& hf) : rep(n, hf, key_equal()) { rep.insert_unique(f, l); } 
template <class InputIterator>
hash_map (Input Iterator f, InputIterator l, size_type n, const hasher& hf const key_equal& eql):rep(n,hf,eql){rep.insert_unique(f,l);}

public:
//所有操作几乎都有hash table对应版本。传递调用就行
size_type size() const { return rep.size(); } 
size_type max_size() const{return rep.max_size(); } 
bool empty () const { return rep. empty () ; } 
void swap(hash_map& hs) { rep.swap(hs.rep); } 
friend bool operator==__STL_NULL_TMPL_ARGS (const hash_map&, const hash_map&); 
iterator begin() { return rep.begin();} 
iterator end() { return rep.end();} 
const_iterator begin() const{return rep.begin() } 
const_iterator end() const{return rep.end(); } 

public:
pair<iterator, bool> insert(const value_type& obj) { return rep.insert_unique(obj); } 
template <class Inputiterator> 
void insert (InputIterator f, InputIterator l) (rep. insert_unique (f, 1l);} 
pair<iterator,bool> insert noresize(const value_type& obj) {return rep.insert_unique_noresize(obj);}
iterator find(const key_type&key) { return rep.find(key); } 
const_iterator find(const key_type& key) const { return rep. find(key); } 
T& operator[] (const key_type& key) {
return rep.find_or_insert(value_type(key,T())).second; 
}
size_type count(const key_type& key) const { return rep.count(key); }
pair<iterator, iterator>equal_range(const key_type& key) 
{ return rep.equal_range(key); } 
pair<const_iterator,const_iterator> equal_range(const key_type& key) const
{return rep.equal_range(key);}
size_type erase(const key_type&key) {return rep.erase(key); } 
void erase(iterator it) {rep.erase(it);} 
void erase(iterator f, iterator l) { rep.erase(f, l) ;} 
void clear() { rep.clear(); }
public:
void resize(size_type hint){rep.resize(hint);} 
size_type bucket count() const{return rep.bucket_count (),} 
size_type max_bucket_count() const { return rep.max_bucket_count();} 
size_type elems_in_bucket(size_type n) const 
{const rep.elems_in_bucket(n);}
};
template <class Key, class T, class HashFcn, class EqualKey, class Alloc> inline bool operator== (const hash_map<Key, T, HashFcn, EqualKey, Alloc>& hm1, const hash_map<Key, T, HashFcn, Equal Key, Alloc>& hm2)
{
return hm1.rep == hm2.rep;
}

下面这个例子,在程序最后新加了一段遍历操作,为的是验证hash_map内的元素并无任何特定排序。程序中对于hash_map的EqualKey必须有特别的设计,不能沿用缺省的equal_to,因为此例之中的元素是字符串,而字符串的相等与否,必须个字符个字符地比较(可使用C标准函数strcmp()) , 不能直接以const cha*做比较。

#include <iostream> 
#include <hash_map> 
#include <cstring> 
using namespace std; 
struct eqstr 
{
bool operator()(const char* s1,const char* s2)const{
return strcmp(s1,s2) == 0;
}
};
int main()
{
hash_map<const char*, int, hash<const char*>, eqstr> days; 
days ["january"] = 31; 
days["february"] = 28; 
days["march"] = 31; 
days["april"] = 30; 
days["may"] = 31; 
days["june"] = 30; 
days["july"] = 30; 
days["august"] = 30; 
days["september"] = 30; 
days["october"] = 30; 
days["november"] = 30; 
days["december"] = 30; 

cout<<"september->"<<days["september"]<<endl;//30
cout<<"june->"<<days["june"]<<endl;//30
cout<<"february->"<<days["february"]<<endl;//28
cout<<"december->"<<days["december"]<<endl;//31

hash_map(const char* ,int,hash<const char*>,eqstr>::iterator ite1=days.begin();
hash_map(const char* ,int,hash<const char*>,eqstr>::iterator ite2=days.end();
for(;ite1 !=ite2;++ite1)
cout<<ite1->first<<' ';
// september june july may january february december march 
// april november october august 
}

10.hash_multiset

  1. hash_multiset 的特性与multiset完全相同,唯一的差别在于它的底层机制是hashtable。也因此,hash_multiset的元素并不会被自动排序。
  2. hash_multiset和hash_set实现上的唯 一差别在于,前者的元素插入操作采用底层机制hashtable的insert_equal(), 后者则是采用insert_unique()。

下面是hash_multiset的源代码摘要7.5节最后谈到,hashtable有一些无法处理的型别(除非用户为那些型别撰写hash function)。凡是hashtable无法处理者,hash_multiset也无法处理。

tempalte<class Value,class hashFcn = hash<Value> clss EqualKey = equal_to<Value>,class Alloc = alloc>
class hash_multiset
{
private:
typedef hashtable<Value,Value,HashFcn,identity<Value>,EqualKey,Alloc> ht;
ht rep;
public:
typedef typename ht::key_type key_type;
typedef typename  ht::value_type value_type;
typedef typename ht::hasher hasher;
typedef typename ht::key_equal key_equal;

typedef typename ht::size_type size_type; 
typedef typename ht::difference_type difference_type; 
typedef typename ht::const_pointer pointer; 
typedef typename ht::const_pointer const_pointer; 
typedef typename ht::const_reference reference; 
typedef typename ht::canst_reference const_reference;
typedef typenarne ht::const_iterator iterator;
typedef typename ht::const_iterator const_iterator;

hasher hash_funct()const { return rep.hash_funct(); }
key_equal key_eq()const{return rep.key_eq();}

public:
//缺省使用大小为100的表格。 将被hash table调整为最接近且较大之质数
hash_multiset():rep(100,hasner(),key_equal()){}
explicit hash_multiset(size_type n):rep(n,hasher(),key_equal()){}

hash_multiset(size_type n,const hasher& hf) : rep(n, hf, key_equal()) {}
hash_multiset(size_type n,const hasher& hf,const key_equal& eql):rep(n,hf,eql){}
//以下, 插入操作全部使用insert_equal(), 允许键值重复
template <class InputIterator>
hash_multiset{InputIterator f, InputIterator l): rep(lOO, hasher(), key_equal()) { rep.insert_equal (f, l);}  
template <class InputIterator>
hash_multist(InputIterator f,Inputerator l,size_type n):rep(n,hasher(),key_equal()){rep.insert_equal(f,l);}
template<class InputIterator>
hash_multiset(InputIterator f, InputIterator l,size_type n,const hasher& hf):rep(n,hf,key_equal()){rep.insert_equal(f,l);}
template<class InputIterator>
hash_multiset(InputIterator f,Inputerator l,size_type n,const hasher& hf,const key_equal& eql):rep(n,hf,eql){rep.insert_equal(f,l);}

public:
//所有操作几乎都有hash table的对应版本,传递调用即可
size_type size()const{return rep.size(); } 
size_type max_size()const{return rep.max_size();}
bool empty() const {return rep.empty();}
void Swap(hash_multiset& hs){return rep.swap(hs.rep);}
friend bool operator==__STL_NULL_TMPL_ARGS(const hash_multiset&,const hash_multiset&);
iterator begin () const{return rep.begin(); }
iterator end() const{ return rep.end(); } 

public:
iterator insert(const value_type& obj) { return rep.insert_equal(obj);}
template <class InputIterator>
void insert(InputIterator f, InputIterator l) { rep.insert_equal(f,l);} 
iterator insert_noresize(const value_type& obj) 
{return rep.insert_equal_noresize(obj);}

iterator find(const key_type& key) const{
return rep.find(key); }
size_type count(const key_type& key) const{
return rep.count(key);}

pair<iterator, iterator> equal_range(const key_type& key) const
{return rep_equal_range(key);}

size_type erase(const key_type& key) {return rep.erase(key); }
void erase(iterator it) { rep.erase(it);}
void erase(iterator f, iterator l) { rep.erase(f, l); } 
void clear() { rep.clear(); } 

public:
void resize(size_type hint) { rep.resize(hint);} 
size_type bucket_count() const { return rep.bucket_count();} 
size_type max_bucket_count() const{ return rep.max_bucket_count();} 
size_type elems_in_bucket(size_type n)const {return rep.elems_in_bucket(n);}
};

template <class Val, class HashFcn, class EqualKey, class Alloc> 
inline bool operator==(const hash_multiset<Val, HashFcn, EqualKey, Alloc>& hsl,const hash_multiset<Val,HashFcn, EqualKey, Alloc>& hs2)
{
return hs1.rep == hs2.rep;
}

hash_multiset的使用方式,与hash_set完全相同。

11.hash_multimap
  1. hash_multimap的特性与multimap完全相同,唯一的差别在于它的底层机制是hashtable。也因此,hash_multimap的元素并不会被自动排序。
  2. hash_multimap和hash_map实现上唯一差别在于,前者的元素插入作用底层机制hashtable的insert_equal(),后者则采用insert_unique()。

下面是hash_multimap的源代码摘要,请注意,7.5节最后谈到,hashtable有一些无法处理的型别(除非用户为那些型别撰写hash function)。凡是hashtable无法处理者,hash_multimap也 无法处理。

ternplate<class Key,class T, class HashFcn = hash<Key>, class EqualKey = equal_to<Key>, class A.lloc = alloc> 
class hash_multimap{
private:
typedef hashtable<pair<constKey, T>, Key, HashFcn,selectlst<pair<const Key, T> >, EqualKey, Alloc> ht;
ht rep;
public:
typedef typename ht::key_type keytype; 
typedef T data_type; 
typedef T mapped_type;
typedef typename ht::value_type value_type; 
typedef typename ht::hasher hasher; 
typedef typename ht::key_equal key_equal; 
typedef typename ht::size_type size_type;

typedef typename ht::difference_type difference_type; 
typedef typename ht::pointer pointer; 
typedef typename ht::const_pointer const_pointer; 
typedef typename ht::reference reference; 
typedef typename ht::const_reference const_reference;
typedef typename ht::iterator iterator;
typedef typename ht::const_iterator const_iterator;
hasher hash_funct() const { return rep.hash—funct (); } 
key_equal key_eq() const{return rep.key_eq(); } 

public:
//缺省使用大小为100的表格。 将被hash table调整为最接近且较大之质数
hash_multimap():rep (100, hasher(), key_equal()) {} 
explicit hash_multimap(size_type n):rep(n, hasher(), key_equal()){} 
hash_multimap(size_type n, const hasher& hf) : rep(n, hf, key_equal()) {} 
hash_multimap(size_type n, const hasher& hf, const key_equal& eql) :rep(n,hf,eql){}

//以下, 插入操作全部使用insert_equal() , 允许键值重复
template <class Inputiterator> 
hash_multimap(InputIterator f, Inputiterator l):rep(lOO, hasher(), key_equal()) { rep.insert_equal(f, l); } 
template <class Inputiterator>
hash_multimap(InputIterator f, Inputiterator l, size_type n):rep(n, hasher(), key_equal()) { rep.insert_equal(f, 1);}
template <class InputIterator>
hash_multimap(InputIterator f, Inputiterator l, size_type n, const hasher& hf):rep(n, hf, key_equal()) { rep.insert_equal(f, l);}
template <class Inputlterator>
hash multimap(Inputiterator f, Inputiterator 1, size_type n, const hasher& hf,const key_equal& eql):rep(n,hf,wql){rep.insert_equal(f,l);}
public:
//所有操作几乎都有hash table的对应版本, 传递调用即可
size_type size() const{ return rep.size(); } 
size_type max_size()const{ return rep.max_size(); } 
bool empty() const{ return rep.empty(); } 
void swap(hash_multimap& hs) { rep.swap(hs.rep); } 
friend bool operator==__STL_NULL_TMPL_ARGS (const hash_multimap&, const hash_multimap&); 
iterator begin() { return rep.begin(); } 
iterator end() { return rep.end(); }
const_iterator begin() const { return rep.begin();}
const_iterator end() const { return rep.end(); } 

public:
iterator insert (const value_type& obj) { return rep. insert_equal (obj);}
template <class Inputiterator> 
void insert (Inputiterator f, Inputiterator l) { rep. insert_equal (f, 1);} 
iterator insert_noresize(const value_type& obj) 
{return rep.insert_equal_noresize(obj);}
iterator find(const key_type& key) { return rep.find(key);} 
const_iterator find(const key_type& key) const{return rep.find(key);} 
size_type count(const key_type& key) const{ return rep.count(key);} 
pair<iterator,iterator>equal_range(const key_type& key) 
{return rep.equal_range(key); } 
pair<const_iterator,const_iterator> equal_range(const key_type& key) const{return rep.equal_range(key);}

size_type erase(const key_type& key) {return rep.erase(key); }
void erase(iterator it){rep.erase(it);} 
void erase(iterator f, iterator l) {rep.erase(f, l);}
void clear(){ rep.clear();} 

public:
void resize(size_type hint){ rep.resize(hint);} 
size_type bucket_count() const{return rep.bucket_count();} 
size type max_bucket_count()const { return rep.max_bucket_count();}
size_type elems_in_bucket(size_type n) const {return rep.elems_in_bucket(n);}
};

template<class Key, class T, class HF, class EqKey, class Alloc> 
inline bool operator==(const hash_multimap<Key,T, HF, EqKey, Alloc>&hm1,const hash_multimap<Key, T, HF, EqKey, Alloc>& hm2)
{
return hm1.rep == hm2.rep;
}

hash_multimap的使用方式,与hash_map完全相同。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值