STL-序列式容器

STL_序列式容器

所谓序列式容器,其中的元素都可序,但未必有序。C++语言本身提供一个序列式容器array。STL提供vector、list、deque、stack、queue、priority-queue等等。

vector

vector的数据安排和操作与array非常相似。两者区别在于空间的运用的灵活性。array是静态空间,一旦配置了就不能改变;要换个大(或小)一点的房子,一切琐细得由客户端自己来:首先配置一块新空间,然后将元素从旧址一一搬往新址,再把原来的空间释还给系统。vector是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。

vector实现技术:关键在于其对大小的控制以及重新配置时的数据移动效率。扩充空间需“配置新空间,数据移动,释还旧空间”。vector维护的是一个连续线性空间。它以两个迭代器的start和finish分别指向配置得来的连续空间中目前已被使用的范围,并以迭代器end_of_storage指向整块连续空间(含备用空间)的尾端。它支持随机存取操作。为了降低空间配置时的速度成本,vector实际配置的大小可能比客户端要求量更大一些,以备将来可能的扩充。vector的容量永远大于或等于其大小。一旦容量等于大小,便是满载,下次再有新增元素,整个vector需另觅居处。使用start,finish,end_of_storage三个迭代器,可轻易提供首尾标示、大小、容量、空容器判断、标注([])运算子、最前端元素值、最后端元素值等。

vector中,当新增加元素s时,如果超过当时的容量,则容量会扩充至两倍,如果两倍容量不足,就扩张至足够大的容量。扩充涉及到:重新配置、元素移动、释放原空间等操作。注意:vector中的动态增加大小,并不是在原空间之后接续新空间(因为无法保证原空间之后尚有可供配置的空间),而是以原大小的两倍另外配置一块较大空间,然后将原内容拷贝过来,然后才开始在原内容之后构造新元素,并释放原空间。故对vector的任何操做,一旦引起空间重新配置,指向原vector的所有迭代器就都失效。

list

list与vector的连续空间相比,复杂一些,它的好处是每次插入或删除一个元素,就配置或释放一个元素空间。故list对于空间的运用有绝对的精准,一点也不浪费,并对任何位置的元素插入或元素移除,list永远是常数时间。

list和vector的使用依据元素的多少,元素的构造复杂度,元素的存取行为的特性来决定。

list是利用双向链表实现的。list不能像vector一样,以普通指针作为迭代器,因为其节点不保证在存储空间中连续存在。list的递增、递减、取值、成员取用等操作是指,递增时指向下一个节点,递减时指向上一个节点,取值时取得是节点的数据值,成员取用时取用的是节点的成员。

list的重要性质是:插入操作和接合操作都不会造成原有list迭代器失效。删除操作,也只是指向被删除元素的那个迭代器失效,其他迭代器不受影响。list不仅是一个双向链表,还是一个环状双向链表。list实现环状链表只需一个标记,便可完全表示整个链表。只要刻意在环状链表的尾端加上一个空白节点,便符合前闭后开区间。

list中插入操作与链表的插入原理一样,即为创建新节点,将其插入正确的位置即可。list中实现的操作主要包括:push_back、push_front、erase、pop_front、clear、remove、unique、splice、merge、reverse、sort等等。

deque

vector是单向开口的连续线性空间,deque则是一种双向开口的连续线性空间。所谓双向开口,指可以在头尾两端分别做元素的插入和删除操作。vector当然也可以在头尾两端进行操作,但是其头部操作的效率极差,无法被接受。与deque区别:1)deque允许于常数时间内对起头端进行元素的插入或移除操作;2)deque没有所谓的容量概念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。deque没必要提供所谓的空间保留功能。由于deque迭代器不是简单的普通指针,所以使用时尽可能选择使用vector而非deque,对deque排序,为提高效率,可将deque先复制到vector中,然后对vector排序,然后在复制到deque中。

