什么是容器
首先,我们必须理解一下什么是容器,在C++ 中容器被定义为:在数据存储上,有一种对象类型,它能够持有其他对象或指向其他对像的指针,这样的对象类型就叫做容器。非常easy,容器就是保存其他对象的对象,当然这是一个朴素的理解,这样的“对象”还包括了一系列处理“其他对象”的方法,由于这些方法在程序的设计上会常常被用到,所以容器也体现了一个优点,就是“容器类是一种对特定代码重用问题的良好的解决方式”。
容器还有还有一个特点是容器能够自行扩展。在解决这个问题时我们经常不知道我们须要存储多少个对象,也就是说我们不知道应该创建多大的内存空间来保存我们的对象。显然,数组在这一方面也力不从心。容器的优势就在这里,它不须要你预先告诉它你要存储多少对象,仅仅要你创建一个容器对象,并合理的调用它所提供的方法,全部的处理细节将由容器来自身完毕。它能够为你申请内存或释放内存,而且用最优的算法来运行您的命令。
容器是随着面向对象语言的诞生而提出的,容器类在面向对象语言中特别重要,甚至它被觉得是早期面向对象语言的基础。在如今差点儿全部的面向对象的语言中也都伴随着一个容器集,在C++ 中,就是标准模板库(STL )。
和其他语言不一样,C++ 中处理容器是採用基于模板的方式。标准C++ 库中的容器提供了多种数据结构,这些数据结构能够与标准算法一起非常好的工作,这为我们的软件开发提供了良好的支持!
通用容器的分类
STL 对定义的通用容器分三类:顺序性容器、关联式容器和容器适配器。
顺序性容器 是一种各元素之间有顺序关系的线性表,是一种线性结构的可序群集。顺序性容器中的每一个元素均有固定的位置,除非用删除或插入的操作改变这个位置。这个位置和元素本身无关,而和操作的时间和地点有关,顺序性容器不会依据元素的特点排序而是直接保存了元素操作时的逻辑顺序。比方我们一次性对一个顺序性容器追加三个元素,这三个元素在容器中的相对位置和追加时的逻辑次序是一致的。
关联式容器 和顺序性容器不一样,关联式容器是非线性的树结构,更准确的说是二叉树结构。各元素之间没有严格的物理上的顺序关系,也就是说元素在容器中并没有保存元素置入容器时的逻辑顺序。可是关联式容器提供了还有一种依据元素特点排序的功能,这样迭代器就能依据元素的特点“顺序地”获取元素。
关联式容器还有一个显著的特点是它是以键值的方式来保存数据,就是说它能把keyword和值关联起来保存,而顺序性容器仅仅能保存一种(能够觉得它仅仅保存keyword,也能够觉得它仅仅保存值)。这在以下详细的容器类中能够说明这一点。
容器适配器 是一个比較抽象的概念, C++的解释是:适配器是使一事物的行为类似于还有一事物的行为的一种机制。容器适配器是让一种已存在的容器类型採用还有一种不同的抽象类型的工作方式来实现的一种机制。事实上仅是发生了接口转换。那么你能够把它理解为容器的容器,它实质还是一个容器,仅仅是他不依赖于详细的标准容器类型,能够理解是容器的模版。或者把它理解为容器的接口,而适配器详细採用哪种容器类型去实现,在定义适配器的时候能够由你决定。
下表列出STL 定义的三类容器所包括的详细容器类:
标准容器类 | 特点 |
顺序性容器 | |
vector | 从后面高速的插入与删除,直接訪问不论什么元素 |
deque | 从前面或后面高速的插入与删除,直接訪问不论什么元素 |
list | 双链表,从不论什么地方高速插入与删除 |
关联容器 | |
set | 高速查找,不同意反复值 |
multiset | 高速查找,同意反复值 |
map | 一对多映射,基于keyword高速查找,不同意反复值 |
multimap | 一对多映射,基于keyword高速查找,同意反复值 |
容器适配器 | |
stack | 后进先出 |
queue | 先进先出 |
priority_queue | 最高优先级元素总是第一个出列 |
vector ,deque 和 list
顺序性容器:
向量 vector :
是一个线性顺序结构。相当于数组,但其大小能够不预先指定,而且自己主动扩展。它能够像数组一样被操作,因为它的特性我们全然能够将vector 看作动态数组。
在创建一个vector 后,它会自己主动在内存中分配一块连续的内存空间进行数据存储,初始的空间大小能够预先指定也能够由vector 默认指定,这个大小即capacity ()函数的返回值。当存储的数据超过分配的空间时vector 会又一次分配一块内存块,但这种分配是非常耗时的,在又一次分配空间时它会做这种动作:
首先,vector 会申请一块更大的内存块;
然后,将原来的数据复制到新的内存块中;
其次,销毁掉原内存块中的对象(调用对象的析构函数);
最后,将原来的内存空间释放掉。
假设vector 保存的数据量非常大时,这种操作一定会导致糟糕的性能(这也是vector 被设计成比較easy拷贝的值类型的原因)。所以说vector 不是在什么情况下性能都好,仅仅有在预先知道它大小的情况下vector 的性能才是最优的。
vector 的特点:
(1) 指定一块如同数组一样的连续存储,但空间能够动态扩展。即它能够像数组一样操作,而且能够进行动态操作。通常体如今push_back() pop_back() 。
(2) 随机訪问方便,它像数组一样被訪问,即支持[ ] 操作符和vector.at()
(3) 节省空间,由于它是连续存储,在存储数据的区域都是没有被浪费的,可是要明白一点vector 大多情况下并非满存的,在未存储的区域实际是浪费的。
(4) 在内部进行插入、删除操作效率很低,这种操作基本上是被禁止的。Vector 被设计成仅仅能在后端进行追加和删除操作,其原因是vector 内部的实现是依照顺序表的原理。
(5) 仅仅能在vector 的最后进行push 和pop ,不能在vector 的头进行push 和pop 。
(6) 当动态加入的数据超过vector 默认分配的大小时要进行内存的又一次分配、拷贝与释放,这个操作很消耗性能。 所以要vector 达到最优的性能,最好在创建vector 时就指定其空间大小。
双向链表list
是一个线性链表结构,它的数据由若干个节点构成,每个节点都包含一个信息块(即实际存储的数据)、一个前驱指针和一个后驱指针。它无需分配指定的内存大小且能够随意伸缩,这是由于它存储在非连续的内存空间中,而且由指针将有序的元素链接起来。
由于其结构的原因,list 随机检索的性能很的不好,由于它不像vector 那样直接找到元素的地址,而是要从头一个一个的顺序查找,这样目标元素越靠后,它的检索时间就越长。检索时间与目标元素的位置成正比。
尽管随机检索的速度不够快,可是它能够迅速地在不论什么节点进行插入和删除操作。由于list 的每一个节点保存着它在链表中的位置,插入或删除一个元素仅对最多三个元素有所影响,不像vector 会对操作点之后的全部元素的存储地址都有所影响,这一点是vector 不可比拟的。
list 的特点:
(1) 不使用连续的内存空间这样能够任意地进行动态操作;
(2) 能够在内部不论什么位置高速地插入或删除,当然也能够在两端进行push 和pop 。
(3) 不能进行内部的随机訪问,即不支持[ ] 操作符和vector.at() ;
(4) 相对于verctor 占用很多其它的内存。
双端队列deque
是一种优化了的、对序列两端元素进行加入和删除操作的基本序列容器。它同意较为高速地随机訪问,但它不像vector 把全部的对象保存在一块连续的内存块,而是採用多个连续的存储块,而且在一个映射结构中保存对这些块及其顺序的跟踪。向deque 两端加入或删除元素的开销非常小。它不须要又一次分配空间,所以向末端添加元素比vector 更有效。
实际上,deque 是对vector 和list 优缺点的结合,它是处于两者之间的一种容器。
deque 的特点:
(1) 随机訪问方便,即支持[ ] 操作符和vector.at() ,但性能没有vector 好;
(2) 能够在内部进行插入和删除操作,但性能不及list ;
(3) 能够在两端进行push 、pop ;
三者的比較
下图描写叙述了vector 、list 、deque 在内存结构上的特点:
vector 是一段连续的内存块,而deque 是多个连续的内存块, list 是全部数据元素分开保存,能够是不论什么两个元素没有连续。
vector 的查询性能最好,而且在末端添加数据也非常好,除非它又一次申请内存段;适合高效地随机存储。
list 是一个链表,不论什么一个元素都能够是不连续的,但它都有两个指向上一元素和下一元素的指针。所以它对插入、删除元素性能是最好的,而查询性能很差;适合 大量地插入和删除操作而不关心随机存取的需求。
deque 是介于两者之间,它兼顾了数组和链表的长处,它是分块的链表和多个数组的联合。所以它有被list 好的查询性能,有被vector 好的插入、删除性能。 假设你须要随即存取又关心两端数据的插入和删除,那么deque 是最佳之选。
关联容器
set, multiset, map, multimap 是一种非线性的树结构,详细的说採用的是一种比較高效的特殊的平衡检索二叉树—— 红黑树结构。(至于什么是红黑树,我也不太理解,仅仅能理解到它是一种二叉树结构)
由于关联容器的这四种容器类都使用同一原理,所以他们核心的算法是一致的,可是它们在应用上又有一些区别,先描写叙述一下它们之间的区别。
set ,又称集合,实际上就是一组元素的集合,但当中所包括的元素的值是唯一的,且是按一定顺序排列的,集合中的每一个元素被称作集合中的实例。由于其内部是通过链表的方式来组织,所以在插入的时候比vector 快,但在查找和末尾加入上被vector 慢。
multiset ,是多重集合,事实上现方式和set 是相似的,仅仅是它不要求集合中的元素是唯一的,也就是说集合中的同一个元素能够出现多次。
map ,提供一种“键- 值”关系的一对一的数据存储能力。其“键”在容器中不可反复,且按一定顺序排列(事实上我们能够将set 也看成是一种键- 值关系的存储,仅仅是它仅仅有键没有值。它是map 的一种特殊形式)。因为其是按链表的方式存储,它也继承了链表的优缺点。
multimap , 和map 的原理基本相似,它同意“键”在容器中能够不唯一。
关联容器的特点是明显的,相对于顺序容器,有下面几个主要特点:
1, 其内部实现是採用非线性的二叉树结构,详细的说是红黑树的结构原理实现的;
2, set 和map 保证了元素的唯一性,mulset 和mulmap 扩展了这一属性,能够同意元素不唯一;
3, 元素是有序的集合,默认在插入的时候按升序排列。
基于以上特点,
1, 关联容器对元素的插入和删除操作比vector 要快,由于vector 是顺序存储,而关联容器是链式存储;比list 要慢,是由于即使它们同是链式结构,但list 是线性的,而关联容器是二叉树结构,其改变一个元素涉及到其他元素的变动比list 要多,而且它是排序的,每次插入和删除都须要对元素又一次排序;
2, 关联容器对元素的检索操作比vector 慢,可是比list 要快非常多。vector 是顺序的连续存储,当然是比不上的,但相对链式的list 要快非常多是由于list 是逐个搜索,它搜索的时间是跟容器的大小成正比,而关联容器 查找的复杂度基本是Log(N) ,比方假设有1000 个记录,最多查找10 次,1,000,000 个记录,最多查找20 次。容器越大,关联容器相对list 的优越性就越能体现;
3, 在使用上set 差别于vector,deque,list 的最大特点就是set 是内部排序的,这在查询上尽管逊色于vector ,可是却大大的强于list 。
4, 在使用上map 的功能是不可代替的,它保存了“键- 值”关系的数据,而这样的键值关系採用了类数组的方式。数组是用数字类型的下标来索引元素的位置,而map 是用字符型keyword来索引元素的位置。在使用上map 也提供了一种类数组操作的方式,即它能够通过下标来检索数据,这是其它容器做不到的,当然也包含set 。(STL 中仅仅有vector 和map 能够通过类数组的方式操作元素,即如同ele[1] 方式)
容器适配器
STL 中包括三种适配器:栈stack 、队列queue 和优先级priority_queue 。
适配器是容器的接口,它本身不能直接保存元素,它保存元素的机制是调用还有一种顺序容器去实现,即能够把适配器看作“它保存一个容器,这个容器再保存全部元素”。
STL 中提供的三种适配器能够由某一种顺序容器去实现。默认下stack 和queue 基于deque 容器实现,priority_queue 则基于vector 容器实现。当然在创建一个适配器时也能够指定详细的实现容器,创建适配器时在第二个參数上指定详细的顺序容器能够覆盖适配器的默认实现。
因为适配器的特点,一个适配器不是能够由任一个顺序容器都能够实现的。
栈stack 的特点是后进先出,所以它关联的基本容器能够是随意一种顺序容器,由于这些容器类型结构都能够提供栈的操作有求,它们都提供了push_back 、pop_back 和back 操作;
队列queue 的特点是先进先出,适配器要求其关联的基础容器必须提供pop_front 操作,因此其不能建立在vector 容器上;
优先级队列priority_queue 适配器要求提供随机訪问功能,因此不能建立在list 容器上。