======STL与泛型编程 第三周======
本页内容是作为 //Boolan// C++ 开发工程师培训系列的笔记。\\
class deque {
public:
typedef T value_type;
....
protected:
typedef pointer* map_pointer; //T**
protected:
map_pointer map;
size_type map_size;
....
};
因为我们在控制器中存储的是指向 //Buffer// 的指针,因此我们需要**指向指针的指针**这种类型。同时,我们也需要维护一个 ''T'' 类型的数据 ''map_size'' 来表示控制器中 //Buffer// 的数量。
==迭代器==
为了匹配接口,我们需要两个迭代器:''start'' 和 ''finish''。这两个迭代器在类中的声明如下:
class deque {
public:
typedef _deque_iterator iterator;
protect:
iterator start;
iterator finish;
....
};
这两个迭代器一个指向 //Deque// 的开头元素,另外一个指向 //Deque// 的末尾元素的后一位。跟别的容器一样,//Deque// 也是通过这两个迭代器来定义了三个基本方法:
iterator begin() { return start; }
iterator end() { return finish; }
iterator size() { return finish - start; }
===迭代器的结构实现===
迭代器在 //Deque// 中扮演了非常重要的角色。我们知道 //Deque// 在物理上不是完全连续的;为了像维护顺序容器一样维护 //Deque//,我们不但需要采用 ''random access'' 类型的迭代器,同时还需要对该迭代器的一系列相关运算进行重载。
\\
\\
除了C++要求的五条基本信息以外,根据本节开头的图示,我们还需要使用迭代器来维护当前的 //Buffer//。因此,我们需要定义前面提到过的四个指针:
T* cur;
T* first;
T* last;
map_pointer node; //T**
同时,迭代器类也定义了两个类型,方便以后重载使用:
typedef size_t size_type;
typedef _deque_iterator self;
===Buffer大小的计算方法===
在 //G 2.9// 的版本中,//Buffer// 的大小(也就是 //Buffer// 包含的元素个数)是通过参数传递到 //Deque// 中的。我们发现默认的语句中有这么一句:
size_t BuffSiz = 0;
这里的 ''0'' 实际上并不是指代的 //Buffer// 的大小。在 //Deque// 中, //Buffer// 的大小控制是通过下面的函数来判断的:
inline size_t _deque_buf_size(size_t n, size_t sz) {
return n != 0 ? n :
(sz < 512 ? size_t(512 / sz) : size_t(1));
}
上面的代码主要分如下几个步骤:
- 如果 ''n'' 不为 ''0'',返回 ''n'', 也就是使用自定义大小的 //Buffer//。
- 如果 ''n'' 为 ''0'',则表示使用默认值 ''512''.
- 在 ''n'' 为 ''0'' 的情况下,如果元素的大小小于 ''512'',则返回的大小为 //Buffer// 的大小除以元素的个数,即 ''512 / sz''。
- 如果元素大小大于 ''512'',则返回 ''1'',即该 //Buffer// 只有一个元素。
===Insert操作的实现===
''Insert'' 可以作为一个典型的例子来说明 //Deque// 的数据结构的另外一个特性:双头开口。因为插入的元素可以处于 //Deque// 的任何一个位置,因此根据不同的位置决定不同的操作,会显著的提高写效率。STL 对 ''insert'' 的实现分为如下几个步骤:
* 首先判断插入点是否在容器的头部或者尾部
* 如果在头部,使用 ''push_front()''
* 如果在尾部,使用 ''push_back()''
* 如果不在头部或者尾部,调用函数 ''insert_aux''。该函数会求出当前位置和起点和终点的距离
* 如果在插入点前面的元素个数较少(距离小于 ''size()'' 的一半),则使用 ''push_front()''
* 否则使用 ''push_back()''
具体的实现代码如下:
iterator insert(iterator position, const value_type& x) {
//if insert at start
if (position.cur == start.cur) {
push_front(x);
return start;
//if insert at end
} else if (position.cur == finish.cur) {
push_back(x);
iterator tmp = finish;
--tmp;
return tmp; //return the iterator that point to the last element
} else {
return insert_aux(position, x);
}
/* insert_aux */
template
typename deque::iterator
deque::insert_aux(iterator pos, const value_type& x) {
difference_type index = pos - start; // get the distance from insert position to start
value_type x+copy = x;
//distance < size() / 2 means the insert point is closer to the start
if (index < size() / 2) {
push_front(front()); //add a new space
...
copy(front2, pos1, front1); //move other elements, make a empty space for the insert emelent
} else {
push_back(back());
...
copy_backward(pos, back2, back1);
}
*pos = x_copy;
return pos;
}
\\
====Deque如何模拟连续空间====
//Deque// 的迭代器是**随机访问**类型的,但其数据结构并不是物理连续的。因此,我们需要对 //Deque// 的迭代器的某些功能进行某些运算的重载,以让其达到随机访问的效果。由于 //Deque// 是由一个个缓冲区组成的,而这些缓冲区并不连续。因此,//Deque// 迭代器最重要的操作,就是判断迭代器移动以后,指向的元素是否还在当前的 //Buffer// 中。如果进行了跨区访问,我们则需要更新迭代器的 ''node'' 的信息。
\\
\\
作为一个随机访问的迭代器,是应该具有以下的功能的:
* ''*'' / ''->'',基本的指针功能。
* 自增 / 自减 功能。
* ''+='' / ''-='' / ''+'' / ''-'',即迭代器的随机移位功能。
===指针基本功能的实现===
//Deque// 中 ''*'' 和 ''->'' 的实现与其他容器一致:
reference operator* () const { return *cur; }
pointer operator->() const {} return &(operator*()); }
===“-”的重载===
operator - (const self& x) const {
return difference_type(buffer_size()) * (node - x.node -1) + (cur - first) + (x.last - x.cur);
}
这里的 ''difference_type(buffer_size())'' 代表了 //Buffer// 的长度,''node'' 是控制器用于指向 //Buffer// 的指针,因此 ''(node - x.node -1)'' 代表了 两个迭代器之间相隔了多少 //Buffer//。''cur - first'' 实际上是 ''this.cur - this.first'',代表了起始迭代器所在 //Buffer// 中包含的元素个数。
===“++”和“--”的重载===
自增 / 自减运算也是迭代器必要的运算之一。在 //Deque// 中,重载自增运算有一个很重要的问题需要考虑:**自增 / 自减操作是否越界(跨区)**。试想一下,如果一个迭代器正处于第一个 //Buffer// 的末尾位置,它的自增操作就会让其指向下一个 //Buffer// 的首位。这个过程需要通过控制器来实现,大致的步骤如下:
- 切换迭代器至下一位
- 如果此时迭代器处于最后一个元素的后一位 ''last'',则证明该自增操作需要进行跨区操作处理
- 通过控制器执行跨区操作
- 将当前迭代器指向新区的起点
整个流程用代码表现出来就是:
self& operator++() {
++cur;
if(cur == last) {
set_node(node + 1); // jump to the next Buffer
cur == first; //set cur iterator to the first
}
return *this;
}
这里的跨区操作通过 ''set_node'' 函数来实现,需要的参数是一个指向 //Buffer// 的指针。该函数通过改变迭代器中的 ''node''、''first'' 和 ''last'' 来实现跨区:
void set_node(map_pointer new_node) {
node = new_node; //switch to the new buffer
first = *new_node; // set first, make it points to the start of the new buffer
last = first + difference_type(buffer_size()); //last = address of first + buffer size
}
上面说到的是前自增版本。后自增版本与 ''list'' 的实现一致,就不再赘述。自减的重载与自增非常类似,因此也不再赘述。
===“+=”和“-=”的重载===
''+=''操作与 ''-'' 操作很类似,也需要判断是否跨区。而相比 ''-'' 操作,因为 ''+='' 是一个包含了赋值操作的运算,因此 ''+='' 不但需要知道是否跨区,而且需要知道**具体跨了多少区**。如果知道了跨区的大小,我们可以利用上一小节中的 ''set_node'' 函数来实现迭代器的重定位。该重载的大概实现流程如下:
- 判断当前位置与移位操作量相加后是否在同一 //Buffer// 中
- 如果是,直接进行 ''+='' 运算
- 如果不是
- 计算相隔的 //Buffer//
- 切换到正确的 //Buffer//
- 切换到正确的元素
上述流程可以用以下代码实现:
self& operator += (difference_type n) {
difference_type offset = n + (cur - first);
if (offset >= 0 && offset < difference_type(buffer_size())) // offset in buffer
cur += n;
//offset out of current buffer
else {
//whether the offset is going forward or backward (using this condition to represent both += and -=)
difference_type node_offset = offset > 0 ? offset / difference_type(buffer_size()))
:-difference_type((-offset - 1) / buffer_size() - 1;
//switch to the right buffer
set_node(node + node_offset);
//move iterator to the right position
cur = fist + (offset - node_offset * difference_type(buffer_size()));
}
return *this;
还有一些有趣的事情:
* ''-='' 其实是可以用 ''+='' 实现的,将 ''n'' 变为 ''-n'' 就可以了
* ''+'' 也是可以用 ''+='' 实现的。因为 ''+'' 不做赋值,不修改现有迭代器,因此返回的值可以用 ''tmp += n;'' 表示, ''tmp'' 表示的是当前迭代器的位置,也就是 ''*this'';
* 而取下标 ''[]'' 这个经典的顺序容器操作,则可以用 ''+'' 来实现了: ''return *(*this + n)''
===控制器的扩充===
前面说到过 //Deque// 的控制器使用了 ''Vector'' 来实现。那么当 //Buffer// 的数量超过了原有的控制器大小,控制器也是会自动扩充的。不过控制器的扩充则做了一点点小小的变化:每次扩充的时候,都会**把现有的控制器元素拷贝到新控制器的中间区域**。这样做有一个很明显的好处:我们知道 //Vector// 中如果插入新元素导致现有元素移位,都需要调用构造 / 析构函数。而 //Deque// 是一个双向开口的容器。因此如果我们将开口附近的区域留空,这样即使插入新的 //Buffer//,控制器在一定程度内是不需要移动现有元素的,这样效率会更高一些。
===Queue和Stack===
严格意义上来说, //Queue// 和 //Stack// 应该被称为**容器适配器**,因为他们是依托基本的容器来实现的。之所以放到 //Deque// 章节讨论,是因为依托 //Deque// 实现的方式效率**最高**。
\\
\\
首先来看看 //Queue// 和 //Stack// 都需要什么样的功能:
\\
\\
//Queue// 是先进先出的数据结构,因此需要实现的函数主要有:
* 查看元素:''front()'', ''back()''
* 操作元素: ''push()'' 和 ''pop()''
//Deque// 和 //List// 都可以通过自身的函数''front()'' 、''back()''、''push_front()'' 和 ''push_back()'' ,因此他们都可以做为 //Queue// 的底层容器。
\\
\\
而 //Stack// 因为是先进先出,因此需要实现的主要函数有:
* 查看元素:''top()''
* 操作元素:''push()'' 和 ''pop()''
''top()'' 操作可以通过 ''back'' 来完成,因此不但 //Deque// 和 //List// 可以作为 //Stack// 的底层容器,连 //Vector// 也可以。
\\
\\
因此我们可以得出一个结论:不管什么样的容器,只要其操作可以实现 //Queue// 和 //Stack// 的数据结构操作,那这些容器都可以作为 //Queue// 和 //Stack// 的底层容器。由这个结论我们可以推一些反例出来:
* 关系容器是没法实现 //Queue// 和 //Stack// 的
* //Vector// 只能实现 //Stack//
不过这里比较 tricky 的是,尽管 //Vector// 不能实现 //Queue//,但可以实现其中的某一些操作,比如需要 ''push_back()'' 的操作都是可以实现的。这是因为编译器在编译过程中,只会针对调用的方法进行检测;如果没有调用到不能实现的方法,那么是不会报错的。
\\
\\
除此之外,//Queue// 和 //Stack// 还有两个非常重要的特性:
* **不允许遍历**
* **不允许使用迭代器**
因为以上两种方式都可能破坏这两种数据结构的特性(先进先出,先进后出)。
====红黑树作为容器的使用====
本系列第一周课程中提到过关系容器的底层实现都是红黑树。为了了解关系容器,我们需要先明白红黑树这个数据结构的一些特点。
\\
\\
红黑树是**平衡搜索二叉树**的一种,通过节点的红黑属性来达到平衡的效果。所谓的平衡,就是指在树不会有远比其他的分支长的分支导致影响搜索的效率。
\\
\\
template
从上面的代码我们发现红黑树需要五种类型的数据:
* ''key'' 用于表示元素的 //Key// 值。有些元素是没有 ''data'' 的,因此自身也又是 ''key'' 又是 ''data'' (比如 //Set//)。
* ''value'' 在 //Set// 里等于 ''key'',在 //Map// 里等于 ''key'' + ''data''。
* ''KeyofValue'' 定义了如何获取指定元素。
* ''header'' 类似于 //List// 中的空节点,是为了更方便的实现数据结构而设计的。
* ''alloc'' 是分配器。
上述的类型通过下面的代码组成了红黑树这个类的结构:
class rb_tree {
....
protected:
typedef _rb_tree_node rb_tree_node; // key + data
size_type node_count; // elements count in rb_tree
link_type header;
Compare key_compare; //condition for sorting
};
===红黑树的使用===
根据上述的信息我们可以很简单的建立一个红黑树:
rb_tree, less, alloc> myTree;
* 第一个 ''int'' 表示了 ''key'' 的类型
* 第二个 ''int'' 表示了 ''value'' 的类型
* 第三个 ''identity
map myMap; // a map with elements that have int key and string data
因为 //Map// 其元素独特的结构,对于取值函数,则不能使用 ''identity()'' 了;取而代之的是 ''selcet1st()''。该函数可以将 ''pair'' 中的第一个数据取出,也就是 ''key''。需要注意的是这个函数也是 //GNU// 独有的;但其他编译器也有类似的实现方法。该函数接收一个 ''pair'' 类型,返回该类型的 ''first'' 值。
\\
\\
由于 //Map// 中涉及到修改元素的 ''data'' 部分,因此 //Map// 的迭代器部分并没有设计成 ''const'' 类型。取而代之的是,//Map// 将作为元素的 ''pair'' 设计成了 ''
template
上述的参数作为类型具体表现在哈希表类中的情况:
hasher hash; // HashFcn
key_equal equals; // EqualKey
ExtractKey get_key;
typedef _hashtable_node node; //unit of linklist
vector buckets; //bucket conatiner
size_type num_elements; //size of hashtable
值得注意的是,''node'' 类型在这里除了表示元素的值以外,结构中还附带了一个指针指向下一个元素:
_hastable_node* next;
Value val;
当然,需要维护这些数据,哈希表也需要有迭代器。哈希表中的迭代器通过自增和自减遍历整个哈希表。但因为哈希表是以桶来存储元素的,因此经常会出现跨桶移动迭代器的情况。为此,哈希表中的迭代器额外设置了一个**指向桶容器的指针** ''hashtable'',用于在跨桶访问的情况下顺利返回桶容器:
_hastable_node* next;
node* cur;
hashtable* ht; //point to bucket vector
===哈希函数===
哈希函数的作用就是利用元素的某个属性来生成元素的编号,这个编号我们也称为 //Hash Code//。在哈希表中,使用怎么样的策略来对元素生成编号,实际上是一件非常困难的事,因为这必须依靠你自身的经验。你自己设计的元素类型和内容,你要如何设定策略,这是没有一个共通的方法的。
\\
\\
当然,哈希表对不同的 //built-in// 类型,甚至 //C-string// 的编号分配策略都是有预先实现的。该策略集合通过模板的偏特化来实现:
//Generic
template hash {};
__STL_TEMPLATE_NULL struct hash {
size_t operator() (char x) const { return x; };
对于基本的可以用数值表示的类型,哈希函数的处理策略都是返回其本身的值作为元素的编号。而对C字符串来说,则有如下的计算方式:
inline size_t _stl_hash_string(const char* s) {
unsigned long h = 0; //h is hash_code
for (; *s, ++s)
h = 5*h + *s; // h of "abc" is 5*(5*'a' + 'b') + ''c''
上面这个方法也同样没有什么数学含义。这个方法的唯一目的在于希望尽可能的使编号离散化。因为编号越离散,越杂乱,越不容易发生重叠,这样造成单元链表变长的几率则越少。理论上来说,一个哈希表的最优情况是做到单元链表的平衡。
\\
\\