序列化容器
1、vector
vector使用线性连续空间。增加新元素 (s) 时,如果超过当时的容量,则容量会扩充至两倍。如果两倍容量仍不足,就扩张至足够大的容量。容量的扩张必须经历「重新配置、元素搬移、释放原空间」等过程,工程浩大。
当我们以 push_back() 将新元素安插于 vector 尾端,该函式首先检查是否还有备用空间?如果有就直接在备用空间上建构元素,并调整迭代器 finish ,使 vector变大。如果没有备用空间了,就扩充空间(重新配置、搬移数据、释放原空间)
TIPS:所谓动态增加大小,并不是在原空间之后接续新空间(因为无法保证原空间之后尚有可供配置的空间),而是以原大小的两倍另外配置一块较大空间,然后将原内容拷贝过来,然后才开始在原内容之后建构新元素,并释放原空间。因此,对 vector 的任何操作,一旦引起空间重新配置,指向原 vector 的所有迭代器就都失效了。这是程序员易犯的一个错误,务需小心。
2、list
相较于 vector 的连续线性空间, list 就显得复杂许多,它的好处是每次安插或删除一个元素,就配置或释放一个元素空间。因此, list 对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素安插或元素移除, list 永
远是常数时间。list 和 vector 是两个最常被使用的容器。什么时机下最适合使用哪一种容器,必须视元素的多寡、元素的构造复杂度(有无 non-trivial copy constructor, non-trivial
copy assignmen operator)、元素存取行为的特性而定。
list的数据结构
(首尾相连的双向链表)
SGI list 不仅是一个双向串行,而且还是一个环状双向串行。所以它只需要一个指标,便可以完整表现整个串行。只要
刻意在环状串行的尾端加上一个空白节点,便符合 STL规范之「前闭后开」区间。
list的迭代器
list 不再能够像 vector 一样以原生指标做为迭代器,因为其节点不保证在储存空间中连续存在。 list 迭代器必须有能力指向 list 的节点,并有能力做正确的递增、递减、取值、成员存取…等动作。所谓「 list 迭代器正确的递增、递减、取值、成员取用」动作是指,递增时指向下一个节点,递减时指向上一个节点,取值时取的是节点的资料值,成员取用时取用的是节点的成员。由于STL list 是一个双向串行(double linked-list),迭代器必须具备前移、后
移的能力。所以 list 提供的是Bidirectional Iterators。
deque
vector 是单向开口的连续线性空间, deque 则是一种双向开口的连续线性空间。所谓双向开口,意思是可以在头尾两端分别做元素的安插和删除动作.
deque 和 vector 的差异:
deque 和 vector 的最大差异,一在于 deque 允许于常数时间内对起头端进行元素的安插或移除动作,二在于 deque 没有所谓容量( capacity )观念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。
换句话说,像 vector 那样「因旧空间不足而重新配置一块更大空间,然后复制元素,再释放旧空间」这样的事情在 deque 是不会发生的。也因此, deque 没有必要提供所谓的空间保留( reserve )功能。虽然 deque 也提供Ramdon Access Iterator,但它的迭代器并不是原生指标,其
复杂度和 vector 不可以道里计(稍后看到源码,你便知道),这当然在在影响了各个运算层面。因此,除非必要,我们应尽可能选择使用 vector 而非 deque 。对deque 进行的排序动作,为了最高效率,可将 deque 先完整复制到一个 vector身上,将 vector 排序后(利用 STL sort 算法),再复制回 deque 。deque 系由一段一段的定量连续空间构成。一旦有必要在 deque 的前端或尾端增加新空间,便配置一段定量连续空间,串接在整个 deque 的头端或尾端。 deque 的最大任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的界面。避开了「重新配置、复制、释放」的轮回,代价则是复杂的迭代器架构。
deque 采用一块所谓的map(注意,不是 STL 的 map 容器)做为主控。这里所谓map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指标,指向另一段(较大的)连续线性空间,称为缓冲区。缓冲区才是 deque 的储存空间主体。SGI STL允许我们指定缓冲区大小,默认值 0表示将使用 512 bytes缓冲区。
deque 除了维护一个先前说过的指向map的指标外,也维护 start, finish 两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素(的下一位置)。此外它当然也必须记住目前的map大小。因为一旦map所提供的节点不足,就必须重新配置更大的一块map。
stack
stack 是一种先进后出(First In Last Out,FILO)的数据结构。它只有一个出口, stack 允许新增元素、移除元素、取得最顶端元素。但除了最顶端外,没有任何其它方法可以存取 stack 的其它元素。换言之 stack 不允许有走访行为。
将元素推入 stack 的动作称为 push,将元素推出 stack 的动作称为pop。
以某种既有容器做为底部结构,将其接口改变,使符合「先进后出」的特性,形
成一个 stack ,是很容易做到的。 deque 是双向开口的数据结构,若以 deque 为底部结构并封闭其头端开口,便轻而易举地形成了一个 stack 。因此,SGI STL 便以 deque 做为预设情况下的 stack 底部结构, stack 的实作因而非常简单,源码十分简短,本处完整列出。由于 stack 系以底部容器完成其所有工作,而具有这种「修改某物接口,形成另一种风貌」之性质者,称为adapter(配接器),因此 STL stack 往往不被归类为 container(容器),而被归类为container adapter。
除了 deque 之外, list 也是双向开口的数据结构。上述 stack 源码中使用的底层容器的函式有 empty, size, back, push_back, pop_back ,凡此种种 list都具备。因此若以 list 为底部结构并封闭其头端开口,一样能够轻易形成一个stack 。
stack没有迭代器。
queue
queue 是一种先进先出(First In First Out,FIFO)的数据结构。它有两个出口, queue 允许新增元素、移除元素、从最底端加入元素、取得最顶端元素。但除了最底端可以加入、最顶端可以取出,没有任何其它方法可以存取queue的其它元素。换言之 queue 不允许有走访行为。将元素推入queue 的动作称为 push,将元素推出 queue 的动作称为pop。以某种既有容器为底部结构,将其接口改变,使符合「先进先出」的特性,形成一个 queue ,是很容易做到的。 deque 是双向开口的数据结构,若以 deque 为底部结构并封闭其底端的出口和前端的入口,便轻而易举地形成了一个 queue。
除了 deque 之外, list 也是双向开口的数据结构。上述 queue 源码中使用的底层容器的函式有 empty, size, back, push_back, pop_back ,凡此种种 list都具备。因此若以 list 为底部结构并封闭其头端开口,一样能够轻易形成一个
queue 。
由于 queue 系以底部容器完成其所有工作,而具有这种「修改某物接口,形成另
一种风貌」之性质者,称为adapter(配接器),因此 STL queue 往往不被归类
为 container(容器),而被归类为container adapter。
heap:
heap 并不归属于STL 容器组件,它是个幕后英雄,扮演 priority queue 的推手。顾名思义, priorityqueue 允许使用者以任何次序将任何元素推入容器内,但取出时一定是从优先权最高(也就是数值最高)之元素开始取。 binary max heap 正是具有这样的特性,适合做为 priority queue 的底层机制。
priority_queue
顾名思义, priority_queue 是一个拥有权值观念的 queue ,它允许加入新元素、
移除旧元素,审视元素值等功能。由于这是一个 queue ,所以只允许在底端加入
元素,并从顶端取出元素,除此之外别无其它存取元素的途径。
priority_queue 带有权值观念,其内的元素并非依照被推入的次序排列,而是自动依照元素的权值排列(通常权值以实值表示)。权值最高者,排在最前面。预设情况下 priority_queue 系利用一个 max-heap 完成,后者是一个以 vector表现的 complete binary tree(4.7 节)。max-heap 可以满足 priority_queue 所需要的「依权值高低自动递增排序」的特性。
priority_queue没有迭代器
priority_queue 的所有元素,进出都有一定的规则,只有 queue 顶端的元素(权值最高者),才有机会被外界取用。 priority_queue 不提供走访功能,也不提供迭代器。
slist:
STL list 是个双向串行(double linked list)。SGI STL 另提供了一个单向串行
(single linked list),名为 slist 。这个容器并不在标准规格之内,不过多做一
些剖析,多看多学一些实作技巧也不错,所以我把它纳入本书范围。
slist 和 list 的主要差别在于,前者的迭代器属于单向的 Forward Iterator,后
者的迭代器属于双向的Bidirectional Iterator。为此, slist 的功能自然也就受到
许多限制。不过,单向串行所耗用的空间更小,某些动作更快,不失为另一种选
择。
slist 和 list 共同具有的一个相同特色是,它们的安插(insert)、移除(erase)、
接合(splice)等动作并不会造成原有的迭代器失效(当然啦,指向被移除元素的
那个迭代器,在移除动作发生之后肯定是会失效的)。
注意,根据STL的习惯,安插动作会将新元素安插于指定位置之前,而非之后。然而做为一个单向串行, slist 没有任何方便的办法可以回头定出前一个位置,因此它必须从头找起。换句话说,除了 slist 起始处附近的区域之外,在其它位置上采用 insert 或 erase 操作函式,都是不智之举。这便是 slist 相较于 list之下的大缺点。为此, slist 特别提供了 insert_after() 和 erase_after() 供弹性运用。基于同样的(效率)考虑, slist 不提供 push_back() ,只提供 push_front() 。因此slist 的元素次序会和元素安插进来的次序相反。