首页 > 其他 > 详细

STL——序列式容器

时间:2015-11-20 09:11:49      阅读:336      评论:0      收藏:0      [点我收藏+]

一、容器概述与分类 1. STL容器即是将运用最广的一些数据结构实现出来。常用的数据结构有array, list, tree, stack, queue, hash table, set, map……等等。根据“数据在容器中的排列”特性,这些数据结构分为序列式和关联式两种。本篇讨论序列式容器。

yxt1-P114-图4-1

这里所谓的衍生,并非派生关系,而是内含关系。例如heap内含一个vector,priority-queue内含一个heap,stack和queue都内含一个deque,set/map/multiset/multimap都内含一个RB-tree,hash_x都内含一个hashtable。

2. 序列式容器 所谓的序列式容器,其中的元素都可序(ordered,有位置属性),但未必有序(sorted,值未必有序)。C++ 语言本身提供了一个序列式容器array,STL另外再提供vector,list, deque, stack, queue, priority-queue等等序列式容器。其中stack和queue由于只是将deque改头换面而成,技术上被归类为一种配接器(adapter)。

二、Vector

1. vector 概述

vector 的数据安排以及操作方式,与array非常相似。两者唯一差别在于空间运用的灵活性。array是静态空间、一旦配置了就不能改变,要修改空间大小,只能通过“配置新空间/数据移动/释还就空间”这种成本很高的方法来进行。vector是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新的元素。

2. 要使用vector,必须先包括<vector>,但SGI STL 将vector实现于更底层的<stl_vector.h>。 参见相关源码。

3. vector 迭代器 vector维护的是一个连续显性空间,所以不论其元素型别为何,普通指针都可以作为vector的迭代器而满足所有必要条件,因为vector迭代器所需要的操作行为,如 operator*, operator->, operator++, operator--, operator+, operator-,operator+=, operator-=,普通指针天生就具备。vector支持随机存取,而普通指针正有着这样的能力。所以,vector提供的是Random Access Iterators。

4. vector数据结构 vector所采用的数据结构非常简单:线性连续空间。它以两个迭代器start和finish分别指向配置得来的连续空间中目前已被使用的范围(容器大小),并以迭代器end_of_storage指向整块连续空间(含备用空间,容器容量)的尾端。

yxt2-P119-图4-2

5. vector的构造与内存管理:constructor,push_back
vector缺省使用alloc(第二章)作为空间配置器,并据此另外定义了一个data_allocator,为的是更方便以元素大小为配置单位:

template <class T, class Alloc = alloc>
class vector
{
    protected:
        typedef simple_alloc<value_type, Alloc> data_allocator;
    ....
};

于是,data_allocator::allocate(n) 表示配置n个元素空间。
vector提供许多constructors,其中一个允许我们指定空间大小及初值:

// fill_initialize(填充并予以初始化)-> 调用allocate_and_fll(配置而后填充)-> 
// 调用uninitialized_fill_n(), uninitialized_fill_n()会根据第一参数的型别特性(type traits),决定
// 使用算法fill_n() 或反复调用construct() 来完成任务(2.2.3节,图2-1),参见相关源码
vector(size_type n, const T& value) { fill_initialize(n, value); }

当我们以push_back() 将新元素插入于vector尾端是,函数首先检查是否还有备用空间,如果有就直接在备用空间上构造元素,并调整迭代器finish,使vector变大。如果没有备用空间了,就扩充空间(重新配置、移动数据、释还原空间)。参见相关源码。

注意:所谓动态增加大小,并不是在原空间之后连续新空间(因为无法保证原空间之后尚有可供配置的空间),而是以原大小的两倍另外配置一块较大空间,然后将原内容拷贝过来,然后才开始在原始内容之后构造新元素,并释还原空间。因此,对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。这是程序员易犯的一个错误,务需小心。

6. vector 的元素操作:pop_back,erase,clear, insert

(1)pop_back:将尾端标记往前移一格,表示将放弃尾端元素;释还尾端元素。

