STL中各类容器详细介绍

本文详细介绍了C++STL中的各种容器,包括vector、list、deque、stack、queue、priority_queue、set、multiset、map和unordered_map,以及它们各自的特点、时间复杂度和适用场景,帮助程序员理解和选择合适的容器设计解决方案。
摘要由CSDN通过智能技术生成

STL介绍

STL(Standard Template Library),即标准模板库,是一个具有工业强度的,高效的C++程序库。它被容纳于C++标准程序库(C++ Standard Library)中,是ANSI/ISO C++标准中最新的也是极具革命性的一部分。该库包含了诸多在计算机科学领域里所常用的基本数据结构和基本算法。为广大C++程序员们提供了一个可扩展的应用框架,高度体现了软件的可复用性。

C++STL提供的数据结构

1. Sequence Containers:维持顺序的容器。

(a). vector:

动态数组,在堆中分配内存,是我们最常使用的数据结构之一。

特点:

  • 底层结构 : 底层由 动态数组 实现 , 特点是 存储空间 连续 ;
  • 访问遍历 : 支持 随机访问迭代器 , 可使用下标访问 , 访问元素非常快 O(1) 复杂度 ;
  • 插入 / 删除 : 尾部插入 / 删除效率高 O(1) 复杂度 ; 中间 和 头部插入/删除效率低 , 由于存储空间连续 , 需要将插入 / 删除位置之后的元素依次改变位置 , O(n) 复杂度 ;
  • 空间效率 : 底层实现时 , 会事先预留一些额外空间 , 以减少重新分配的次数 ;
  • 使用场景 : 需要 随机访问 且 频繁在尾部进行操作 的场景 ; 如果频繁增删元素 则 不适用该容器 ;

时间复杂度:

实现原理:

简单理解,就是vector是利用上述三个指针来表示的,基本示意图如下:
在这里插入图片描述
两个关键大小:
大小:size=_Mylast - _Myfirst;
容量:capacity=_Myend - _Myfirst;
分别对应于resize()、reserve()两个函数。


size表示vector中已有元素的个数,capacity表示vector最多可存储的元素的个数;

为了降低二次分配时的成本,vector实际配置的内存空间大小会比客户需求的更大一些,以备将来扩充,这就是capacity的概念。即capacity>=size,当等于时,容器此时已满,若再要加入新的元素时,就要重新进行内存分配,整个vector的数据都要移动到新内存。

vector:扩容机制:

vector 容器扩容的过程需要经历以下 3 步:

1. 完全弃用现有的内存空间,重新申请更大的内存空间;

2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;

3. 最后将旧的内存空间释放。

因为 vector 扩容需要申请新的空间,所以扩容以后它的内存地址会发生改变。vector 扩容是非常耗时的,为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间(这也就是 vector 容量的由来,即 capacity>=size),以便后期使用。VS里面扩容机制是1.5倍扩容,gcc里面是2倍扩容。


(b). list:

双向链表,也可以当作 stack 和 queue 来使用。有个缺点,链表不支持快速随机读取。

特点

  • 底层结构 : 底层由 双向链表 实现 , 特点是 存储空间 不连续 ;
  • 访问遍历 : 不支持 随机访问迭代器 , 只能通过迭代器进行访问 ;
  • 插入 / 删除 : 任意位置 插入 / 删除 效率都很高 ;
  • 空间效率 : 每个元素 都需要 分配额外的空间 , 存储 当前元素的 前驱元素 和 后继元素 ;
  • 使用场景 : 需要 在任意位置 频繁 插入 / 删除 操作的 场景 ;

时间复杂度:


(c). deque:

双端队列, 是一种优化了的、对序列两端元素进行添加和删除操作的基本序列容器。它允许较为快速地随机访问,但它不像vector 把所有的对象保存在一块连续的内存块,而是采用多个连续的存储块,并且在一个映射结构中保存对这些块及其顺序的跟踪。向deque 两端添加或删除元素的开销很小。它不需要重新分配空间,所以在两端端增删元素比vector 更有效。

特点:

  • 底层结构 : 底层由 双向队列 实现 , 特点是 存储空间 连续 ;
  • 访问遍历 : 支持 随机访问迭代器 , 其性能比 vector 动态素组要低 ;
  • 插入 / 删除 : 头部 和 尾部 插入 / 删除效率高 , O(1) 复杂度 ; 中间 插入/删除效率低 , 由于存储空间连续 , 需要将插入 / 删除位置之后的元素依次改变位置 , 比 vector 动态数组要快一些 ;
  • 空间效率 : 底层实现时比 vector 的结构要复杂 , 也会事先预留一些额外空间 , 以减少重新分配的次数 ;
  • 使用场景 : 需要 随机访问 且 频繁在 首部 和 尾部 进行操作 的场景 ; 如果频繁 在中部 增删元素 则 不适用该容器 ;

