STL——序列式容器
一、容器概述与分类 1. STL容器即是将运用最广的一些数据结构实现出来。常用的数据结构有array, list, tree, stack, queue, hash table, set, map……等等。根据“数据在容器中的排列”特性,这些数据结构分为序列式和关联式两种。本篇讨论序列式容器。
这里所谓的衍生,并非派生关系,而是内含关系。例如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指向整块连续空间(含备用空间,容器容量)的尾端。
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操作图如下:
三、List
1. list概述 相较于vector的连续线性空间,list就显得复杂许多,它的好处是每次插入或删除一个元素,就配置或释还一个元素空间。因此,list对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插入或元素移除,list永远是常数时间。
2. list的节点 参见相关源码
3. list的迭代器 list 不再能够像vector一样一普通指针作为迭代器,因为其节点不保证在存储空间中连续存在。list 迭代器必须有能力指向list的节点,并有能力进行正确的递增、递减、取值、成员存取等操作。所谓“list迭代器正确的递增、递减、取值、成员取用”操作是指,递增时指向下一个节点,递减时指向上一个节点,取值时取的是节点的数据值,成员取用时取用的是节点的成员。由于STL list 是一个双向链表,迭代器必须具备前移、后移的能力,所以list提供的是Bidirectional iterators。
list有一个重要性质:插入操作和接合操作都不会造成原有的list迭代器失效。这在vector是不成立的,因为vector 的插入操作可能造成容器重新配置,导致原有的迭代器全部失效。甚至list的元素删除操作,也只有“指向被删除元素”的那个迭代器失效,其他迭代器不受任何影响。
list迭代器的设计,参见相关源码
4. list 的数据结构
SGI list 不仅是一个双向链表,而且还是一个环状双向链表。所以它只需一个指针,便可完整表现整个链表。如果让指针node指向刻意置于尾端的一个空白节点,node便能符合STL 对于“前闭后开”区间的要求,成为last迭代器。
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()。
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容器相关源码;
3. deque 的迭代器
参见deque迭代器相关源码
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节点列表最中央区段。),可使头尾两端的扩充能力一样大。每一节点可对应一个缓冲区。
push_back/push_front 参见相关源码
什么时候map需要重新整治?这个问题的判断由reserve_map_at_back() 和 reserve_map_at_front() 进行,实际操作则由reallocate_map() 执行。
参见相关源码。
6. deque 的元素操作:pop_back, pop_front, clear, erase, insert
泛型算法find() 寻找deque某个元素
注意,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)
八、priority_queue
1. priority_queue 概述
顾名思义,priority_queue是一个拥有权值的queue,其内的元素并非依照被推入的次序排列,而是自动依照元素的权值排列(通常权值以实值表示)。权值最高者,排在最前面。缺省情况下priority_queue系利用一个max-heap完成,后者是一个以vector表现的complete binary tree(参见STL——heap结构及算法)。max-heap可以满足priority_queue所需要的“依权值高低自动递减排序”的特性。
2. 和queue一样,priority_queue 也被归类为container adapter。priority_queue的所有元素,进出都有一定的规则,只有queue顶端的元素(权值最高者),才有机会被外界取用。priority_queue不提供遍历功能,也不提供迭代器。
九、slist
1. slist 概述 STL list 是个双向链表。SGI STL 另提供了一个单向链表,名为slist。这个容器并不在标准规格之内。slist 和 list 的主要差别在于,前者的迭代器属于单向的Forward Iterator, 后者的迭代器属于双向的Bidirectional Iterator。由于slist 是一个单向链表,STL 默认的插入操作“将新元素插入于指定位置之前”对于slist来说并不适用,故而slist 特别提供了insert_after() 和 erase_after()供灵活运用。基于同样的(效率)考虑,slist 不提供push_back(), 只提供push_front()。(因为对于slist, push_back会导致遍历整个链表,才能找到最后一个节点的位置,执行插入;slist 默认提供链表头节点指针,push_front可以快速插入。)
2. slist 的节点 slist 节点和其迭代器的设计,架构上比list复杂许多,运用了继承关系,运用了继承关系,因此在型别转换上有复杂的表现。这种设计方式在第5章RB-tree将再一次出现。如下图:
slist node源码参见相关源码
3. slist 的迭代器
参见相关源码
注意:slist 没有实现operator--,因为这是一个forward iterator。
4. slist 的数据结构
参见相关源码