可能下面的这些容器类你都见过,甚至每天都在不断使用。
vector, list, forward_list#, deque, priority_queue,
set, multiset, unordered_set#, unordered_multiset#,
map, multimap, unordered_map#, unordered_multimap#,
array#, bitset, stack, queue
带#的容器是在C++11版本才正式进入STL
但是,你真的理解它们的特性,各自的适用场合,以及能够在面对一个实际问题时进行准确的抉择吗?
array, bitset, stack, queue, priority_queue
因为它们的元素数量固定,跟其他容器比较没有意义,所以不深入分析和比较它们。
array
:固定长度的数组。定义方式如下:std::array
;bitset
:固定长度的数组(但每个元素只占1bit
的空间,取值为0
或1
)。定义方式如下:bitset
这些容器没有独立的底层数据结构实现,只是在其他容器的基础上提取出几个特殊的接口,使程序可读性更强。准确地说,它们应该叫做容器适配器,而不是容器。
stack
:后进先出,基本只用到这四个接口,top
,pop
,push
,empty
。内部实现实际上是deque
;queue
:先进先出,基本只用到这四个接口,front
,pop
,push
,empty
。内部实现实际上是deque
;priority_queue
:最大(小)的先出,基本只用到这四个接口,top
,pop
,push
,empty
。内部实现实际上是vector
;因此,接下来我们重点讨论的只剩下12
个容器了。
vector, list, forward_list#, deque,
set, multiset, unordered_set#, unordered_multiset,
map, multimap, unordered_map, unordered_multimap
vector
C语言
/C++
的数组很相似,但能够在必要时自动扩容。list
forward_list
deque
vector
的机制保存起来。与vector
相比,头部数据的插入和删除都很快。顺带一提,这个固定大小的内存块在gcc
中是512字节
。set
,multiset
,map
,multimap
logn
的x
倍(n
是元素数量)。经常使用的是红黑树
,这时x
的值为2
。unordered_set
,unordered_multiset
,unordered_map
, unordered_multimap
hash值
,再分配到对应的位置上。以下面的代码为例
std::vector v[3] = {2, 6, 7};
std::cout << v[1] << std::endl;
像这样子取出第x
个元素,如果对于关联数组(***map
),则为访问键为x
对应的元素所需的时间。
时间从少到多,如下图列出:
容器 | 时间 |
---|---|
vector | O(1) |
deque | O(1) |
unordered_map,unordered_multimap | O(1)(平均),O(n)(最差) |
map,multimap | O(log n) |
list,forward_list | O(n) |
set,multiset,unordered_set,unordered_multiset | 不可能 |
对于vector
,某个特定元素的地址能够根据该元素的大小和下标值准确地计算出来;而deque
也同理,只不过需要计算两次才能最终确定,因此它们都能在O(1)
时间之内访问到该元素。
对于list
和forward_list
,只能从头到尾一个个地慢慢前进,才能最终确定目标元素的内存地址,因此STL
根本就没有为它们重载[]运算符
。
对于关联数组(***map
)的对比讨论放在比较3
再集中说明。
比如,对{2,6,7}
这样的容器,在6之后插入5
、删除6
这样的操作。
容器 | 时间 |
---|---|
vector | O(1)(尾部的删除),O(1)(尾部的插入,平均),O(n)(尾部的插入,最差),O(n)(其它情况) |
deque | O(1)(头部或尾部的删除),O(1)(头部或尾部的插入,平均),O(n)(头部或尾部的插入,最差),O(n)(其它情况) |
list,forward_list | O(1) |
unordered_set,unordered_multiset,unordered_map, unordered_multimap | O(1)(平均),O(n)(最差) |
set,multiset,map,multimap | O(log n) |
list
和forward_list
很高效,因为它们只需要把更改下一两个节点的前后节点指针的值就可以了。但是这是以已经找到要插入的位置为前提(我们一般使用迭代器完成这一任务),这又回到比较1
和比较3
的讨论内容。
对于vector
,如果要在某个位置插入或删除元素,它后面的元素必须重新调整位置。(尾部的插入和删除除外)同时,包括尾部在内的所有插入都有可能导致要进行扩容,进而所有元素都要移动到新内存空间的对应位置。
对于deque
,它相对于vector
在头部的删除和插入十分高效,因为只需调整第一个内存块(固定大小)的元素的位置即可。必要时可以增加(或减少)一个内存块,然后调整存放内存块地址的vector
,因此仍需要O(n)
的时间耗费,但相对于vector
要重新调整全部元素,它的速度仍然要快很多。
对于unordered***
来说,由于哈希冲突和哈希表扩容,因此最坏情况会到达O(n)
,但只要哈希函数设计得合理,再加上估计哈希表的合理大小预先分配,仍能使总体的平均时间保持为O(1)
,因此实际运用中其性能往往比平均消耗时间为O(log n)
的set
,multiset
,map
,multimap
要好。
这里所谓的查找某一个元素,仅仅指以下的应用场景:对于一个{2, 6, 7}
这样的容器,询问元素6
是否存在,若存在则把它找出来给我。因此对于***map
来说,并不是以键为查找的基准(这已经在比较1
进行了分析),而是以值为查找的基准。
容器 | 时间 |
---|---|
unordered_set, unordered_multiset | O(1)(平均),O(n)(最差) |
set,multiset | O(log n) |
vector,deque,list,forward_list,map,multimap,unordered_map,unordered_map | O(n) |
但是对于已经排好序的vector
和deque
,可以使用二分查找,从而将查找成本将为O(log n)
。
以下将讨论在gcc
的编译环境下:元素个数为n
;对于一般容器ST
=sizeof(单个元素)
,对于关联数组(***map
)ST
= sizeof(单个值) + sizeof(单个键)
;SP
=sizeof(void*)
,即一个指针所占的内存空间大小。
内存占用从小到大,如下图列出:
容器 | 时间 |
---|---|
vector | nST + 3SP(最好/平均情况,刚好塞满),n2ST + 3*SP(最坏情况,空了一半) |
deque | n*(ST + 2ST/512SP) + 6*SP(GCC4.5.2环境下) |
forward_list | (n+1)*(ST+SP) + SP |
list | (n+1)(ST+2SP) + SP |
unordered_set,unordered_multiset |
n*(ST+2SP) + 2SP(最好/平均情况,n/容量≈负载因子),n*(ST+3SP) + 2SP(最坏情况,n/容量<<负载因子) |
set,multiset,map |
(n+1)(ST+3SP) + SP(仅作估算) |
为了插入和删除节点方便,list
和forward_list
增加了一个空的头结点,即使容器内没有元素,仍需要ST
+SP
的空间来存放这个头结点,因此才会出现那个诡异的(n+1)
对于vector
,每增加一个元素,不需要额外的指针,因此为n*(ST)
;
对于forward_list
,每增加一个元素,需要增加一个指针,因此为(n+1)*(ST+SP)
;
对于list
,每增加一个元素,需要增加两个指针,因此为(n+1)*(ST+2*SP)
;
对于set
等,每增加一个元素,需要增加三个指针(分别指向左儿子、右儿子、父节点),因此为(n+1)*(ST+3*SP)
;
如果你看不太懂unordered_set
的空间复杂度中,下面这幅图应该能帮到你:
为什么vector
在最后要加3*SP
?这三个指针分别用于①指示第一个元素的内存位置;②指示最后一个元素的内存位置,对应于size()
方法;③指示最后一个可容纳元素的内存位置,对应于capacity()
方法。
为什么deque
在最后要加6*SP
?首先是两个指针记录首个内存块和最后一个内存块的地址,为了加速头部和尾部的操作,也要记录首个内存块的起始和终止位置、最后一个内存块的起始和终止位置,这样一共6
个指针。
对于有利的因素,文字会加粗。
容器 | 1.访问特定位置元素的时间 | 2.插入/删除的时间 | 3.查找某个元素的时间 | 4.内存占用量 |
---|---|---|---|---|
vector | O(1) | O(1)(尾部),O(n)(其他) | O(n) | 0~ST |
deque | O(1) | O(1)(头部或尾部),O(n)(其他) | O(n) | 2ST/512SP |
list | O(n) | O(1) | O(n) | 2*SP |
forward_list | O(n) | O(1) | O(n) | SP |
unordered_set,unordered_multiset | 不可能 | O(1) | O(1) | 2*SP |
set,multiset | 不可能 | O(log n) | O(log n) | 3*SP |
unordered_map,unordered_multimap | O(1) | O(1) | O(n) | 2*SP |
map,multimap | O(log n) | O(log n) | O(n) | 3*SP |