C++ STL容器分析

C++ STL

在这里插入图片描述

STL 中容器分为序列式容器容器、关联式容器、容器适配器三种类型,三种类型容器特性分别如下:
1.序列式容器 容器并非排序的,元素的插入位置同元素的值无关,包含 vector、deque、list。 - vector:动态数组 元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。但由于分配是连续的内存空间,不适合对数组任意位置插入或者删除操作。

  • deque:双向队列 元素在内存连续存放。随机存取任何元素都能在常数时间完成(仅次于 vector )。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。
  • list:双向链表 元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。

2.关联式容器 元素是排序的;插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现,包含set、multiset、map、multimap。

  • set/multiset set中不允许相同元素,multiset 中允许存在相同元素。
  • map/multimap map 与 set 的不同在于 map 中存放的元素有且仅有两个成员变,一个名为 first,另一个名为 second,map 根据 first 值对元素从小到大排序,并可快速地根据 first 来检索元素。map 和multimap 的不同在于是否允许相同 first 值的元素。

3.容器适配器 封装了一些基本的容器,使之具备了新的函数功能,包含 stack、queue、priority_queue。

  • stack:栈 栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最进插入序列的项(栈顶的项),后进先出,先进后出

STL 中常用的容器有 vector、deque、list、map、set、multimap、multiset、unordered_map、unordered_set 等。

各容器的时间复杂度分析

容器底层实现方式及时间复杂度分别如下:
1.vector 采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为: 插入: O(N) 查看: O(1) 删除: O(N)
2.deque 采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为: 插入: O(N) 查看: O(1) 删除: O(N)
3.list 采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为: 插入: O(1) 查看: O(N) 删除: O(1)
4.map、set、multimap、multiset 上述四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为: 插入: O(logN) 查看: O(logN) 删除: O(logN)
5.unordered_map、unordered_set、unordered_multimap、 unordered_multiset 上述四种容器采用哈希表实现,不同操作的时间复杂度为: 插入: O(1),最坏情况O(N) 查看: O(1),最坏情况O(N) 删除: O(1),最坏情况O(N) 注意:容器的时间复杂度取决于其底层实现方式。

各容器的底层实现

vector

在这里插入图片描述
vector类似于数组,和array的区别在于array是固定长度的连续内存空间,而vector是动态数组,可以在尾端进行插入删除的操作。vector拥有一段连续的内存空间,并且起始地址不变。可以用于高效的进行随机存取,时间复杂段是O(1)。因为内存空间是连续的,所以在进行插入和删除的操作的时候,会造成内存块的拷贝,时间复杂度是0(n)。
另外vector最大的特点就是可以动态增长。当数组中的内存不后的时候,会重新申请一块内存空间并进行内存拷贝。支持对数组的高效率访问。

vector的扩容机制:

  1. 完全弃用现有的内存空间,重新申请更大的内存空间
  2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中; (将旧内存的数据拷贝到新内存空间中)
  3. 最后将旧的内存空间释放。 因为 vector 扩容需要申请新的空间,所以扩容以后它的内存地址会发生改变。vector 扩容是非常耗时的,为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间

下面来分析一下vector的底层实现:
vector的迭代器有三个指针:start,finish,end_of_storage;分别表示容器的开头,元素的结尾,以及容器的结尾;当finish=end_of_storage也就是我们所说的vector的 size=capacity 的时候,就说明此时的容器已经满了,再加入元素的话,就要进行扩容。
在这里插入图片描述
在这里插入图片描述
新增大内存空间大小是原来旧数组的2倍。这个还是要取决于编译器,windows下vs2019在源码里实际上是1.5倍。这样能更节省成本。

VS2019
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
看到了么?扩容是1.5倍。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
思想是一样的。

剩下重点分析一下deque容器

Deque

在这里插入图片描述
在这里插入图片描述
deque是双端队列。deque 是由一段一段的定量的连续空间构成。一旦有必要在 deque 前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在 deque 的头端或者尾端。deque 最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。

既然 deque 是分段连续内存空间,那么就必须有中央控制,维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。

deque 采取一块所谓的 map(不是 STL 的 map 容器)作为主控,这里所谓的 map 是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区(buffer)。缓冲区才是 deque的存储空间的主体。然后主要是通过两个迭代器start,finish来控制连续性。通过重载++,–运算符来实现迭代器的移动,实现空间的连续性。

