C++ STL 序列容器

C++ STL也就是C++标准模板库,它是C++标准库的一部分。STL的目的是标准化组件,它实现了常用的数据结构和算法。本次主要讲下STL的几个序列容器。

1、vector

数据结构:一段连续的线性内存空间,元素连续存放。

通过分析源码,vector就是使用了3个迭代器来表示的:

源码片段:位置指针
vector结构示意图

通过灵活运用以上三个迭代器,vector容器可以轻松的实现诸如首尾标识、大小、容器、空容器判断等几乎所有功能,如:

源码片段:成员函数

vector元素的插入:

push_back在最后插入数据
insert指定位置插入

vector元素的移除和插入同理,需要将指定元素移除后,指定元素后面的数据往前移动覆盖数据,当然,移除最后一个元素时,不需要移动拷贝数据。

结论:

1、有一段连续的内存空间,并且起始地址不变,因此能够很好的支持随即存取,即[]取值操作符;

2、由于内存是连续的,在中间进行插入和删除时会造成内存块的拷贝,另外,当vector的内存空间不够时,需要重新申请一块足够大的内存并进行数据的重新拷贝,极其影响效率。

提升效率的方法:

1、预先分配足够大小的内存,避免因频繁的执行扩容影响效率;

2、尽量使用在最后面插入新元素的方法;即尽量使用push_back,不用或少用insert、erase方法。

2、list

数据结构:双向链表


list数据结构


list结构源码
list插入过程


list插入源码


list删除过程


list删除源码

总结:

1、具有vector和deque容器所不具备的优势,它可以在常规时间内,在序列已知的任何位置插入或删除元素。

2、无法通过位置来直接访问序列中的元素,即,不能索引元素。为了访问list内部的一个元素,必须一个一个的从第一个或最后一个开始遍历元素。

3、deque

数据结构:双向队列,可以在头尾两端插入或删除元素;

形象说来,Map中控器和缓存区的关系就类似于一本字典,字典以拼音首字母为索引,拼音首字母对应字典的页数范围就类似于Map中控器节点指向一段缓存区。假如要加入新的首字母,则又在字典插入一段该首字母对应的页数,同时将页数范围记录到字典首字母对应的页数中。

Deque数据结构

迭代器里面含有4个成员,连续空间开始地址(first),结束地址(last),空间中当前元素的地址(cur)以及连续空间地址在map中的位置(node)。这里的map不是stl里的map,而是一小块连续空间,其中每一个元素都是指针,指向另一段(较大的)连续线性空间(deque储存空间的主体),当map空间已满,需要再找一块更大的空间来作为map。first、last指向node所指缓冲区的头和尾,标示出缓冲区的边界,当iterator++(node向右移动)或iterator--(由node向左移动)到边界时,为了保持其连续线,需要跳到下一个缓冲区。

map中控器

1、map中控器的大小如何确定?

map管理的节点个数是求max(8, 所需节点数+2),其中8是map的默认节点数;

所需节点数算法:元素个数/缓存区能存储的元素数 + 1;(缓存区默认是512字节的大小)

以要存储的是20个int类型的元素为例:

缓存区能存储 size = 512字节/sizeof(int) = 128个int类型的元素;

所需节点数nodeNum = 20/size + 1 = 1个节点;

则map管理的节点个数max(8, 1+2)即8个节点。

申请的map空间不够时,也需要重新配置更大的空间,将原来map里的指针拷贝过来,最后释放原来的空间。

扩容新map的分配规则:旧map.size() + max(旧map.size(),新增节点数) + 2.

若新增节点数为5,旧map的大小为8,则扩容后map的大小为:

8 + max(8,5) + 2 = 20;

2、deque数据的插入、删除是怎样的?

两端插入与删除:

以push_front为例,叙述头端插入过程:(画流程图太麻烦,直接以步骤的方式展现)

push_front(x)步骤

以pop_front为例,叙述头端删除过程:(画流程图太麻烦,直接以步骤的方式展现)


pop_front(x)步骤


