19.1 C++STL标准模板库大局观-STL总述、发展史、组成与数据结构谈
19.2 C++STL标准模板库大局观-容器分类与array、vector容器精解
19.3 C++STL标准模板库大局观-容器的说明和简单应用例续
19.4 C++STL标准模板库大局观-分配器简介、使用与工作原理说
19.5 C++STL标准模板库大局观-迭代器的概念和分类
19.6 C++STL标准模板库大局观-算法简介、内部处理与使用范例
19.7 C++STL标准模板库大局观-函数对象回顾、系统函数对象与范例
19.8 C++STL标准模板库大局观-适配器概念、分类、范例与总结
文章目录
3.容器的说明和简单应用例续
3.1 deque和stack
(1)deque
deque这种顺序容器是一个双端队列(双向开口),deque是double-ended queue的缩写。这种容器数据存储结构的感觉如图所示。
该队列相当于一个动态数组,因为它是双端的,所以无论在头部还是在尾部插入和删除数据都会很快,但是若要在中间插入数据,因为要移动其他元素,效率就会比较低。看看如下范例。
class A
{
public:
int m_i;
A(int tmpv) :m_i(tmpv) //构造函数
{
cout << "A::A()构造函数执行" << endl;
}
A(const A& tmpA)
{
m_i = tmpA.m_i;
cout << "A::A()拷贝构造函数执行" << endl;
}
~A()
{
cout << "A::~A()析构函数执行" << endl;
}
};
int main()
{
deque<A> mydeque;
for (int i = 0; i < 5; ++i)
{
cout << "--------------begin----------------" << endl;
mydeque.push_front(A(i));
cout << "--------------end----------------" << endl;
}
for (int i = 0; i < 5; ++i)
{
cout << "--------------begin2----------------" << endl;
mydeque.push_back(A(i));
cout << "--------------end2----------------" << endl;
}
for (int i = 0; i < mydeque.size(); ++i)
{
cout << "下标为" << i << "的元素的m_i值为:" << mydeque[i].m_i << endl;
printf("对象mydeque[%d]的地址为%p \n", i, &mydeque[i]);
}
}
通过结果可以看到,虽然看起来图19.8绘制的deque容器的内存是连续的,但是测试结果表明它的内存并不连续。
deque其实是一个分段数组。当插入元素多的时候,它就会把元素分到多个段中去,当然,每一段的内存是连续的(所以只能说内存是分段连续)。当然,每一段能存多少个元素或者说每一段内存有多大,笔者并没有进一步深入研究。根据上面的结果,似乎每一段能保存4个元素。
(2)stack
stack这种顺序容器和deque类似,stack称为栈或者堆栈都可以。
栈是一种比较基本的数据结构,特点是后进先出。deque是两端都有开口,而stack只有一端有开口。stack这种容器数据存储结构的如图所示。
有的读者可能觉得stack和vector有点类似,但请注意,vector还支持insert、erase,也就是说,vector支持从中间插入和删除元素的操作。但是stack只支持往栈顶放入元素和从栈顶取出元素(删除元素),因为stack这种容器设计的初衷就要求具备这种特性。不难看出,deque其实是包含stack功能的
3.2 queue
前面讲过的deque是双端队列,这里要讲的queue是普通队列(简称队列)。队列是一种比较基本的数据结构,其特点是先进先出。也就是说,元素从一端进入,从另一端取出(删除元素),queue容器设计的初衷就要求具备这种特性。
queue这种容器数据存储结构的感觉如图所示。
可以看到,queue这个普通队列,元素是从一端入,从另一端出。所以,deque其实也是包含queue功能的。
3.3 list
list这种顺序容器是一个双向链表。这种容器数据存储结构的如图所示
list从图中不难看到,因为list是链表,所以各个元素之间就不需要紧挨在一起,只需要用指针通过指向把元素关联起来即可。
那么,这个list双向链表有什么特点呢?查找元素要沿着链来找,所以查找的效率并不突出,但因为它是一个双向链,所以在任意位置插入和删除元素都非常迅速——几个元素中的指针一改变指向就可以了。
在C++面试过程中,经常被问到vector和list这两个容器的区别。通过这里的学习,它们非常明显的区别:
vector类似于数组,它的内存空间是连续的,list是双向链表,内存空间并不连续。
· vector插入、删除元素效率比较低,但list插入、删除元素效率就非常高。
· vector当内存不够时,会重新找一块内存,对原来内存中的元素做析构,在新找的内存重新建立这些对象(容器中的元素)。
· vector能进行高效的随机存取(跳转到某个指定的位置存取),而list做不到这一点。例如,要访问第5个元素,vector因为内存是连续的,所以极其迅速,它一下就能定位到第5个元素(一个指针跳跃一定数量的字节就可以到达指定位置)。反观list,要找到第5个元素,得顺着这个链逐个地找下去,一直找到第5个。所以说vector随机存取非常快,而list随机存取比较慢。
3.4 其他
(1)forward_list
这是C++11新增加的顺序容器,是一个单向链表。这种容器数据存储结构的感觉如图所示
不难看出,forward_list比list少了一个方向的链(指针),官方称它为受限的list。少了一个方向的指针,会造成一定的访问不便,但是少了一个方向的指针后,一个容器中的元素就能节省下4字节的内存(在x86平台下)。容器中的元素若是很多,则省下的内存也很可观。
另外,很多容器都有push_back成员函数,但是forward_list容器只有push_front,这说明这个容器最适合的是往前头插入元素。看看如下范例。
forward_list<A> myforlist;
myforlist.push_frout(A(1));
myforlist.push_frout(A(2));
(2)map和set
map和set都是关联容器。
map容器数据存储结构的感觉如图所示。
观察图可以看到,该图的外观像一棵树,根据资料记载,map和set这类容器内部的实现多为红黑树(树的一种),红黑树这种数据结构本身内部有一套很好的保存数据的机制,但在这里无须研究红黑树到底是怎样保存数据的。向这种容器中保存数据的时候不需要指定数据的位置,这种容器会自动给加入的元素根据内部算法安排位置。
另外注意到,图所示的map容器的每个元素(树节点)包含两项(图中用两个方块代表),也就是说,每个元素其实都是一个键值对(key/value)。一般来讲,这类容器的使用通常都是通过key来查找value。这种容器通过key查找value的速度非常快,但不允许在一个map容器中出现两个相同的key,所以,如果key有可能重复,请使用multimap容器。
{
map<int, string> mymap;
mymap.insert(std::make_pair(1, "老王")); //通过make_pair创建一个键值对,作为一个元素插入到map中
mymap.insert(std::make_pair(2, "老李"));
mymap.insert(pair<int, string>(3, "老赵"));
mymap.insert(pair<int, string>(3, "老白")); //如果键重复了,则这行等于没有执行
auto iter = mymap.find(3); //查找key为3的元素
if (iter != mymap.end())
{
//找到
printf("编号为%d,名字为%s\n", iter->first, iter->second.c_str());
}
}
再看一看set。set容器数据存储结构的感觉如图
set容器中的元素没有key和value之分,每个元素就是一个value。元素保存到容器中后,容器会自动把这个元素放到一个位置,每个元素的值不允许重复,重复的元素插入进去也没有效果。如果想插入重复的元素,请使用multiset容器。
分析上面的这种关联容器,可以总结出一些特点:插入元素的时候,因为这类容器要给插入的元素找一个合适的位置,所以插入的速度可能会慢一些,但是,得到的好处是查找的时候快,所以对于需要快速找到元素的应用场景,重点考虑使用map、set这类容器。
(3)unordered_map与unordered_set等
以往的诸如hash_set、hash_map、hash_multiset、hash_multimap,这些老的容器也能使用,但并不推荐使用了,新版本的容器一般都是以unordered_开头了。
以unordered_开头的容器属于无序容器(关联容器的一种),无序容器内部一般是使用哈希表(散列表)来实现的。哈希表,上一节也大概介绍了一下工作原理。其数据存储结构图如图所示。
unordered_map和unordered_multimap每个元素同样是一个键值对,unordered_map中保存的键是不允许重复的,而unordered_multimap中保存的键可以重复。对于内部采用哈希表这种数据结构存储数据的容器,在理解的时候就可以把这种容器中的元素理解成是无序的。所以,这两个容器的数据存储结构图也可以如图所示。
unordered_set和unordered_multiset的每个元素都是一个值,unordered_set中保存的值不允许重复,而unordered_multiset中保存的值可以重复。这两个容器的数据存储结构图如图所示。
{
unordered_set<int> myset;
cout << "bucket_count() = " << myset.bucket_count() << endl; //篮子数量:8
for (int i = 0; i < 8; ++i)
{
myset.insert(i);
}
cout << "bucket_count() = " << myset.bucket_count() << endl; //8
myset.insert(8); //装第9个元素,看篮子数量是否增加
cout << "bucket_count() = " << myset.bucket_count() << endl; //64,突然变成64个了,挺狠(有的资料说2倍增长,这里却8倍增长)
cout << "max_bucket_count() = " << myset.max_bucket_count() << endl; //最大篮子数量:536870911
printf("所有篮子(本容器)里有的元素数量为%d\n", myset.size());//9
//打印每个篮子里的元素个数
for (int i = 0; i < myset.bucket_count(); ++i)
{
printf("第%d个篮子里有的元素数量为%d\n", i, myset.bucket_size(i)); //从0开始
}
auto pmyfind = myset.find(5); //对于查找这种操作,如果容器本身提供,一定要用容器本身提供的,效率最高;如果容器不提供,可以考虑使用一个全局的find函数,全局find函数也是stl的组成部分,是属于算法里的
if (pmyfind != myset.end())
{
cout << "元素5存在于容器中" << endl;
}
if (find(myset.begin(), myset.end(), 5) != myset.end()) //全局find函数(算法)
{
cout << "元素5存在于容器中" << endl;
}
}
至此,容器的话题就告一段落了。