如果写c++程序那么STL容器是不可避免要使用的,而正确合理地使用这些容器才能够简化我们的程序、提高运行的效率,所以这篇我们就来简单介绍下STL容器内部的故事。
一、顺序容器
顺序容器:是一种各元素之间有顺序关系的线性表,是一种线性结构的可序群集。顺序性容器中的每个元素均有固定的位置,除非用删除或插入的操作改变这个位置。顺序容器的元素排列次序与元素值无关,而是由元素添加到容器里的次序决定。
1、vector
vector应该是我们使用的最多的stl容器了。它的本质是动态数组,在堆中分配内存(allocator),元素连续存放,有保留内存,如果减少大小后,内存也不会释放;如果新值大于当前大小时才会重新分配内存。
本质上vector就是数据结构里的顺序表:
顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指用一组地址连续的存储单元依次存储线性表中的各个元素、使得线性表中在逻辑结构上相邻的数据元素存储在相邻的物理存储单元中,即通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系,采用顺序存储结构的线性表通常称为顺序表。顺序表是将表中的结点依次存放在计算机内存中一组地址连续的存储单元中。
vector有以下特点:
- 随机存取快速 o(1)
- 删除插入中间元素需要移动其他元素 o(n)
- 新增元素如果超过当前容量首先尝试扩容两倍
- erase并不会删除内存,此时vector容量不会减少
- clear也并不会删除内存,只是把size置为0(可以使用这种方法清除V的内存:vector<int>().swap(V);)
- vector的内存扩充实际上是申请一个更大的内存,之后将原内存处数据拷贝过去。
2、list
vector虽然能够支持随机读取,但是插入删除效率过低,这时我们就需要使用list。
list的数据结构为双向链表,元素也存放在堆中,每个元素都是放在一块内存中,他的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点使得它的随机存取变得非常没有效率,因此它没有提供[]操作符的重载。但是由于链表的特点,它可以很有效率的支持任意地方的删除和插入操作。
本质上list就是数据结构里的链表:
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入删除的时候可以达到O(1)的复杂度。
list有以下特点:
- 每分配一个元素都会从内存中分配,每删除一个元素都会释放它占用的内存
- 快速的插入删除o(1)
- 不支持随机存取
3、deque
deque 即双端队列,支持随机读取(效率比vector低),与快速的头尾插入删除(同链表),较好的内部插入删除(效率比vector高)。
deque的内存模型相比于vector与list要复杂许多,它不像vector 把所有的对象保存在一块连续的内存块,而是采用多个连续的存储块,并且在一个映射结构中保存对这些块及其顺序的跟踪。向deque 两端添加或删除元素的开销很小,它不需要重新分配空间。
具体模型见下图:
deque通过维护一个_map(一段连续内存类似vector内存),里面存有分段缓冲区的地址指针,缓冲区(也是一段连续的内存)是deque的储存空间主体,每次插入删除可以只在较小的缓冲区进行,于是这样既能支持随机读取,又能保证插入删除的效率。
deque的特点:
- 在开始和最后添加删除元素一样快,并且提供了随机访问的方法;
- 每一个缓冲区大小是一定的不存在扩充,而map的大小扩充类似于vector;
4、stack
stack为栈,这里先介绍一下栈的定义:
栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
可以看到栈本质上也是一种线性结构,只不过它只能在一端进行删除插入的操作,也不存在随机存取。
在stl里stack可以以vector、list、deque为底层模型构建(默认为deque)。
stack的特点:
- 先进后出(FILO)的数据结构
- 以某种既有的容器(vector、list、deque)作为底层结构
5、queue
queue为队列,这里介绍一下队列的定义:
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
同stack一样,queue也是一种操作受限的顺序表,在stl里queue也是以既有的容器(默认为deque)为底层结构定义的。
queue的特点:
- 先进先出(FIFO)的数据结构
- 以某种既有的容器(vector、list、deque)作为底层结构
6、priority_queue
priority_queue为优先队列,这里介绍一下优先队列:
普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。
例如:压入顺序为4,1,5,2,3时,以小优先的弹出顺序为1,2,3,4,5.
优先队列底层可以用vector(默认)或deque来实现(注意list不能用来实现queue,因为list的迭代器不是任意存取iterator,而pop中用到堆排序时是要求randomaccess iterator 的!)。简单来说priority_queue内存结构还是一个简单的线性表,不过利用了堆排序(具体可以百度这里不展开讲了)的性质,实现了最高级先出的行为特征。
priority_queue的特点:
- 最高级先出
- 以某种既有的容器(vector、deque)作为底层结构
- 默认为小优先队列
二、关联类容器
1、map/set
map/set为关联类容器,map中元素的结构为K-V(键值),set为K(键),它们均不允许两个元素有相同的键值,并且所有元素会根据元素的键值自动排序。
为了实现上述表现,STL底层使用了红黑树(同样 感兴趣可以问问度娘)来构造map/set,红黑树能够以O(log2(N))的时间复杂度进行搜索、插入、删除操作。
默认的排序顺序是非降序。
2、unordered_map/unordered_set
这两与map/set功能类似,但内部所存的元素是无序的。
STL底层使用了哈希表的结构来实现这两个容器。
这里介绍一下哈希表:
哈希表是根据关键码值而进行直接访问的数据结构,通过相应的哈希函数(也称散列函数)处理关键字得到相应的关键码值,关键码值对应着一个特定位置,用该位置来存取相应的信息,这样就能以较快的速度获取关键字的信息。比如:现有公司员工的个人信息(包括年龄),需要查询某个年龄的员工个数。由于人的年龄范围大约在[0,200],所以可以开一个200大小的数组,然后通过哈希函数得到key对应的key-value,这样就能完成统计某个年龄的员工个数。而在这个例子中,也存在这样一个问题,两个员工的年龄相同,但其他信息(如:名字、身份证)不同,通过前面说的哈希函数,会发现其都位于数组的相同位置,这里,就涉及到“冲突”。准确来说,冲突是不可避免的,而解决冲突的方法常见的有:开发地址法、再散列法、链地址法(也称拉链法)。例如unordered_set内部解决冲突采用的是----链地址法,当用冲突发生时把具有同一关键码的数据组成一个链表。下图展示了链地址法的使用:
哈希表实际上是一种采取了用空间换时间的方式,来加快了搜索、插入、删除的效率。
所以和vector类似哈希表也存在扩容的情况,不过每次扩容都需要重新计算哈希地址。
三、STL容器的通性
- 向STL容器中插入对象,均是拷贝原对象。
- STL容器均不是线程安全的