Deque的内部是由一个指向map(一小块连续的内存空间)的指针,记录map size的变量,两个迭代器start,finish四部分组成。在两个迭代器里分别有记录开头位置的first,结尾位置的last,当前位置的cur,以及指向map结点的指针node四部分组成。

在这里插入图片描述
在这里插入图片描述
deque的两个迭代器
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们看一下当插入元素的时候,是怎么实现的?
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
deque是怎么模拟连续空间的呢?
主要是通过运算符的重载;
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
移动位置时候,首先需要判断是否在同一个缓冲区中,如果不在同一个缓冲区中,那么首先计算buffer的大小来判断应该处于哪个正确的Buffer上,然后再计算buffer上正确的位置。
在这里插入图片描述

stack和queue

这两个称为适配器,因为底层都是封装了deque,实际上也可以用list代替,但是人家源码就是用的deque那肯定有人家的道理,估计是快吧。
在这里插入图片描述

关联式容器

STL中关联容器包括set,map,multiset,multimap;底层的实现是个红黑树,是平衡二叉搜索树。

红黑树的特点:
1、具有二叉查找树的特点;
2、根节点是黑色的;
3、每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存数据;
4、任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
5、每个节点,从该节点到达其可达的叶子节点是所有路径,都包含相同数目的黑色节点。
在这里插入图片描述
在源码中,对于红黑树的插入结点,主要是两个函数insert_unique(){保证唯一性,不允许重复},insert_equal(){可以重复插入}。看到这两个函数应该就可以猜出set/multiset,map/multimap的底层区别了吧。

set 的实现

set 底层使用红黑树实现,一种高效的平衡检索二叉树。 set 容器中每一个元素就是二叉树的每一个节点,对于 set 容器的插入删除操作,效率都比较高,原因是二叉树的删除插入元素并不需要进行内存拷贝和内存移动,只是改变了指针的指向。 对 set 进行插入删除操作 都不会引起迭代器的失效,因为迭代器相当于一个指针指向每一个二叉树的节点,对 set的插入删除并不会改变原有内存中节点的改变。 set 中的元素都是唯一的,而且默认情况下会对元素进行升序排列。不能直接改变元素值,因为那样会打乱原本正确的顺序,要改变元素值必须先删除旧元素,再插入新元素。不提供直接存取元素的任何操作函数,只能通过迭代器进行间接存取。

map的实现
  1. map 实现原理 map 内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而 AVL是严格平衡二叉搜索树),红黑树有自动排序的功能,因此 map 内部所有元素都是有序的,红黑树的每一个节点都代表着 map 的一个元素。因此,对于 map 进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map 中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值,使用中序遍历可将键值按照从小到大遍历出来。
  2. 各操作的时间复杂度 插入: O(logN) 查看: O(logN) 删除: O(logN)

在这里插入图片描述

unordered_map/unordered_map

底层采用的是哈希表存储结构,该结构本身不具有对数据的排序功能,所以此容器内部不会自行对存储的键值对进行排序。底层采用哈希表实现无序容器时,会将所有数据存储到一整块连续的内存空间中,并且当数据存储位置发生冲突时,解决方法选用的是“链地址法”(又称“开链法”)。整个存储结构如下图(其中,Pi 表示存储的各个键值对):
在这里插入图片描述

在这里插入图片描述
当使用无序容器存储键值对时,会先申请一整块连续的存储空间,但此空间并不用来直接存储键值对,而是存储各个链表的头指针,各键值对真正的存储位置是各个链表的节点。在这里插入图片描述

  1. 在bucket的数量上,内置了28个质数【53,97,193…】,在创建哈西表的时候,会根据存入的元素个数选择大于或等于元素个数的质数作为哈希表的容量(vector的长度)。
  2. 当元素的个数超过了Bucket的个数之后,就要进行rehash操作,找到下一个质数,创建新的bucket vector,重新计算新哈希表的位置。
  3. 通过哈希函数会得到一个哈希值,然后将哈希值H与bucket的数量N做整除运算,得到的结果就是新的bucket的编号。
  4. 建立新的结点存储,然后链接到相应编号的Bucket上。

期待后面STL的补充。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值