(2)erase(first, last):将last之后的n个元素赋值到以first为起点的区间内(copy(last, finish, first);),释还元素;

(3)insert操作图如下: yxt3-P126-图4-3b1、图4-3b2、图4-3b3

三、List

1. list概述 相较于vector的连续线性空间,list就显得复杂许多,它的好处是每次插入或删除一个元素,就配置或释还一个元素空间。因此,list对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插入或元素移除,list永远是常数时间。

2. list的节点 参见相关源码

3. list的迭代器 list 不再能够像vector一样一普通指针作为迭代器,因为其节点不保证在存储空间中连续存在。list 迭代器必须有能力指向list的节点,并有能力进行正确的递增、递减、取值、成员存取等操作。所谓“list迭代器正确的递增、递减、取值、成员取用”操作是指,递增时指向下一个节点,递减时指向上一个节点,取值时取的是节点的数据值,成员取用时取用的是节点的成员。由于STL list 是一个双向链表,迭代器必须具备前移、后移的能力,所以list提供的是Bidirectional iterators。

yxt4-P130-图4-4

list有一个重要性质:插入操作和接合操作都不会造成原有的list迭代器失效。这在vector是不成立的,因为vector 的插入操作可能造成容器重新配置,导致原有的迭代器全部失效。甚至list的元素删除操作,也只有“指向被删除元素”的那个迭代器失效,其他迭代器不受任何影响。

list迭代器的设计,参见相关源码

4. list 的数据结构

SGI list 不仅是一个双向链表,而且还是一个环状双向链表。所以它只需一个指针,便可完整表现整个链表。如果让指针node指向刻意置于尾端的一个空白节点,node便能符合STL 对于“前闭后开”区间的要求,成为last迭代器。 yxt5-P132-图4-5

5. list的构造与内存管理:constructor、push_back、insert

list 缺省使用alloc 作为空间配置器,并据此另外定义了一个list_node_allocator,为的是更方便地以节点大小为配置单位:

template <class T, class Alloc = alloc>
class list
{
    protected:
        typedef __list_node<T> list_node;
        // 专属之空间配置器,每次配置一个节点大小
        typedef simple_alloc<list_node, Alloc> list_node_allocator;
    ....
};

push_back() 函数内部调用insert()。

yxt6-P136-图4-6

6. list 的元素操作:  push_front, push_back, erase, pop_front, pop_back,  clear, remove, unique, splice, merge, reverse, sort

list 内部提供一个所谓的迁移操作(transfer):将某连续范围的元素迁移到某个特定位置之前。技术上很简单,节点间的指针移动而已。 参见相关源码

四、deque

1. deque 概述

vector是单向开口的连续线性空间,deque则是一种双向开口的连续线性空间。所谓双向开口,意思是可以在头尾两端分别作元素的插入和删除操作。vector当然也可以在头尾两端进行操作,但其头部操作效率奇差,无法被接受。

deque和vector最大差异,一在于deque允许于常数时间内对起头端进行元素的插入或移除操作,二在于deque没有所谓容量(capacity)概念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。

虽然deque也提供Random Access Iterator,但它的迭代器并不是普通指针,其复杂度和vector不可以道里计。因此,除非必要,我们应尽可能选择使用vector而非deque。

2. deque系由一段一段的定量连续空间构成。一旦有必要在deque的前段或尾端增加新空间,便配置一段定量连续空间,串接在整个deque的头端或尾端。deque的最大任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取接口。避开了“重新配置、复制、释放”的轮回,代价则是复杂的迭代器架构。

