容器用于保存一组相同类型元素,因此一个容器可以看作是一类数据的集合。容器按其对元素的管理形式分为值容器和引用容器两种类型。值容器里,插入一个元素时,容器保存的是一个元素的副本,而引用类型容器则是保存该元素的引用或者地址。值容器可以简单的模拟引用容器,只需要把元素类型定义为指针就可以了,因此值容器更为通用。C++标准库里所有的容器都是值容器。
容器按类型分类包括有序和无序两种。标准库的有序容器包括set、map、multiset、multimap四类,这类容器内部的元素始终是有序的,容器内部默认使用‘<’操作符完成元素大小的比较,使用者也可以提供自己的元素大小比较函数。这类容器增加元素只提供了insert操作,插入的元素由容器按其大小自动放到合适的位置。
无序容器即内部元素是没有排序的,但也不是乱序的,元素的顺序为元素加入容器中的顺序。这类容器包括vector、list、deque三类。这类容器提供了push_back和push_front操作,即在尾部或者是头部插入元素,insert操作可以在容器指定的位置插入元素。
还有一种是容器适配器,比如stack、queue,可以从上面的无序容器适配出相关的接口。
以上就是标准库包含的基本容器类型,由于时间的关系,标准库还没有提供hash表的实现,但会在未来的扩展里增加。
不同容器虽然实现的方式差别很大,但都提供了相同或相似的接口,因此如何根据使用场景选择合适的容器类型是很重要的。这就需要我们稍微了解一下容器的底层实现细节:
- 有序的容器的实现可以理解为是相同的,就是一棵平衡二叉树。set和multiset中,树节点包含的就是每个元素,并按元素的大小比较结果排序,set中不允许有相同的元素,而multiset则可以存在相同的元素。map和multimap中,每个树节点就是map的每个元素的键和值,并按键的大小比较结果排序,map中不允许有相同键的元素,而multimap则可以存在相同键的元素。
- vector是C++对数组的封装,因此内部元素的排列顺序就是元素插入数组时的顺序,vector里元素占用的内存是连续的。因此vector支持下标操作,类似内置数组的下标操作。另外vector的元素个数是动态扩展的,定义一个vector的时候不需要指定长度,因此vector需要在插入元素时自动扩展内存的大小来包含更多的元素:比如当前的内存只能容纳5个元素,在插入第6个元素时,就需要先分配一块更大的内存,比如可以容纳10个元素的,将之前的5个元素复制过来,释放旧的内存,再存入第6个元素。
- list是一个双向链表,内部元素的排列顺序就是元素插入数组时的顺序,但list可以在头上和尾部插入元素,list不支持下标操作,而是使用一个双向的迭代器来完成元素的遍历,list提供了非常丰富的接口,比如sort成员函数用于将list内的元素进行排序,unique用于去除重复的节点,splice函数用于高效的合并两个子链表。
- deque是一个介于list和vector之间的一种容器,deque提供了比vector丰富的功能,同时支持下标操作和在头尾插入元素的操作,deque也提供了随机访问迭代器。在deque中间插入元素也比vector更高效。deque内部实现不是一块连续的内存,而是由多块的连续内存组成。这样就可以在元素个数增长时,降低需要拷贝的元素个数。
总结如下:
是否连续内存 | 支持头部插入或删除元素 | 支持中间插入或删除元素 | 支持随机访问 | |
vector | 是 | 否 | 是(效率低) | 是 |
list | 否 | 是 | 是(效率很高) | 否 |
deque | 块内连续 | 是 | 是(效率高) | 是 |
需要说明的是,支持随机访问的容器就可以用标准库的快速排序算法std::sort进行排序。
根据容器的这些基本特性,我们就可以在具体的应用中选择合适的容器了。vector占用内存少,并且和C的数组兼容,支持随机访问,因此访问vector中间某个元素效率很高,但从vector的头部或中间插入或删除元素效率最低,因此对于不需要频繁的从中间插入删除数据的应用,应该优先使用vector。
如果元素个数比较多,并且需要从中间插入删除元素,又能快速访问到中间元素,则应该选用deque。
list从中间增加删除元素效率很高,但如果访问中间的某个元素必须从链表头开始遍历,因此对于需要频繁增加和删除中间元素的应用,应该选择list。
如果是需要元素能自动排序,则选择set或map。
使用例子:
std::vector<int>oVec; //定义一个元素为int类型的数组
for (inti=0; i<10; ++i) //插入10个元素
{
oVec.push_back(i);
}
//遍历并打印所有元素,(方法1)
for (intj=0; j<oVec.size(); ++j)
{
std::cout << oVec[j] ;
std::cout << "\n" ;
}
//或者,(方法2)
std::vector<int>::iterator it = oVec.begin();
for (; it!= oVec.end(); ++it)
{
std::cout << *it ;
std::cout << "\n" ;
}
oVec.pop_back();//删除vector中最后一个元素
list用法:
std::list<int>oList;
//在底部增加或删除元素操作同vector,遍历操作只支持vector里的方法2,不支持方法1
oList.remove(5);//删除list中值为5的元素
std::list<int>::iteratorit = oList.begin();
std::advance(it,4); //获取指向第5个元素的迭代器
it = oList.erase(it);//删除第5个元素并获取下一个元素
oList.insert(it,4); //在第5个元素前重新插入元素4
std::list<int>oList2;
oList2.push_front(10);//list头上增加一个元素
oList2.splice(oList2.begin(),oList) //将oList的所有元素加入到oList2的前面,由于是直接将oList的节点挂过来,所以效率很高,副作用是oList链表被清空了。
deque元素快速排序:
std::deque<int>oDeque;
//deque的操作同vector
//快速排序deque内的元素
std::sort(oDeque.begin(), oDeque.end());
所有的容器类型都支持一个无异常的数据交换操作swap:
std::list<int>list1;
std::list<int>list2;
//swap成员函数通过交换内部数据指针来完成操作,不涉及任何内存分配或复制,
//因此所有的标准库容器都保证该操作是无异常的,即永远不会失败。
list1.swap(list2);
另外容器提供的成员函数可能功能上和标准算法有重叠,比如查找元素的操作,可以使用标准算法查找:
std::set<int>oSet;
for (inti=0; i<10; ++i)
{
oSet.insert(i);
}
std::set<int>::iteratorit = std::find(oSet.begin(), oSet.end(), 1);
也可以用成员函数:
std::set<int>::iteratorit = oSet.find(1);
上面两种实现是不一样的,成员函数更高效,表达也更简洁,标准库算法是根据迭代器进行线性查找,而set的find方法则是从二叉树进行查找,效率更高,所以对于实现同一个功能,应该优先使用成员函数。