容器
容器的定义
在数据存储上,有一种对象类型,它可以持有其它对象或指向其它对像的指针,这种对象类型就叫做容器。很简单,容器就是保存其它对象的对象,当然这是一个朴素的理解,这种“对象”还包含了一系列处理“其它对象”的方法。
容器的种类
1、顺序容器:是一种各元素之间有顺序关系的线性表,是一种线性结构的可序群集。顺序性容器中的每个元素均有固定的位置,除非用删除或插入的操作改变这个位置。顺序容器的元素排列次序与元素值无关,而是由元素添加到容器里的次序决定。顺序容器包括:vector(向量)、list(列表)、deque(队列)。
2、关联容器:关联式容器是非线性的树结构,更准确的说是二叉树结构。各元素之间没有严格的物理上的顺序关系,也就是说元素在容器中并没有保存元素置入容器时的逻辑顺序。但是关联式容器提供了另一种根据元素特点排序的功能,这样迭代器就能根据元素的特点“顺序地”获取元素。元素是有序的集合,默认在插入的时候按升序排列。关联容器包括:map(集合)、set(映射)、multimap(多重集合)、multiset(多重映射)。
此外,SGI STL还提供了一个不在标准规格之列的关联式容器:hash_table(散列表),以及以此hashtable为底层机制而完成的hash_set、hash_map、hash_multiset、hash_multimap。
3、容器适配器:本质上,适配器是使一种不同的行为类似于另一事物的行为的一种机制。容器适配器让一种已存在的容器类型采用另一种不同的抽象类型的工作方式实现。适配器是容器的接口,它本身不能直接保存元素,它保存元素的机制是调用另一种顺序容器去实现,即可以把适配器看作“它保存一个容器,这个容器再保存所有元素”。STL 中包含三种适配器:栈stack 、队列queue 和优先级队列priority_queue 。
容器类自动申请和释放内存,因此无需new和delete操作。
容器 | 支持的迭代器类别 | 说明 |
vector | 随机访问 | 一种随机访问的数组类型,提供了对数组元素进行快速随机访问以及在序列尾部进行快速的插入和删除操作的功能。可以再需要的时候修改其自身的大小 |
deque | 随机访问 | 一种随机访问的数组类型,提供了序列两端快速进行插入和删除操作的功能。可以再需要的时候修改其自身的大小 |
list | 双向 | 一种不支持随机访问的数组类型,插入和删除所花费的时间是固定的,与位置无关。 |
set | 双向 | 一种随机存取的容器,其关键字和数据元素是同一个值。所有元素都必须具有惟一值。 |
multiset | 双向 | 一种随机存取的容器,其关键字和数据元素是同一个值。可以包含重复的元素。 |
map | 双向 | 一种包含成对数值的容器,一个值是实际数据值,另一个是用来寻找数据的关键字。一个特定的关键字只能与一个元素关联。 |
multimap | 双向 | 一种包含成对数值的容器,一个值是实际数据值,另一个是用来寻找数据的关键字。一个关键字可以与多个数据元素关联。 |
stack | 不支持 | 适配器容器类型,用vector,deque或list对象创建了一个先进后出容器 |
queue | 不支持 | 适配器容器类型,用deque或list对象创建了一个先进先出容器 |
priority_queue | 不支持 | 适配器容器类型,用vector或deque对象创建了一个排序队列 |
list概述
篇幅较长,单做一篇list源码解读
stack概述
stack是一种先进后出的数据结构。它只有一个出口。除了最顶端外,没有任何办法可以存取stack的其它元素。也就是说,stack不允许有遍历行为。
以某种既有容器作为底部结构,将其接口改变,使之符合“先进后出”的特性,形成一个stack,是很容易的。
deque是双向开口的数据结构,以deque为底部结构并封闭其头端开口,便可形成stack。
stack没有迭代器。只有stack顶端的元素,才有机会被外界取用。stack不提供走访功能,也不提供迭代器。
SGI STL以deque作为缺省情况下的stack底部结构。
由于stack是以底部容器完成其所有工作,而具有这种“修改某物接口,形成另一种风貌”之性质者,称为adapter(配接器),因此,STL stack不归类为容器,而是归类为container adapter(容器配接器)。
除了deque之外,list也是双向开口的数据结构,所以也可以用来实现stack。
top | 访问顶部元素 |
empty | 判断是否为空 |
size | 返回有效元素个数 |
push | 在容器顶部插入元素 |
pop | 移除容器顶部的元素 |
emplace(C++11) | 在容器顶部放置插入元素 |
swap | 交换容器的内容 |
queue概述
queue是一种先进先出的数据结构。它有两个出口。
从最低端加入元素,从最顶端获取元素,除此之外,没有办法可以存取其它元素。
deque是双向开口的数据结构,以deque为底部结构便可形成queue。
queue没有迭代器。只有queue顶端的元素,才有机会被外界取用。queue不提供遍历功能,也不提供迭代器。
SGI STL以deque作为缺省情况下的queue底部结构。
除了deque之外,list也是双向开口的数据结构,所以也可以用来实现queue。
front | 访问第一个元素 |
back | 访问最后一个元素 |
empty | 判断是否为空 |
size | 返回有效元素个数 |
push | 在容器顶部插入元素 |
pop | 移除容器顶部的元素 |
emplace(C++11) | 在容器顶部放置插入元素 |
swap | 交换容器的内容 |
dequeue概述
篇幅较长,单做一篇deque
priority_queue概述
优先级队列是一个拥有权值观念的queue,由于这是一个queue,所以只允许在底端加入元素,并从顶端取出元素。
priority_queue带有权值观念,其内的元素并非依照被推入的次序排列,而是自动依照元素的权值排列(通常权值以实值表示)。权值最高者,排在最前面。
缺省情况下priority_queue用一个max-heap完成,后者是一个以vector表现的完全二叉树。
STL中的 priority_queue也被归类为container adapter(适配器)。
priority_queue的所有元素,进出都有一定的规则,只有queue顶端的元素(权值最高者)才有机会被外界使用。priority_queue不提供遍历功能,也不提供迭代器。
priority_queue中函数其实就是借助于heap的处理规则(make_heap、push_heap、pop_heap)实现的,可参考另一篇博文heap相关函数的实现
slist概述
STL list是双向串行。SGI STL另提供了一个单向串行,名为slist。这个容器并不在标准规格之内。
slist和list的主要差别在于,前者的迭代器属于单向的Forward Iterator,后者的迭代器属于双向的Bidirectional Iterator。此为,slist的功能自然也就受到许多限制。不过,单向串行所耗用的空间更小,某些动作更快,不失为另一种选择。
slist和list的共同具有一个相同特色是,他们的安插insert,移除erase,接合splice等动作并不会造成原有的迭代器失效。(当然,指向被移除原始的那个迭代器,在移除动作发生之后肯定是会失效)。
根据STL的习惯,安插动作会将新元素安插于指定位置之前,而非之后。然而做为一个单向串行,slist没有任何方便的办法可以回头定前一个位置,因此它必须从头找起。换句话说,除了slist起始处附近的区域之外,在其它位置上采用insert或erase操作函数,都是不智之举。这便是slist相对于list之下的缺点。为此,slist特别提供了insert_after()和erase_after()提供弹性运用。
基于同样的效率考虑,slist不提供push_back(),只提供push_front()。因此slist的元素次序会和元素安插位置的次序相反。
单链表的节点结构
//单向链表的节点基本结构
struct __slist_node_base
{
__slist_node_base* next;
};
//单向链表的节点结构
template <class T>
struct __slist_node : public __slist_node_base
{
T data;
};
//全局函数:已知某一节点,插入新节点于其后
inline __slist_node_base* __slist_make_link(__slist_node_base* prev_node,
__slist_node_base* new_node)
{
//其实就是单向链表的插入过程
new_node->next = prev_node->next;
prev_node->next = new_node;
return new_node;
}
RB-tree概述
篇幅较长,单做一篇RB-tree
set概述
set的特性是,所有元素都会根据元素的键值自动被排序,set的元素不像map那样可以同时拥有实值(value)和键值(key),set元素的键值就是实值,实值就是键值。set不允许两个元素有相同的键值。
我们可以通过set的迭代器改变set的元素吗?不行的,因为set元素值就是其键值,关系到set元素的排列规则。如果任意改变set元素值,会严重破坏set组织。set<T>::iterator被定义为底层RB-tree的const_iterator,杜绝写入操作。换句话说,set iterator是一种constant iterator(相对于mutable iterators)。 所以你只能先删除,然后再添加。
set拥有与list相同的某些性质:当客户端对它进行元素新增操作(insert)或删除操作(erase)时,操作之前的所有迭代器,在操作完成之后都依然有效。当然被删除的那个元素的迭代器必然是个例外。
STL 特别提供了一组 set/multiset 相关的算法,包括交集set_intersection、联集set-union、差集set_difference、对称差集set_symmetric_difference。
由于RB_tree是一种平衡二叉搜索树,自动排序的效果很不错,所以标准的STLset即以RB_tree为底层机制。又由于set所开放的各种操作接口,RB_tree也都提供了,所以几乎所有的set操作行为,都只是转调用RB_tree的操作行为而已。
insert | 插入元素 |
erase | 删除元素 |
swap | 交换内容 |
clear | 清空内容 |
emplace(C++11) | 构造及插入一个元素 |
find | 通过给定值查找元素 |
count | 返回匹配给定值的元素的个数 |
lower_bound | 返回指向容器中第一个值等于给定搜索值或在给定搜索值之后的元素的迭代器 |
upper_bound | 返回指向容器中第一个值在给定搜索值之后的元素的迭代器 |
equal_range | 返回值匹配给定搜索值的元素组成的范围 |
set默认是从小到大排序,如果想要从大到小,需要加头文件和greater<int>
#include<xfunctional>
set<int, greater<int>> s;
map概述
map的特性是,所有元素都会根据元素的键值自动被排序,map的所有元素都是pair,同时拥有实值(value)和键值(key)。pair的第一个元素被视为键值,第二个元素被视为实值。map不允许相同键值,但multimap允许相同键值。
我们可以通过map的迭代器改变map的元素内容吗? 如果想要修正元素的键值,答案是不行的,因为map元素的键值关系到map元素的排列顺序.任意改变map元素键值将会严重破坏map组织(可以通过删除之后再添加解决)。但如果想要修正元素的实值,答案是可以,因为map元素的实值并不影响map元素的排列规则,因此,map iterator既不是一种consttant iterator,也不是一种mutable iterators。所以只能删除之后再添加。
map拥有与list相同的某些性质: 当客户端对它进行元素新增操作或删除操作时,操作之前的所有迭代器,在操作完成之后都依然有效,当然那个被删除元素迭代器必然是个例外。
map也是以RB-tree为底层机制的。
size | 返回有效元素个数 |
max_size | 返回 map 能容纳的最大元素个数 |
empty | 判断是否为空 |
operator[] | 访问元素 |
at(C++11) | 访问元素,eg:s.at("wang")=2; |
insert | 插入元素,单元素版(1) 返回一个二元组(Pair) |
erase | 删除元素 |
swap | 交换内容 |
clear | 清空内容 |
emplace(C++11) | 插入操作,如果当前函数成功插入一个元素,将会返回由指向新插入元素的迭代器及
|
key_comp | 返回键比较对象 |
value_comp | 返回值比较对象 |
find | 通过给定主键查找元素,找到指向被搜索到的元素的迭代器,否则返回指向末尾的迭代器。 |
count | 返回匹配给定主键的元素的个数 |
lower_bound | 返回指向容器中第一个主键等于给定搜索值或在给定搜索值之后的元素的迭代器 |
upper_bound | 返回指向容器中第一个主键在给定搜索值之后的元素的迭代器 |
equal_range | 返回值匹配给定搜索值的元素组成的范围 |
为何map和set不能像vector一样有个reserve函数来预分配数据?
我在map和set内部存储的已经不是元素本身了,而是包含元素的节点。你想想树的结构有resize操作吗?而且也就不需要。
为何map和set的插入删除效率比用其他序列容器高?
因为对于关联容器来说,不需要做内存拷贝和内存移动。map和set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。因此插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点就OK了。这里的一切操作就是指针换来换去,和内存移动没有关系。
map和set为何每次insert之后,以前保存的iterator不会失效?
看见了上面答案的解释,已经可以很容易解释这个问题。iterator这里就相当于指向节点的指针,内存没有变,指向内存的指针怎么会失效呢(当然被删除的那个元素本身已经失效了)。
emplace
emplace操作是C++11新特性,新引入的的三个成员emlace_front、empace 和 emplace_back,这些操作构造而不是拷贝元素到容器中,这些操作分别对应push_front、insert 和push_back,允许我们将元素放在容器头部、一个指定的位置和容器尾部。
两者的区别
当调用insert时,我们将元素类型的对象传递给insert,元素的对象被拷贝到容器中,而当我们使用emplace时,我们将参数传递元素类型的构造函,emplace使用这些参数在容器管理的内存空间中直接构造元素。
例子
假定d是一个Date类型的容器。
//使用三个参数的Date构造函数,在容器管理的内存空间中构造新元素。
d.emplace_back(“2016”,”05”,”26”);
//错误,push_back没有这种用法
d.push_back(“2016”,”05”,”26”);
//push_back()创建一个临时对象,然后将临时对象拷贝到容器中
d.push_back(Date(“2016”,”05”,”26”));
通过例子发现,使用C++11新特性emplace向容器中添加新元素,在容器管理的内存空间中构造新元素,与insert相比,省去了构造临时对象,减少了内存开销。
emplace函数在容器中直接构造元素,传递给emplace函数的参数必须与元素类型的构造函数相匹配。
multiset概述
multiset的特性以及用法和set完全相同,唯一的差别在于它允许键值重复,因此它的插入操作采用的是底层机制RB-tree的insert_qual()而非insert_unique()。
#include<iostream>
#include<vector>
#include<set>
#include<algorithm>
using namespace std;
struct myComp
{
bool operator() (const int &a, const int &b)
{
return a > b; //从大到小排序
//return a < b; //从小到大排序
}
};
int main(void)
{
int N;
cin >> N;
int value;
set<int, myComp> s;
for (int i = 0; i < N; ++i)
{
cin >> value;
s.insert(value);
}
auto iter = s.begin();
for (; iter != s.end(); ++iter)
cout << *iter << " ";
cout << endl;
system("pause");
return 0;
}
multimap概述
multimap的特性以及用法和map完全相同,唯一的差别在于它允许键值重复,因此它的插入操作采用的是底层机制RB-tree的insert_qual()而非insert_unique()。
hashtable概述
篇幅较长,单做一篇hash table
hash_set概述
STL中的set多半以RB-tree为底层机制,SGI则在STL标准规格之外令提供了一个所谓的hash_set,以hashtable为底层机制。由于hash_set所供应的操作接口在hashtable中都提供了,所有hash_set的操作行为只是调用hashtable的操作。
运用set,为的是能够快速搜寻元素。不论底层是RB-tree或是hashtable,都可以做到。但是RB-tree有自动排序功能而hashtable没有,所以set的元素有自动排序功能而hash_set没有。
hash_map概述
SGI在STL标准规格之外令提供了一个所谓的hash_map,以hashtable为底层机制。由于hash_map所供应的操作接口在hashtable中都提供了,所有hash_map的操作行为只是调用hashtable的操作。
运用map,为的是能够根据键值快速搜寻元素。不论底层是RB-tree或是hashtable,都可以做到。但是RB-tree有自动排序功能而hashtable没有,所以map的元素有自动排序功能而hash_map没有。
hash_multiset概述
hash_multiset的特性以及用法和multiset完全相同,唯一的差别在于它的底层机制是hash_table。也因此,hash_multiset的元素并不会自动排序。
hash_multiset和hash_set实现上的唯一区别在于,前者的元素插入操作采用底层机制hashtable的insert_qual(),后者采用的是insert_unique()。
hash_multimap概述
hash_multimap的特性以及用法和multimap完全相同,唯一的差别在于它的底层机制是hash_table。也因此,hash_multiset的元素并不会自动排序。
hash_multimap和hash_map实现上的唯一区别在于,前者的元素插入操作采用底层机制hashtable的insert_qual(),后者采用的是insert_unique()。
几种容器优劣分析
序列式容器
1、Vector:
优点:
A、支持随机访问,访问效率高和方便,它像数组一样被访问,即支持[ ] 操作符和vector.at()。
B、节省空间,因为它是连续存储,在存储数据的区域都是没有被浪费的,但是要明确一点vector 大多情况下并不是满存的,在未存储的区域实际是浪费的。
缺点:
A、在内部进行插入、删除操作效率非常低。
B、只能在vector 的最后进行push 和pop ,不能在vector 的头进行push 和pop 。
C、 当动态添加的数据超过vector 默认分配的大小时要进行内存的重新分配、拷贝与释放,这个操作非常消耗能。
2、List:
优点:
不使用连续的内存空间这样可以随意地进行动态操作,插入、删除操作效率高;
缺点:
A、不能进行内部的随机访问,即不支持[ ] 操作符和vector.at(),访问效率低。
B、相对于verctor 占用更多的内存。
3、Deque:
优点:
A、支持随机访问,方便,即支持[ ] 操作符和vector.at() ,但性能没有vector 好;
B、可以在两端进行push 、pop 。
缺点:
A、 在内部进行插入、删除操作效率低。
B、受到分段连续线性空间的字面影响,我们可能以为deque的操作复杂度和vector相差不多。其实不是这样,虽然是分段连续线性空间,就必须有中央控制,而为了维护整体连续的假象,数据结构的设计及迭代器前进后退等动作颇为繁琐。deque的操作代码量远比vector或list都多得多。这也就是deque最主要的缺点。
综述
vector 的查询性能最好,并且在末端增加数据也很好,除非它重新申请内存段;适合高效地随机存储。
list 是一个链表,任何一个元素都可以是不连续的,但它都有两个指向上一元素和下一元素的指针。所以它对插入、删除元素性能是最好的,而查询性能非常差;适合 大量地插入和删除操作而不关心随机存取的需求。
deque 是介于两者之间,它兼顾了数组和链表的优点,它是分块的链表和多个数组的联合。所以它有被list 好的查询性能,有被vector 好的插入、删除性能。 如果你需要随即存取又关心两端数据的插入和删除,那么deque 是最佳之选。
关联容器
关联容器的特点:
1、其内部实现是采用非线性的二叉树结构,具体的说是红黑树的结构原理实现的;
2、set 和map 保证了元素的唯一性,mulset 和mulmap 扩展了这一属性,可以允许元素不唯一;
3、元素是有序的集合,默认在插入的时候按升序排列。
基于上述特点
A、关联容器对元素的插入和删除操作比vector 要快,因为vector 是顺序存储,而关联容器是链式存储;比list 要慢,是因为即使它们同是链式结构,但list 是线性的,而关联容器是二叉树结构,其改变一个元素涉及到其它元素的变动比list 要多,并且它是排序的,每次插入和删除都需要对元素重新排序;
B、关联容器对元素的检索操作比vector 慢,但是比list 要快很多。vector 是顺序的连续存储,当然是比不上的,但相对链式的list 要快很多是因为list 是逐个搜索,它搜索的时间是跟容器的大小成正比,而关联容器 查找的复杂度基本是Log(N) ,比如如果有1000 个记录,最多查找10 次,1,000,000 个记录,最多查找20 次。容器越大,关联容器相对list 的优越性就越能体现;
C、在使用上set 区别于vector,deque,list 的最大特点就是set 是内部排序的,这在查询上虽然逊色于vector ,但是却大大的强于list 。
D、在使用上map 的功能是不可取代的,它保存了“键- 值”关系的数据,而这种键值关系采用了类数组的方式。数组是用数字类型的下标来索引元素的位置,而map 是用字符型关键字来索引元素的位置。在使用上map 也提供了一种类数组操作的方式,即它可以通过下标来检索数据,这是其他容器做不到的,当然也包括set 。(STL 中只有vector 和map 可以通过类数组的方式操作元素,即如同ele[1] 方式)。