deque采用一块所谓的map(注意,不是STL的map容器)作为主控,这里的map是一小块连续空间,其中每个元素都是指针,指向另一段(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的存储空间主体。SGI STL 允许我们指定缓冲区大小,默认值0表示将使用512bytes缓冲区。map其实是一个T**,它是一个指针,所指之物又是一个指针。

参见deque容器相关源码;

yxt7-P145-图4-10  

3. deque 的迭代器

参见deque迭代器相关源码

yxt8-P147-图4-11、图4-12

4. deque的数据结构 deque除了维护一个先前说过的指向map的指针外,也维护start,finish两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素(的下一位置)。此外,它当然也必须记住目前的map大小。因为一旦map所提供的节点不足,就必须重新配置更大的一块map。 参见deque容器相关源码

5. deque的构造与内存管理:ctor, push_back, push_front deque自行定义了两个专属的空间配置器:

protected:
// 专属空间配置器,每次配置一个元素大小
typedef simple_alloc<value_type, Alloc> data_allocator;
// 专属空间配置器,每次配置一个指针大小
typedef simple_alloc<pointer, Alloc> map_allocator;

// 并提供一个constructor如下:
deque(int n, const value_type& value)
    :start(), finish(), map(0), map_size(0)
{
    // 参见相关源码
    fill_initialize(n, value);
}

注意,在create_map_and_nodes()函数中:

(1)需要节点数 = ( 元素个数 / 每个缓冲区可容纳的元素个数) + 1 ;

(2)一个map要管理几个节点。最少8个,最多是“所需节点数加2”(前后各预留一个,扩充时可用);

(3)nstart和nfinish 指向map所拥有之全部节点的最中央区段,保持在最中央(已初始化节点位于map节点列表最中央区段。),可使头尾两端的扩充能力一样大。每一节点可对应一个缓冲区。

yxt9-P155-图4-13

push_back/push_front 参见相关源码

yxt10-P157-图4-14

yxt11-P159-图4-15

yxt12-P160-图4-16

什么时候map需要重新整治?这个问题的判断由reserve_map_at_back() 和 reserve_map_at_front() 进行,实际操作则由reallocate_map() 执行。

参见相关源码。

6. deque 的元素操作:pop_back, pop_front, clear, erase, insert

泛型算法find() 寻找deque某个元素 yxt13-P162-图-4-17

注意,deque的最初状态(无任何元素时)保有一个缓冲区,因此,clear() 完成之后回复初始状态,也一样要保留一个缓冲区。

参见相关源码

五、stack

1. stack概述

stack是一种先进后出(First In Last Out, FILO)的数据结构。它只有一个出口。stack允许新增元素、移除元素、取得最顶端元素。但除了最顶端外,没有任何其它方法可以存取stack的其他元素。换言之,stack不允许有遍历行为。

2. stack定义 以某种既有容器作为底部结构,将其接口改变,使之符合“先进后出”的特性,形成一个stack,是很容易做到的。deque是双向开口的数据结构,若以deque为底部结构并封闭其头端开口,便轻而易举地形成了一个stack。因此,SGI STL 便以deque作为缺省情况下的stack底部结构,stack的实现因而非常简单,源代码十分简短。 除了deque之外,list也可以作为stack的底部容器。

由于stack系以底部容器完成其所有工作,而具有这种“修改某物接口,形成另一种风貌”之性质者,称为adapter(配接器),因此,STL stack 往往不被归类为container(容器),而被归类为container/adapter。

3. stack没有迭代器 stack 所有元素的进出都必须符合“先进后出”的条件,只有stack顶端的元素,才有机会被外界取用。stack不提供走访功能,也不提供迭代器。

六、queue

1. queue概述

queue 是一种先进先出(First In First Out, FIFO)的数据结构。queue允许新增元素、移除元素、从最底端加入元素、取得最顶端元素。但除了最底端可以加入、最顶端可以取出外,没有任何其他方法可以存取queue的其他元素。换言之,queue不允许有遍历行为。

2. queue定义 queue同样也是一种配接器,也没有迭代器。参见相关源码。

七、heap(隐式表述,implicit representation)

参见STL——heap结构及算法

 八、priority_queue

 

STL——序列式容器

原文:http://www.cnblogs.com/yyxt/p/4979678.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!