deque是由一段一段的定量连续空间构成。一旦有必要在deque的前端或尾端增加新空间,便配置一段定量连续空间,串接在整个deque的头端和尾端。deque的最大任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的接口。避开了“重新配置、复制、释放”的轮回,代价则是复杂的迭代器架构。deque实现非常复杂,因为是分段连续线性空间,就必须有中央控制,而为了维护整体连续的假象,数据结构的设计及迭代器前进后退等操作都颇为繁琐。

deque采用一块map作为主控。map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指针,指向另一段(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的存储空间主体。SGI允许我们制定缓冲区大小,默认值0表示将使用512 bytes缓冲区。map其实是一个T**,也就是说它是一个指针,所指之物又是一个指针,指向型别为T的一块空间。当map使用率已经满载,便需要再找一块更大的空间来作为map。

deque是分段连续空间。维持其整体连续假象的任务,落在了迭代器operator++和operator–连个运算子身上。deque迭代器应该具有的结构:1)首先,它必须能够指出分段连续空间(亦即缓冲区)在哪里;2)其次它必须能够判断自己是否已经处于其所在缓冲区的边缘,如果是,一旦前进或后退时就必须跳跃至下一个或上一个缓冲区,为了能够正确跳跃,deque必须随时掌握管控中心(map)。

deque的操作主要包括:push_back,push_front,pop_back,clear,erase,insert等。

stack

stack是一种先进后出的数据结构。他只有一个结构。stack允许新增元素,移除元素,取得最顶端元素。但除最顶端外,没有任何其它方法可以存取stack的其他元素。stack的形成依靠deque,deque是双向开口的数据结构,若以deque为底部结构并封闭其头端开口,便轻而易举地形成了一个stack。由于stack系以底部容器完成其所有工作,而具有这种“修改某物接口,形成另一种风貌”之性质者,称为adapter(配接器)。故stack往往不被归类为container(容器),而被归类为container adapter(容器配接器)。

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

除deque之外,list也是双向开口的数据结构。stack源码中使用的底层容器的函数有empty,size,back,push_back,pop_back,凡此种种,list都具备。若以list为底部结构并封闭其头端开口,一样能够轻而易举形成一个stack。

queue

queue是一种先进先出的数据结构。他有两个出口。queue允许新增元素,移除元素,从最底端加入元素,取得最顶端元素。但除了最底端可以加入、最顶端可以取出外,没有任何其它方法可以存取queue的其它元素。queue不允许有遍历行为。将元素推入queue称为push,将元素推出queue称为pop。

queue实现利用deque,deque是双向开口的数据结构,若以deque为底部结构并封闭其底端的出口和前端的入口,便轻而易举地形成了一个queue。由于queue系底部容器完成其所有工作,而具有这种“修改某物接口,形成另一种风貌”之性质者,称为adapter(配接器)。在STL中queue不被归类为container(容器),而被归类为container adapter(容器配接器)。

queue没有迭代器。queue所有元素的进出必须符合先进先出条件,只有queue顶端元素,才有机会被外界取用,queue不提供遍历功能,也不提供迭代器。由于list也是双向开口的数据结构,故其也可以实现queue。

heap

heap是priority queue的助手。priority heap允许用户以任何次序将任何元素推入容器中,但取出时一定是从优先权最高(也就是数值最高)的元素开始取。binary max heap正是具有这样的结构,适合作为priority heap的底层机制。binary heap是实现priority heap的适当候选。

所谓binary heap就是一种complete binary tree(完全二叉树),即整棵binary tree除了最底层的叶节点之外,是填满的,而最底层的叶节点由左至右又不得有空隙。

完全二叉树整棵树内没有任何节点漏洞,这带来一个极大的好处:可以利用array来存储所有节点,将array的#0元素保留,这样完全二叉树的某个节点位于array的i处时,其左子节点必位于2i处,右子节点必位于array的2i+1处,其父节点位于i/2处,从而可以使用array简单实现完全二叉树。以array表示tree的方式,被称为隐式表述法。