实现原理:

deque 是由一段一段的定量的连续空间构成。一旦有必要在 deque 前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在 deque 的头端或者尾端。deque 最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。
既然 deque 是分段连续内存空间,那么就必须有中央控制,维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。

deque 采取一块所谓的 map(不是 STL 的 map 容器)作为主控,这里所谓的 map 是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是 deque的存储空间的主体。

时间复杂度:


(d). array:

固定大小的数组,一般在刷题时我们不使用。


(e). forward_list:

单向链表,一般在刷题时我们不使用。


2. Container Adaptors:容器适配器。

(a). stack:

后入先出(LIFO)的数据结构,默认基于 deque 实现。stack 常用于深度优先搜
索、一些字符串匹配问题以及单调栈问题。


(b). queue:

先入先出(FIFO)的数据结构,默认基于 deque 实现。queue 常用于广度优先
搜索。


(c). priority_queue:

最大值先出的数据结构,默认基于vector实现堆结构。它可以在O(n log n)
的时间排序数组,O(log n) 的时间插入任意值,O(1) 的时间获得最大值,O(log n) 的时
间删除最大值。priority_queue 常用于维护数据结构并快速获取最大或最小值。


3. Associative Containers:有序关联容器。

(a). set:

有序集合,元素不可重复,底层实现默认为红黑树,它可以在 O(n log n) 的时间排序数组,O(log n) 的时间插入、删除、查找任何节点。这里注意,set 和 priority_queue 都可以用
于维护数据结构并快速获取最大最小值,但是它们的时间复杂度和功能略有区别,如
priority_queue 默认不支持删除任意值,而 set 获得最大或最小值的时间复杂度略高,具
体使用哪个根据需求而定。

特点:

  • 底层结构 : 底层由 红黑树 实现 , 红黑树 是 一种 平衡二叉搜索树 , 存储空间 不连续 ;
  • 访问遍历 : 不支持 随机访问迭代器 , 不能通过过下标访问 , 只能通过迭代器进行访问 ;
  • 插入 / 删除 : 查询 / 插入 / 删除 效率 为 O(log n) 复杂度 ;
  • 排序方式 : 默认使用 less 仿函数 , 即 < 运算符进行排序 ; 也可以自定义 排序规则 仿函数 ;
  • 使用场景 : 需要 有序集合 且 元素 不重复 的场景 ;

时间复杂度:

增删改查都近似 = O(log N)


(b). multiset:

支持重复元素的 set。

(c). map:

有序映射或有序表,在 set 的基础上加上映射关系,可以对每个元素 key 存一个
值 value。

特点:

  • 底层结构 : 底层由 红黑树 实现 , 红黑树 是 一种 平衡二叉搜索树 , 存储空间 不连续 ; 存储的 元素 是 键值对 元素 ;
  • 访问遍历 : 不支持 随机访问迭代器 , 不能听过下标访问 , 只能通过迭代器进行访问 ;
  • 插入 / 删除 : 查询 / 插入 / 删除 效率 为 O(log n) 复杂度 ; 与 set 集合容器相同 ;
  • 排序方式 : 默认使用 less 仿函数 , 即 < 运算符进行排序 ; 也可以自定义 排序规则 仿函数 ; map 映射容器 不允许重复的键 , multimap 多重映射容器允许重复的键 ;
  • 使用场景 : 需要 有序 键值对 且 元素 不重复 的场景 ;
std::map 映射容器 与 std::set 集合容器 的区别是
map 容器存储的是 键值对 元素 , 元素是 pair  
set 容器 存储的是 单纯的 键 单个元素 ;

时间复杂度:

增删改查都近似 = O(log N)


(d). multimap:

支持重复元素的 map。


4. Unordered Associative Containers:无序关联容器

(a). unordered_set:

哈希集合,可以在 O(1) 的时间快速插入、查找、删除元素,常用于快
速的查询一个元素是否在这个容器内。


(b). unordered_multiset:

支持重复元素的 unordered_set。


(c). unordered_map:

哈希映射或哈希表,在 unordered_set 的基础上加上映射关系,可以对
每一个元素 key 存一个值 value。在某些情况下,如果 key 的范围已知且较小,我们也
可以用 vector 代替 unordered_map,用位置表示 key,用每个位置的值表示 value。

实现原理

