STL容器由于各自用途的不同,底层实现的数据结构也有所不同。

具体来讲,容器的主要用途就是对其中存储的数据进行“增删改查”。那么不同的数据结构的设计,增删改查的效率是不一样的。

 

下面是《The C++ Standard Library 2nd》的一个截图,

容器水平布局 容器平面图_数据结构与算法

 

我们可以看到,STL容器的类型可以分为两种:序列容器和关联容器

关联容器中又包括不定序容器。不定序容器是指元素在容器中的位置是不确定的,也就是减少了关联容器的排序的过程,对于不需要排序的数据来讲,效率会更高一些。

下面分别讨论下。

容器水平布局 容器平面图_容器水平布局_02

从图中可以看出来,stl中的array是固定大小容器,底层实现是C-Style array。

这个容器和vector和C类型数组的功能类似,那么它的优势在哪?

1、和std::vector相比,std::array是静态数组实现,在编译时就可以确定大小,所以没有vector的容量动态扩充时的消耗。当然也就没有vector灵活。

2、和C-Style array相比,

  std::array保持了和一般STL容器的一致的接口函数,迭代器,这样可以更好的利用STL的算法,在使用上也更为灵活。

  std::array不会隐式转成指针,在作为参数传递时,也不会丢失大小信息。

 

容器水平布局 容器平面图_数据结构与算法_03

底层也是数组,和array不同的是可以动态改变大小。

内存连续。所以可以通过迭代器,指针和偏移量来随机访问。

容量动态扩充。当插入新的元素时,如果空间不足,会重新申请一块2倍于当前容量的内存,并和当前内存空间做交换。原来的指针和引用会失效。

 

 

容器水平布局 容器平面图_数据结构与算法_04

双端队列。从上面这个图可以看出来,deque可以实现两端的插入和删除,同时又支持随机访问。但其实底层实现比上面这张图实现的更复杂。

以下这个图很好的阐明了底层的实现,转自知乎

容器水平布局 容器平面图_底层实现_05

1. deque是由多块不连续的内存chunk组成的,再用一个表来存储索引每一个chunk,数据存储在chunk中,每个chunk的大小是相同的。

    这样设计最重要的原因是:

    同时保证在两端的插入删除随机访问可以在常量时间内完成。

  这其实兼容了vector的随机访问和后面要提到的list常量时间插入删除的优点。

2. deque可以做到动态大小。但是又不像vector需要重新申请内存和内存交换。

3. deque的随机访问需要做两次定位。先查询索引表,再查询chunk中的偏移。

 

std::queue和std::stack是对deque的封装。

 

容器水平布局 容器平面图_数据结构与算法_06

前者双向链表,后者单向链表。

 由于是链表,所以两个容器都可以实现O(1)的任意位置的插入和移除。但都不支持随机访问。

前者提供双向迭代器,同时在空间上占用的更多。

后者只提供在头部插入和删除,空间占用比list更高效,如果只需要在一端操作,forward_list更适合。

 

容器水平布局 容器平面图_底层实现_07

容器水平布局 容器平面图_c/c++_08

关联型容器底层实现都是红黑树

为什么要使用红黑树呢?因为关联型容器对查询的效率要求很高。而红黑树是一种自平衡二叉搜索树(属于二叉搜索树 BST)

我们知道在BST中,左子树小于父节点,右子树大于父节点。所以查询就是遍历节点的过程。如果树的结构是平衡二叉树,也就是节点的高度之间不会大于2,那么查询的复杂度就是O(logn),这还不仅是平均时间复杂度,在最坏的情况下也是如此,因为平衡。

红黑树通过一些规则限制,保证节点在创建,插入和删除后,依然能保持平衡。

所以这些关联型容器的插入,查询,删除的时间复杂度都是O(logn)

后面我要总结一篇红黑树的文章。

 

容器水平布局 容器平面图_容器水平布局_09

 

容器水平布局 容器平面图_容器水平布局_10

这两个也是关联型容器,不同的是内部存储的数据并不按照一定的顺序排列。

为了实现平均时间复杂度O(1)的搜索,插入和删除,底层的实现采用哈希表的方式。

容器水平布局 容器平面图_底层实现_11

链地址法:HashTable由多个bucket组成,bucket以hashkey为索引,bucket里面存储的是相同hashkey的元素。