一个array和一组heap算法(用来插入元素,删除元素,取极值,将某一整数数据排列成一个heap)。array缺点是无法动态改变大小,而heap却需要此功能,故vector代替array是更好的选择。

heap分为max-heap和min-heap。max-heap指每个节点的键值(key)都大于或等于其子节点键值,后者的每个节点键值(key)都小于或等于其子节点键值。max-heap的最大值在根节点,并总是位于底层array或vector的起头处,min-heap的最小值在根节点,亦总是位于底层array或vector的起头处。

heap算法

1)push_heap算法

为满足完全二叉树条件,新加入的元素一定要放在最下一层作为叶子节点,并填补在由左至右的第一个空格,即新元素插入在底层vector的end()处。插入新元素后,为了满足max-heap条件(每个节点的键值都大于或等于其子节点键值),需执行所谓的percolate up(上溯)程序:将新节点拿来与父节点比较,如果其键值(key)比父节点大,就父子对换位置。如此一直上溯,直到不需对换或直到根节点为止。

2)pop_heap算法

因为max-heap,最大值必然在根节点。pop操作取走根节点(其实是设置底部容器vector的尾部节点)后,为了满足完全二叉树条件,必须割舍最小层的最右边的叶子节点,并将其重新安插至max-heap,然后调整heap结构。为满足max-heap次序特性(每个节点的键值大于或等于其子节点键值),需执行所谓的percolate down(下溯)程序:将空间节点和其较大子节点“对调”,并持续下放,直到叶节点为止。然后将前述被割舍之元素值设给这个“已经达叶层的空洞节点”,再对它执行一次percolate up(上溯)程序,便可以。pop_heap之后,最大元素只是被置放于底部容器的最尾端,尚未被取走。如果要取其值,可使用底部容器(vector)所提供的back()操作函数。如果要移除它,可使用底部容器(vector)所提供的pop_back()操作函数。

sort_heap算法

既然每次pop_heap可获得heap中键值最大的元素,如果持续对整个heap做pop_heap操作,每次将操作范围从后向前缩减一个元素(因为pop_heap会把键值最大的元素放在底部容器的最尾端),当整个程序执行完毕时,便有一个递增序列。

make_heap算法

make_heap算法用来将一段现有的数据转化为一个heap。

heap没有迭代器。heap的所有元素都必须遵循特别的完全二叉树排列规则,所以heap不提供遍历功能,也不提供迭代器。

priority_queue

priority_queue是一个拥有权值观念的queue,它允许加入新元素,移除旧元素,审视元素值等功能。由于它是一个queue,所以只允许从底端加入元素,并从底端去除元素,除此之外别无其他存取元素的途径。priority_heap带有权值,所以内部元素并非依照被推入的次序排列,而是自动依照元素的权值排列(通常权值是实值表示)。权值最高者,排在最前面。priority_queue使用max-heap完成。max-heap可以满足priority_queue所需要的“依权值高低自动递减排序”的特性。

priority_queue利用vector和heap规则实现,故为容器适配器。priority_queue没有迭代器。所有元素进出都有一定的规则,只有queue顶端的元素(权值最高者),才有机会被外界取用。priority_queue不提供遍历,也不能提供迭代。

slist

STL list是个双向链表。SGI STL另提供一个单向链表,为slist。slist和list主要差别:slist的迭代器属于单向的Forward Iterator,而list的迭代器属于双向的Bidirectional Iterator。slist的功能受一定限制,但是单向链表耗用的空间更小,某些操作更快,不失为另一种选择。list和slist共有特色是插入、移除、接合等操作并不会造成原有的迭代器失效,而指向被移除元素的那个迭代器,在移除操作发生之后肯定会失效。slist的插入操作必须从头节点找起,然后执行插入或删除操作等。slist在插入和删除上不如list。slist提供insert_after()和erase_after(),并提供push_front()。slist的元素次序会和元素插入进来的次序相反。

参考文献:STL源码剖析,侯捷

你可能感兴趣的:(STL)