STL容器底层实现

发布于 2021-09-16  73 次阅读


1、array 容器

std::array是在C++11中才引入的,就是对普通数组做了包装,添加了相关成员函数,使其符合容器的标准。与内置数组相比,array是一种更安全、更容易使用的数组类型。与内置数组类似,array对象的大小是固定的。因此,array不支持添加和删除元素以及改变容器大小的操作。

总结:是更安全的数组

2、vector 容器

vector 是动态数组。在堆上分配空间。vector 是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。vector的内存空间是连续的,提供O(1)的随机访问。

vector主要关注它的扩容机制,vector为了避免扩容次数过多,选择一次扩容就把空间增大一倍。但同时,vector的内存要保证连续,而直接扩容不能保证后头有足够的连续空间,所以会新开辟一段空间,再把之前的内容复制过来。

同样由于内存空间保证连续,所以对vector的尾部插入元素的性能是最高的。但如果对其他位置插入元素,就会涉及到内存的移动。可能导致内部对象的析构和重新构造,效率最低。

总结:vector 常用来保存需要经常进行随机访问的内容,并且不需要经常对中间元素进行添加删除操作。

3、list 容器

STL 中的list 底层是一个双向链表,而且是一个环状双向链表。所以它可以提供O(1)的元素插入和删除,但不支持随机访问。

总结:通常用于保存较大的数据块,并且要进行频繁的插入删除。

4、deque 容器

deque 是一种双向开口的连续线性空间,元素也是在堆中。与vector不同的是,它在头部进行插入的操作效率也很高。

其底层是许多分段的连续空间,通过内部控制算法维护其整体连续的假象。这些连续空间被称为缓冲区,deque通过维护一个map来组织这些缓冲区。

deque 支持随机访问,速度和vector相差无几,并且在头部插入的时候具有很高的效率,此外在扩容机制上也具有一定优势,因为deque是按照缓冲区来扩容的,提高了内存的利用率。代价就是内部结构较为复杂。

总结:deque 在开始和最后添加元素都一样快,并提供了随机访问方法,像vector一样使用 [] 访问任意元素,但是随机访问速度比不上vector快,因为它要内部处理堆跳转。deque 也有保留空间。另外,由于 deque 不要求连续空间,所以可以保存的元素比 vector 更大,这点也要注意一下。还有就是在前面和后面添加元素时都不需要移动其它块的元素,所以性能也很高。


以上就是STL的所有线性容器,我们在使用容器的时候需要按照需求选择。线性容器提供了线性的访问,如果你想要做一些查找,那只能进行遍历了。需要查找的情况请选择后面提到的非线性容器。


5、容器适配器

STL提供了对基础容器的再封装,以实现某些特定数据结构,包括:stack、queue、priority_queue

此外还有虽然没提供适配器,但提供了相关算法的heap。

stack

stack是一种先进后出(First In Last Out , FILO)的数据结构,其内部要求使用线性容器,默认使用deque 实现,隐藏了对于头部的操作使其一端封口。对于大型数据可以使用list,一般不使用vector,因为其扩容耗时。

queue

queue 是一种先进先出(First In First Out,FIFO)的数据结构。它有两个出口,仅支持从最底端加入元素,取得最顶端元素。其底层一样是要求使用线性容器,默认是 deque 实现 , 对于大型数据可以使用list,一般不使用vector,因为其扩容耗时。

heap

stl没有直接提供适配器,而是在算法库中提供了make_heap,push_heap,pop_heap;通过传入一对线性容器的迭代器和比较函数,就可以构造一个最大/最小堆结构。其中 push_heap需要先将元素插入线性容器的尾端,才能调用该函数维护堆结构。pop_heap也是如此,会把堆顶元素移动到线性容器的尾端并维护堆结构,需要手动调用容器的方法进一步删除。

priority_queue

优先队列是基于上述的最大/最小堆的,其内部默认是一个vector,利用stl的 make_heap,push_heap,pop_heap 来维护堆结构。

这些容器适配器因为需要维护对应的数据结构和操作规则,对外不提供遍历接口!

6、map、set、multi_map、multi_set

所有这些的底层都是红黑树。不同的是map有自己的key,value。而set的key是value本身。

multi_map、multi_set 允许出现相同的键,这里猜测是直接按照一定规则插入,如后插入的比前插入的值大。

7、unordered_map、 unordered_set、 unordered_ multi_ map、 unordered_ multi_ set

这些底层都是哈希表,主要需要了解扩容机制:

首先是计算元素的哈希值,出现哈希碰撞的时候就使用链表来处理。当元素数量超过阈值的时候,触发扩容,把哈希表的地址扩大,元素重新计算哈希值进行重排,这个阈值一般是0.75。

这里对比一下java下的hashmap。java同样是使用链表来处理哈希碰撞,扩容机制也差不多。但如果某个链表的元素超过8个,java就会把它转换为一个红黑树以提高查找效率。删除的时候如果红黑树节点小于8个就又会退化成链表。


以上就是所有的STL关联容器,也提一下哈希表和红黑树的选择场景。哈希表的开销主要在于计算哈希值和对哈希碰撞的处理,由于散列函数不是完全均匀的,也就导致哈希表对内存的利用率较低。优点就是提供常数时间的查找。所以在数据量大,数据规模可知且较为稳定的场景下选择哈希表。对于内存要求比较高,数据规模不定的情况下,使用红黑树可能更好(logN的查找复杂度)。


题外话:cpp20才提供了无序容器的默认比较,不然无序容器是没有办法插入map,set的。


当其他人都认为你要鸽的时候,你鸽了,亦是一种不鸽