C++ STL容器时间复杂度下的最佳选择

引言

今天一个电话面试的HR问了我许多问题,当问到我关于STL的时间复杂度的问题,我就开始支支吾吾了。
要知道,一般情况下对于STL的使用,我只是概念上的了解他们的优劣,但是在使用上还是比较随性的,除非遇到频繁操作的数据结构才考虑具体该用哪种STL,但到用的时候向来都是google 百度的。

还是备一备吧,不然也应付不了HR。

简介

STL在C++11中还算是火热,想必大家早有耳闻,对于泛型编程而言,或者数据结构而言,STL都显得尤为重要。今天让我们来了解一下,根据时间复杂度这个条件,挑选最适合自己程序的STL。



容器类型

  连续内存的容器:这种类型容器包含vector、deque。特点是在一块连续的内存块上存放数据,所以有数据插入和删除的时候,如果不是在序列的 或者两端那么花费的代价是非常大的,因为需要保证连续内存,同时给新元素腾出空间或者填充删除元素的空间,如果存储的是复杂结构的话就要花费大量的时间进行拷贝操作(可以存储复杂结构的指针来弥补这个缺陷,这个讨论在另个总结中进行)。

  基于节点的容器:这类容器是剩余的几个list、set、multiset、map、multimap.这类容器中的数据是分别存储在不同的内存块中,可能连续也可能不连续(一般不认为是连续的),这样的容器在插入删除元素的时候修改的只是节点的指针,这样的消耗是非常小的。

使用注意

  在使用的过程中,需要考虑的问题有元素顺序、标准的一致性、迭代器能力、内存布局和C的兼容性、查找速度这些,考虑了这些问题你选择的容器应该会非常适合你当前的情景。

  1. 需要大量添加新元素:

      vector在大量添加元素的时候问题最大,因为他的一种最常见的内存分配实现方法是当前的容量(capacity)不足就申请一块当前容量2倍的新内存空间,然后将所有的老元素全部拷贝到新内存中,添加大量元素的时候的花费的惊人的大。如果由于其他因素必须使用vector,并且还需要大量添加新元素,那么可以使用成员函数reserve来事先分配内存,这样可以减少很多不必要的消耗。

      list对这种情况的适应能力就非常好,都是常数时间的插入消耗。deque前面说过了,他是vector和list的折衷形式,内存不够了就申请一块新的内存,但并不拷贝老的元素。

  2. 查找速度:

      这个因素主要取决于算法,而算法最终是作用在容器中元素上的,所以这里的查找速度指的是容器能够达到的最好查找效率。

      对于序列容器需要分两种情况,区分依据是元素是否排序,1)对于已经排序的序列容器,使用binary_search、lower_bound、upper_bound、equal_range可以获得对数时间复杂度的查找速度(O(logN));2)而未排序的序列容器二分查找肯定是用不了,能达到的最好的时间复杂度是线性的(O(n))。

      对于关联容器,存储的时候存储的是一棵红黑树(一种更为严格的平衡二叉树,文档最后有介绍),总是能达到对数时间复杂度(O(logN))的效率,因为关联容器是按照键值排好序的。

  3. 是否是连续内存:

      连续内存的容器有个明显的缺点,就是有新元素插入或老元素删除的时候,为了给新元素腾出位置或者填充老元素的空缺,同一块内存中的其他数据需要进行整体的移位,这种移位的拷贝代价有时是非常巨大的。标准容器中的vector、deque是连续内存的,其中vector是完全连续内存,而deque是vector和list的折衷实现,是多个内存块组成的,每个块中存放的元素连续内存,而内存块又像链表一样连接起来。

      所以需要考虑在操作的过程中是否有在任意位置插入元素的需求,有这种需求的话尽量避免使用连续内存的vector、deque

  4. 元素的排序:

      序列容器中的元素不会自动排序,程序员插入什么顺序内存中就是什么顺序,而关联容器不是这样的,他会以自己的键值按照某种等价关系(equivalence)进行排序。所以默认情况下序列容器中的元素是无序的,而关联容器中的元素是有序的。

      所以容器在遍历元素的时候序列容器输出的顺序和插入的顺序式一致的,关联容器就不一定了。下面给出两个例子:

      通过例子看到序列容器vector遍历的顺序和插入的顺序是一样的,而关联容器set把插入的元素按照某种顺序重新组织了,所以选择容器的时候如果很在意插入顺序的话就选择序列容器。

  5. 内存是否和C兼容:

      适合的容器只有一个vector,意思就是如果需要把容器中的数据放到C类型的数组中那么不需要做多余复杂的操作,如果有vector v,只需要直接使用&v[0]就可以得到v中第一个元素的指针,因为vector和C数组的内存布局是一样的,这个要求同时也是标准C++委员会制定的标准。所以能保证有这样特性的容器只有vector,那么vector以外的其他STL容器中的数据如果需要变换成C数组形式,或者C数组放到其他类型容器中,可以把vector作为一个桥梁,下面给个例子:

//假设函数void read(const int* pInt, unsigned int num);

//从pInt指针位置开始读取num个int型数据

std::set<int> temp_set;

... //省略给temp_set插入元素的操作

std::vector<int> temp_vector(temp_set.begin(), temp_set.end());

if (!temp_vector.empty())

read(&temp_vector[0], temp_vector.size());

容器优缺点

  用哪种容器的选择看起来非常繁琐,头脑中如果有个每个容器大概的模型,在选择的时候会更为轻松点。

  1. Vector的数据模型就是数组。

     优点:内存和C完全兼容、高效随机访问、节省空间

     缺点:内部插入删除元素代价巨大、动态大小查过自身容量需要申请大量内存做大量拷贝。

  2. List 的数据结构模型是链表

     优点:任意位置插入删除元素常量时间复杂度、两个容器融合是常量时间复杂度

     缺点:不支持随机访问、比vector占用更多的存储空间

  3. Deque的数据模型是数组和链表的折衷:

     优点:高效随机访问、内部插入删除元素效率方便、两端push pop

     缺点:内存占用比较高

  4. Map、set、multimap、multiset的数据结构模型是二叉树(红黑树)

     优点:元素会按照键值排序、查找是对数时间复杂度、通过键值查元素、map提供了下标访问

容器特性

容器类型容器特性
vector典型的序列容器,C++标准严格要求次容器的实现内存必须是连续的唯一可以和标准C兼容的stl容器,任意元素的读取、修改具有常数时间复杂度,在序列尾部进行插入、删除是常数时间复杂度,但在序列的头部插入、删除的时间复杂度是O(n),可以 在任何位置插入新元素,有随机访问功能,插入删除操作需要考虑
deque典型的序列容器,C++标准严格要求次容器的实现内存必须是连续的,唯一可以和标准C兼容的stl容器,任意元素的读取、修改具有常数时间复杂度,在序列尾部进行插入、删除是常数时间复杂度,但在序列的头部插入、删除的时间复杂度是O(n),可以 在任何位置插入新元素,有随机访问功能,插入删除操作需要考虑
list典型的序列容器,C++标准严格要求次容器的实现内存必须是连续的,唯一可以和标准C兼容的stl容器,任意元素的读取、修改具有常数时间复杂度,在序列尾部进行插入、删除是常数时间复杂度,但在序列的头部插入、删除的时间复杂度是O(n),可以 在任何位置插入新元素,有随机访问功能,插入删除操作需要考虑
set关联容器,元素不允许有重复,数据被组织成一棵红黑树,查找的速度非常快,时间复杂度是O(logN)
multiset关联容器,和set一样,却别是允许有重复的元素,具备时间复杂度O(logN)查找功能。
map关联容器,按照{键,值}方式组成集合,按照键组织成一棵红黑树,查找的时间复杂度O(logN),其中键不允许重复
multimap和map一样,区别是键可以重复

C++支持情况

STL

点击查看大图

总结

1) 如果需要随机访问,用vector

2) 如果存储元素的数目已知,用vector

3) 需要任意位置随机插入删除,用list

4) 只有需要更多在容器的首部尾部插入删除元素,用deque