unordered_map 容器和 map 容器一样,以键值对(pair类型)的形式存储数据,存储的各个键值对的键互不相同且不允许被修改。但由于 unordered_map 容器底层采用的是哈希表存储结构,该结构本身不具有对数据的排序功能,所以此容器内部不会自行对存储的键值对进行排序。底层采用哈希表实现无序容器时,会将所有数据存储到一整块连续的内存空间中,并且当数据存储位置发生冲突时,解决方法选用的是“链地址法”(又称“开链法”)。整个存储结构如下图(其中,Pi 表示存储的各个键值对):

可以看到,当使用无序容器存储键值对时,会先申请一整块连续的存储空间,但此空间并不用来直接存储键值对,而是存储各个链表的头指针,各键值对真正的存储位置是各个链表的节点。

为什么会出现哈希冲突:

由于通过哈希函数产生的哈希值是有限的,而数据可能比较多,导致经过哈希函数处理后仍然有不同的数据对应相同的值,这时候就产生了哈希冲突。

哈希冲突具体解决方法:

在正式讲解hash冲突之前,先介绍一个术语:在hashtable中,数组的每一个元素叫做桶(bucket)。

为了解决hash冲突,hashtable在每个桶里bucket[index]不再直接存储待插入节点的值,而是存储一个哨兵节点,使其指向一个链表,由这个链表来存储每次插入节点的值:

•桶的索引值index依然是bucket_index函数的计算方式,即通过待插入节点的键来获取•待插入节点的值在哨兵指向的链表头部插入,由于是头部插入整个插入过程还是O(1)时间复杂度。

当发生hash冲突时,将所有hashcode相同的节点都插入到同一个链表中,如图1所示。由于采用的是头部插入法,那么即便是发生了hash冲突,此时插入时间复杂度也依然是O(1)。

图片

图1  hash冲突的解决方法

上述解决hash冲突的方法,叫做开链法。此时,hash冲突的问题似乎解决了,能够插入多个hashcode一样的节点,并且插入操作的时间复杂度仍然是O(1)。

但是!!!,当出现严重的hash冲突,会造成bucket[idx]指向的链表节点很长,此时搜索和删除一个节点的时间复杂度最坏却可能变成O(N),即哈希表已经退化成链表,那么就违背了一开始设计hashtable的初衷,即弥补数组O(N)的搜索、删除时间复杂度。

 hash退化

 负载因子

为了解决hash退化,引入了两个概念:

•负载因子(load_factor),是hashtable的元素个数与hashtable的桶数之间比值;•最大负载因子(max_load_factor),是负载因子的上限

他们之间要满足:

load_factor = map.size() / map.buck_count() // load_factor 计算方式load_factor <= max_load_factor              // 限制条件

当hashtable中的元素个数与桶数比值load_factor >= max_load_factor时,hashtable就会自动增加桶数,并重新进行哈希扩容,并重新插入,来降低load_factor。

哈希扩容:即使分配一块更大内存,来容纳更多的桶。

重新插入:按照上述插入步骤将原来桶中的buck_size个节点重新插入到新的桶中。

Rehash后,桶数增加了而元素个数不变,再次满足load_factor <= max_load_factor条件。

 Rehash

hashtable,由于要一直满足 load_factor <= max_load_factor ,限制着hash冲突程度,即每个桶的链表节点数不会无限制增加,整个hashtable的节点数达到一定程度就会Rehash,确保hashtable的搜索、删除的平均时间复杂度还是O(1)。

在unordered_map有个rehash函数,可以在任意时候使unordered_map发生Rehash,也可以等load_factor > max_load_factor时自动发生。注意:

•不同编译器的扩容策略不同,因此不编译器Rehash后桶的个数不一致很正常。•目前msvc和g++中的max_load_factor字段默认值都是1。


(d). unordered_multimap:

支持重复元素的 unordered_map。

STL 各容器使用场景示例

  • 如果需要 随机访问 , 则使用 vector 单端数组 或 deque 双端数组 容器 ;
  • 如果 需要 在 尾部 频繁 插入 / 删除 , 则使用 vector 单端数组 ;
  • 如果 需要 在 首部 和 尾部 频繁 插入 / 删除 , 则使用 deque 双端数组 ;
  • 如果 需要 在 任意位置 频繁 插入 / 删除 , 则使用 list 双向链表 ;
  • 如果需要保持 元素 有序 且 不重复 , 则使用 set 集合容器 ;
  • 如果需要保持 元素 有序 且 可重复 , 则使用 multiset 多重集合容器 ;
  • 如果不需要保持 元素 有序 , 可使用unordered_set, unordered_map;


————————————————
引用:

https://blog.csdn.net/zuihaobizui/article/details/119741156

https://blog.csdn.net/shulianghan/article/details/135363350

https://www.cnblogs.com/yinbiao/p/11636405.html

C++ vector的内部实现原理及基本用法_vector内部实现-CSDN博客

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值