后端插入删除push_back()、push_front()步骤和前述同理,理解了前端就能理解后端的,不再赘述。

双端插入和删除不需要对已有元素进行移动,因此效率非常高。

中间插入与删除:


insert(x)步骤

中间删除步骤和插入步骤类似,不再赘述。

注意:在中间插入和删除元素时,会导致指向deque元素的任何pointer、iterator失效。但是,deque的内存重分配优于vector,因为其内部结构不需要复制所有元素。

3、deque数据是否支持随机访问元素?

支持,它可以像vector一样使用[]访问任意元素,但是随机访问速度比不上vector快,因为它要内部处理堆跳转。

总结:

1、deque与vector最大的差异就是:deque是分段连续线性空间,随时可以增加一段新的空间;而vector是当内存不够时,需要重新分配、复制数据、释放原始空间。

2、比较优劣:

vector是可以快速地在最后添加删除元素,并可以快速地访问任意元素。

list是可以快速地在所有地方添加删除元素,但是只能快速地访问最开始与最后的元素

deque在开始和最后添加元素都一样快,并提供了随机访问方法。

由于deque不要求连续空间,所以可以保存的元素比vector更大,在前面和后面添加元素时都不需要移动其它块的元素,所以性能也很高。

3、如何选择:

在实际使用时,如何选择这三个容器中哪一个,应根据你的需要而定,一般应遵循下面的原则:

(1)如果你需要高效的随即存取,而不在乎插入和删除的效率,使用vector;

(2)如果你需要大量的插入和删除,而不关心随即存取,则应使用list;

(3)如果你需要随即存取,而且关心两端数据的插入和删除,则应使用deque;

4、queue

数据结构:一种先进先出的队列;底层容器默认为deque容器,list也可以实现,是单端和双端的区别。(严格来说,queue其实是适配器,而不是容器,因为它是对容器的再封装)

入队出队

总结:

1、必须从一个口元素入队,另一个口元素出队。

2、不能随机存取,不支持遍历。

5、stack

数据结构:后进先出的栈;底层容器默认为deque容器,list也可以实现,是单端和双端的区别。(严格来说,stack其实是适配器,而不是容器,因为它是对容器的再封装)

stack和“撤销”操作是一样的,操作入栈,撤销时,最先出栈的不就是刚刚进行的操作吗。


入栈出栈

总结:

1、必须从同一个口入栈出栈。

2、不能随机存取,不支持遍历。

6、priority_queue

数据结构:不论先进后进,优先级最高者先出的队列。但是如何定义“优先级”取决于我们,比如在医院,病情最严重的得到最先救治,操作系统进程调度中,优先级最高的得到优先调用。其内部使用的是堆排序。

想具体了解堆排序的推荐看下:

https://www.cnblogs.com/chengxiao/p/6129630.html

说下相关概念:

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

大顶堆小顶堆

先写个简单的例子:

priority_queue<Type, Container, Functional>,模板申明带3个参数,其中Type 为数据类型,Container为保存数据的容器,Functional 为元素比较方式。Container必须是用数组实现的容器,比如vector, deque等等,但不能用 list(原因是堆排序是通过数组构建的堆)。STL里面默认用的是vector。

大顶堆排序


小顶堆排序

为了方便理解,提出这样一个需求:公司微波炉热饭顺序按年龄来确定热饭优先级,年长者优先热饭。

热饭类,重写operator<方法


年长者优先热饭

总结:

 1、priority_queue只允许访问最顶端元素(priority_queue.top),即堆顶元素;

 2、priority_queue内部采取的最大堆的算法,每次弹出的元素都是优先级最高的元素,并且加入或弹出元素,内部元素都需要重新调整结构,即排序;

 3、由于需要计算优先级,所以如果不是基本数据类型,则必须重载operator确定优先级。


参考链接:

https://juejin.im/post/5c9de926f265da30b53eb970

https://www.cnblogs.com/xiaogege/archive/2013/04/06/STL_deque.html

http://blog.chinaunix.net/uid-13909379-id-4902042.html

//www.greatytc.com/p/65fdd3099238

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容