5) 元素是复杂结构用list,也可以用vector存储指针(需要额外的精力去维护内存),看需求

6) 如果操作是基于键值,用set map

7) 如果需要经常的搜索,用map set

8) map set 的区别是map中的元素都是pair

本文章参考STL容器的适用情况,对该文章进行了补充,希望能够帮助到读者。

容器适用情况

点击查看大图

  • 18
    点赞
  • 79
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
目录 1 前面的话 1 1.1 历史 1 1.2 内容 1 1.3 基础知识 1 1.4 三种境界 1 1.5 STL特点 1 1.6 资源 1 1.7 学习方法 1 2 语言特性 3 2.1 模板 3 2.2 typename 3 2.3 模板类的拷贝构造函数 3 2.4 基本类型数据初始化 3 2.5 异常处理 4 2.6 命名空间 4 2.7 using声明 4 2.8 namespace std 4 2.9 explicit关键字 5 2.10 新的类型转换符 5 2.11 静态常量成员的初始化 6 2.12 时间复杂度O记号 6 3 一般概念 7 3.1 头文件 7 3.2 错误处理和异常处理 7 3.2.1 异常头文件 7 3.2.2 标准异常分类 7 3.2.3 异常规格 8 3.2.4 如何在程序中找出异常类型 8 3.2.5 抛出标准异常和实现自己的异常 8 3.3 配置器 8 4 通用工具 9 4.1 简介 9 4.1.1 类别 9 4.1.2 头文件 9 4.2 Pairs 9 4.2.1 简介 9 4.2.2 示例 9 4.3 auto_ptr 10 4.3.1 作用 10 4.3.2 引入原因 10 4.3.3 声明 10 4.3.4 auto_ptr拥有权的转移 10 4.3.5 示例 11 4.3.6 用途 12 4.4 数值极限 13 4.4.1 引入原因 13 4.4.2 头文件 13 4.4.3 numeric_limits<> 13 4.5 辅助函数 14 4.5.1 max、min 14 4.5.2 swap 15 4.6 头文件<cstddef>、<cstdlib> 15 4.6.1 <cstddef> 15 4.6.2 <cstdlib> 15 5 STL标准程序库 16 5.1 STL组件 16 5.1.1 分类 16 5.1.2 基本观念 16 5.1.3 好处 16 5.2 容器(containers) 16 5.2.1 分类 16 5.2.2 序列式容器示例 16 5.2.3 关联式容器 18 5.3 迭代器 18 5.3.1 示例 19 5.3.2 迭代器分类 21 5.4 算法 21 5.4.1 区间 22 5.4.2 处理多个区间 22 5.5 迭代器的配接器 24 5.5.1 种类 24 5.5.2 Insert Insertors 24 5.5.3 Stream Iterator 25 5.5.4 reverse iterator 25 5.6 变动型算法 26 5.6.1 删除元素 26 5.6.2 变动型算法和关联式容器 27 5.6.3 算法vs.成员函数 28 5.7 使用者自定义的泛型函数 29 5.8 以函数作为算法的参数 29 5.8.1 示例for_each和transform 29 5.8.2 判断式(predicates) 30 5.9 仿函数 33 5.9.1 什么是仿函数 33 5.9.2 预先定义的仿函数 35 5.10 容器内的元素<class T> 36 5.10.1 容器元素的条件 36 5.10.2 value和reference 37 5.11 STL内部的错误处理和异常处理 37 5.11.1 错误处理(Error Handling) 37 5.11.2 异常处理 38 5.12 扩展STL 38 6 STL容器 39 6.1 容器的共同能力 39 6.2 vector 39 6.2.1 vector的能力 39 6.2.2 vector实例 40 6.3 deque 41 6.3.1 Deque的能力 41 6.3.2 Deque的使用时机 41 6.3.3 示例 41 6.4 List 42 6.4.1 list的能力 42 6.4.2 list的操作函数 42 6.4.3 splice函数 43 6.4.4 示例 43 6.5 set和multiset 44 6.5.1 示例 44 6.5.2 set和multiset举例 46 6.6 map和multimap 49 6.6.1 map和multimap的能力 50 6.6.2 将map视为关联式数组 52 6.6.3 map和multimap运用示例 52 6.6.4 综合示例 55 6.7 其它STL容器 57 6.7.1 HashTable 59 6.7.2 引用计数 59 6.8 各种容器的运用时机 61 6.8.1 各种容器的使用时机 61 7 STL迭代器 64 7.1 迭代器头文件 64 7.2 迭代器类型 64 7.2.1 Input迭代器 64 7.2.2 Output迭代器 64 7.2.3 Forward迭代器 65 7.2.4 双向迭代器 65 7.2.5 随机存取迭代器 65 7.2.6 Vector迭代器的递增和递减 67 7.3 迭代器辅助函数 67 7.3.1 advance()可令迭代器前进 67 7.3.2 distance()可处理迭代器之间的距离 68 7.3.3 iter_swap()交换两个迭代器所指内容 68 7.4 迭代器配接器(adapter) 69 7.4.1 逆向迭代器 69 7.4.2 Insert迭代器 72 7.4.3 Stream迭代器 75 7.5 迭代器特性 76 8 STL仿函数 77 8.1 仿函数概念 77 8.1.1 仿函数当做排序准则 77 8.1.2 拥有内部状态的仿函数 78 8.1.3 for_each()的返回值 80 8.1.4 判断式和仿函数 81 8.2 预定义的仿函数 82 8.2.1 函数配接器 82 8.2.2 针对成员函数而设计的函数配接器 83 9 STL算法 85 9.1 算法头文件 85 9.2 算法概览 85 9.2.1 简介 85 9.2.2 算法分类 85 9.3 辅助函数 85 9.4 for_each()算法 86 9.5 非变动性算法 88 9.5.1 元素计数 88 9.5.2 最小值和最大值 88 9.5.3 搜寻元素 89 9.5.4 区间的比较 95 9.6 变动性算法 98 9.6.1 复制元素 98 9.6.2 转换和结合元素 99 9.6.3 互换元素内容 101 9.6.4 赋予新值 101 9.6.5 替换元素 103 9.7 移除性算法 104 9.7.1 移除某些特定元素 104 9.7.2 移除重复元素 105 9.8 变序性算法 107 9.8.1 逆转元素次序 107 9.8.2 旋转元素次序 107 9.8.3 排列元素 109 9.8.4 重排元素 109 9.8.5 将元素向前搬移 110 9.9 排序算法 111 9.9.1 对所有元素排序 111 9.9.2 局部排序 112 9.9.3 根据第n个元素排序 113 9.9.4 heap算法 114 9.10 已序区间算法 115 9.10.1 搜寻元素 115 9.10.2 合并元素 117 9.11 数值算法 120 9.11.1 加工运算后产生结果 120 9.11.2 相对值和绝对值之间的转换 121 10 特殊容器 123 10.1 Stacks 123 10.1.1 核心接口 123 10.1.2 Stack运用实例 123 10.1.3 使用自定义的Stack类 124 10.2 Queue 125 10.2.1 核心接口 126 10.2.2 Queue运用实例 126 10.2.3 使用者自定义的队列 126 10.3 Priority Queue 128 10.3.1 核心接口 128 10.3.2 运用实例 128 10.4 Bitset 129 10.4.1 Bitset运用实例 129 11 Strings 131 11.1 动机 131 11.1.1 示例:引出一个临时文件名 131 11.1.2 例二:引出一段文字并逆向打印 132 11.2 未提供的操作函数 132 11.2.1 大小和容量 132 12 数值 135 12.1 复数 135 12.2 valarray 136 12.2.1 认识valarray 136 12.2.2 valarray的子集 138 13 以stream classes完成输入/输出 143 13.1.1 示例 143 13.2 基本的Stream类别和Stream对象 144 13.3 文件存取 144 13.3.1 重定向 147 13.3.2 用于读写的Stream 147 13.4 String Stream class 148 13.4.1 Stream缓冲区迭代器示例 14 国际化 150149 15 空间配置器

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值