第四章 序列式容器
所谓序列式容器,其中的元素都可序,但未必有序。
C++语言本身提供了一个序列式容器array。
序列式容器: array C++内建
vector
heap 以算法呈现(xxx_heap)
priority-queue
list
slist 非标准
deque
stack 配接器
queue 配接器
以下只介绍各种容器的理论技术,具体的代码请查看SGI STL源码。
1.vector 包含于<vertor>,实现于<stl_vector.h>
①vector概述
vector的数据安排和操作方式跟array相似,唯一差别在于空间运用的灵活性。
array是静态空间,一旦配置了就不能改变,如需改变,需用户自己实现细节。
vector是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。
vector的实现技术,关键在于其对大小的控制以及重新配置时的数据移动效率。
"配置新空间/数据移动/释放旧空间",时间成本很高,应该加入未雨绸缪的考虑。
②vecotr定义摘要
<stl_vector.h>
③vecotr迭代器
vector维护的是一个连续线性空间,所以不论其元素类型为何,普通指针都可以作为vector的迭代器而满足所有条件。
vector支持随机存取,因此vector提供的是random access iterators。
④vector数据结构
vector所采用的数据结构非常简单:线性连续空间。它以两个迭代器start和finish分别指向配置得来的连续空间中目前已被使用的范围,并以迭代器end_of_storage指向整块连续空间(含备用空间)的尾端。
为了降低空间配置时的速度成本,vector实际配置的大小可能比客户端需求更大,以备将来可能的扩充。这便是容量的概念。一旦容量等于大小,便是满载,再增加新元素,整个vector就要另觅新所。
运用start、finish、end_of_storage三个迭代器,可以轻易的提供首尾标识、大小、容量、空容器判断、下标运算符、最前/后端元素等。
vector缺省使用alloc作为空间配置器,并据此另外定义了data_allocator,以便以元素大小为配置单位。
⑤vector构造与内存管理
constructor
push_back
会根据第一个模板参数来决定调用构造还是fill_n。
push_back的时候会判断是否还有备用空间,没有则重新申请空间(2倍)。
⑥vector元素操作
pop_back 拿掉尾部元素并调整大小
erase 清除区段内元素或清除单个元素
clear = erase(begin(),end());
清除所有元素
insert 插入n个节点到position节点之前
2.list
包含于<list>,实现于<stl_list.h>
①list概述
list的好处是每次插入或删除一个元素,就配置或释放一个元素空间。list对空间的利用绝对精准,一点也不浪费。
而且对于任何位置的元素插入或删除,list永远是常数时间。
list和vector,什么时机下最适合使用哪种容器,必须是元素的数量,构造复杂度,元素存取行为的特性而定。
②list结点
list本身与list结点是不同的结构,要分开设计。list是双向链表。
③list迭代器
list的结点不保证在内存中连续,所以不能用普通指针作为迭代器。
list迭代器必须有能力指向list的结点,并有能力进行正确的递增++、递减--、取值*、成员存取->等操作。
list是个双向链表,迭代器必须具备前移后移能力,所以list提供的是Bidirectional Iterators。
④list数据结构
SGI list是一个环状双向链表,所以它只需一个指针,便可以完整的表现整个链表。
让list中的node指针,指向刻意置于尾端的一个空白结点,node便符合STL前开后闭的要求,成为last迭代器。
⑤list构造与内存管理
constructor
push_back 内部调用insert
insert 有n个重载
list缺省使用alloc作为空间配置器,并据此另外定义了list_node_allocator,以便以元素大小为配置单位。
⑥list元素操作
push_front/
push_back/
erase/
pop_front/
pop_back/
clear/
remove/
unique/
splice(接合)/
merge/
reverse/
sort
list内部提供一个所谓迁移操作(transfer),将某连续范围内的元素迁移到某个特定位置之前。
3.deque
包含于<deque>,实现于<stl_
deque
.h>
①deque概述
vector是单向开口的连续线性空间,deque是一种双向开口的连续线性空间。
deque允许常数时间内进行元素的插入和移除操作;deque没有容器的概念,它是动态的以分段连续空间组合而成。不会出现向vector那样因空间不足而配置复制释放的动作。
对deque的排序,为了最高效率,讲deque完整复制到一个vector,排序后,复制回deque。
②deque中控器
deque是由一段段的定量连续空间构成,一旦有必要在deque的前端或尾端增加新空间,便配置一段定量连续空间,串接在整个deque头端或尾端。
deque的最大任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取接口。
避开了重新配置复制释放的轮回,代价是复杂的迭代器结构。
deque采用一块所谓map作为主控。这里的map是一小块连续空间,其中每个结点都是指针,指向另一块称为缓冲区的连续线性空间。缓冲区才是deque的储存空间主体。默认值0表示将使用512Bytes缓冲区。
③deque迭代器
deque是分段连续空间,维持其整体连续假象的任务,落在了迭代器的operator++和
operator--身上。
__deque_iterator未继承自std::iterator。
__deque_iterator中的四个指针分别表示:
T* cur; 当前缓冲区指针
T* first; 该缓冲区的头端
T* last; 该缓冲区的尾端
map_pointer node; 该缓冲区在map中的位置。
各种指针运算,加、减、前进、后退,都需注意:一旦行进到缓冲区边缘,可能需要调用set_node()跳转缓冲区。
④deque数据结构
deque除了维护一个指向map的指针外,也维护start、finish两个迭代器,分别指向第一缓冲区的第一元素和最后缓冲区的最后一个元素。同时也记住目前map的大小。
⑤deque构造与内存管理
ctor
push_back push_back_aux
push_front push_front_aux
deque自行定义了两个专属的空间配置器data_allocator和map_allocator。
map的重新整治reallocate,当map满载之后发生(因为map是不能动态增长的),
由reserve_map_at_back()和
reserve_map_at_front()判断,
reallocate_map执行。
⑥deque
元素操作
pop_back\pop_front\clear(清除整个deque,保有一个缓冲区)\erase\insert
4.stack
包含于<stack>,实现于<stl_
stack
.h>
①stack概述
stack是一种FILO的数据结构。只有一个出口。允许增加、移除、取得顶部元素。不允许有遍历行为。
②stack定义完整列表
<stl_
stack
.h>
SGI STL缺省情况下用deque作为底层容器。更改某物接口,形成另一种风貌。容器配接器。
③stack没有迭代器
stack所有元素都必须符合先进后出条件,只有顶端的元素才有机会被外界取用,不提供走访功能。
④以list作为stack的底层容器
list也是一种双向开口的数据结构,若以list为底部结构并封闭其头端开口也可以轻易实现stack.
5.queue
包含于<queue>,实现于<stl_
queue
.h>
①queue概述
queue
是一种FIFO的数据结构。有两个出口。允许增加、移除、取得顶部元素、从底部加入元素。不允许有遍历行为。
②queue
定义完整列表
<stl_
queue
.h>
③queue没有迭代器
④以list作为queue的底层容器
6.heap 隐式表述,实现于<
stl_heap.h>
①heap概述
heap并不属于STL容器组件,它扮演priority queue的助手。
priority queue允许用户以任何次序将任何元素推入容器,但取出时一定是以优先权最高的元素开始取。
采用二叉堆(binary heap)作为底层机制。
binary heap是一种complete binary tree完全二叉树。
实现:完全二叉树的特点是整棵树内没有任何结点漏洞,这样我们用线性存储方式vector来储存所有结点。
将第0个元素保留,那么当完全二叉树的某个结点位于i处时候,其左子结点位于2i处,右子结点位于2i+1处,父结点位于i/2处。
这样就可以用vector轻易实现完全二叉树,这种以线性空间表示树的方式,称为隐式表述法。
其实这种方式可以反过来看,线性存储空间里面,是把树以层序遍历的方式存储到vector中。
当然SGI STL的heap没采用这种技巧,其计算左右孩子和父结点的方式另有不同。回头我们研究源码。
heap分为max-heap和min-heap,区别是排序方式,从大到小和从小到大。
max-heap的要求,第一是完全二叉树,第二父结点总是大于等于子节点。
min-heap的要求,第一是完全二叉树,第二父结点总是小于等于子节点。
②heap算法
push_heap
首先要将新加入的元素放到最下一层作为叶节点,并填补从左到右的第一个空格,也就是vector的end处。
然后为了满足父结点总是大于等于子节点这一条件,与于父结点比较,如果大于父结点,就交换,直到满足条件。
pop_heap
因为pop的总是最大值(max-heap),而最大值肯定是根节点。
所以做法是把根节点与最后一个结点交换,然后再调整新的根节点的位置。
pop_heap不会移除结点,只是调整到vector的最末。
sort_heap
采用pop_heap,每次pop_heap后最大值肯定在vector的最后。
所以一直调用pop_heap直到只剩下一个元素。当然之后此vector就不是一个有效heap了。
make_heap
将现有数据转化为一个heap。依照之前提到的完全二叉树的隐式表述。
③heap没有迭代器
heap的所有元素都必须遵循特别的(完全二叉树)排列规则,所以heap不提供遍历功能,也不提供迭代器。
7.priority_queue 包含于<queue>,实现于<stl_queue.h>
①
priority_queue概述
priority_queue是一个拥有权值概念的queue,它允许加入新元素,移除旧元素,审视元素值。
由于这是一个queue,所以只允许在低端加入元素,从顶端取出元素。
缺省情况下
priority_queue是用一个max-heap完成,max-heap是一个以vector表现的complete binary tree。
②
priority_queue定义完整列表
priority_queue完全以底部容器为根据,再加上heap处理规则,所以实现非常简单。
缺省情况下以vector作为底部容器。
③
priority_queue没有迭代器
priority_queue的所有元素,进出都有一定的规则,只有顶端的元素才有机会被外界取用。
priority_queue 不提供遍历功能,也不提供迭代器。
8.slist 单向链表,
包含于<slist>,实现于<stl_slist.h>
①slist概述
slist并不在标准规格之内。
slist与list的主要差别在于,slist的迭代器属于单向的Forward iterator,list的迭代器属于双向的Bidirectional Iterator。共同特色在于,插入移除接合操作不会造成原有的迭代器失效。
STL的习惯是插入操作会将元素插入于指定位置之前,但是slist不能反向定位。于是提供了insert_after()和erase_after(),基于同样的效率考虑,slist不提供push_back(),只提供push_front()。
②slist结点
slist的结点设计比list要复杂,运用了继承关系:
struct __slist_node_base
{
__slist_node_base* next;
};
template <class T>
struct __slist_node : public __slist_node_base
{
T data;
};
③slist迭代器
同样采用继承关系:
struct __slist_iterator_base
{...};//重载了== 和!=操作符
template <class T, class Ref, class Ptr>
struct __slist_iterator : public __slist_iterator_base
{...};//重载了前置++和后置++操作符。
④slist数据结构
template <class T, class Alloc = alloc>
class slist
{
...
private:
list_node_base head;//头结点
...
};
结点的插入push_front():
构造结点,create_node();
把结点添加到slist中,__slist_make_link();
结点的移除pop_front():
移除结点 head->next = head->next->next;
销毁结点,destroy_node()
⑤slist元素操作
begin()
end()//返回是iterator(0),为了实现前闭后开[first,last)的规则。
push_front()
pop_front()
insert()
insert_after()
...