深入浅出之容器

一、容器的定义

容器是用来管理某一类对象的集合。在C++中属于标准模板库(STL)。C++标准模板库里提供了3类容器:

                                 

容器的优点
       1)容器类是一种对特定代码重用问题的良好的解决方案。

       2)可以自行扩展。当不知道需要存储多少对象时,就不知道应当开辟多大内存空间,而容器不需要预先设定空间长度,只需要创建一个对象并合理调用其提供的方法,其余的细节则由它自身完成,它自己申请内存或释放内存,并使用最优算法执行所有命令。

       3)容器类自动申请和释放内存,因此无需进行new和delete操作。
 

二、顺序容器

C++标准模板库里提供有3种顺序容器,即vector,list和deque。其中vector与deque是以数组为基础的,list是以链表为基础的。

2.1 向量vector容器

2.1.1、什么是vector?

向量(Vector)是一个封装了动态大小数组的顺序容器(Sequence Container)。跟任意其它类型容器一样,它能够存放各种类型的对象。可以简单的认为,向量是一个能够存放任意类型的动态数组。 

2.1.2、特性

1.顺序序列

顺序容器中的元素按照严格的线性顺序排序。可以通过元素在序列中的位置访问对应的元素。

2.动态数组

支持对序列中的任意元素进行快速直接访问,甚至可以通过指针算述进行该操作。操供了在序列末尾相对快速地添加/删除元素的操作。

3.能够感知内存分配器的(Allocator-aware)

容器使用一个内存分配器对象来动态地处理它的存储需求。 

2.1.3、基本函数实现

1.构造函数
vector():创建一个空vector
vector(int nSize):创建一个vector,元素个数为nSize
vector(int nSize,const t& t):创建一个vector,元素个数为nSize,且值均为t
vector(const vector&):复制构造函数
vector(begin,end):复制[begin,end)区间内另一个数组的元素到vector中

2.增加函数
void push_back(const T& x):向量尾部增加一个元素X
iterator insert(iterator it,const T& x):向量中迭代器指向元素前增加一个元素x
iterator insert(iterator it,int n,const T& x):向量中迭代器指向元素前增加n个相同的元素x
iterator insert(iterator it,const_iterator first,const_iterator last):向量中迭代器指向元素前插入另一个相同类型向量的[first,last)间的数据

3.删除函数
iterator erase(iterator it):删除向量中迭代器指向元素
iterator erase(iterator first,iterator last):删除向量中[first,last)中元素
void pop_back():删除向量中最后一个元素
void clear():清空向量中所有元素

4.遍历函数
reference at(int pos):返回pos位置元素的引用
reference front():返回首元素的引用
reference back():返回尾元素的引用
iterator begin():返回向量头指针,指向第一个元素
iterator end():返回向量尾指针,指向向量最后一个元素的下一个位置
reverse_iterator rbegin():反向迭代器,指向最后一个元素
reverse_iterator rend():反向迭代器,指向第一个元素之前的位置

5.判断函数
bool empty() const:判断向量是否为空,若为空,则向量中无元素

6.大小函数
int size() const:返回向量中元素的个数
int capacity() const:返回当前向量张红所能容纳的最大元素值
int max_size() const:返回最大可允许的vector元素数量值

7.其他函数
void swap(vector&):交换两个同类型向量的数据
void assign(int n,const T& x):设置向量中第n个元素的值为x
void assign(const_iterator first,const_iterator last):向量中[first,last)中元素设置成当前向量元素

8.看着清楚
1.push_back 在数组的最后添加一个数据
2.pop_back 去掉数组的最后一个数据
3.at 得到编号位置的数据
4.begin 得到数组头的指针
5.end 得到数组的最后一个单元+1的指针
6.front 得到数组头的引用
7.back 得到数组的最后一个单元的引用
8.max_size 得到vector最大可以是多大
9.capacity 当前vector分配的大小
10.size 当前使用数据的大小
11.resize 改变当前使用数据的大小,如果它比当前使用的大,者填充默认值
12.reserve 改变当前vecotr所分配空间的大小
13.erase 删除指针指向的数据项
14.clear 清空当前的vector
15.rbegin 将vector反转后的开始指针返回(其实就是原来的end-1)
16.rend 将vector反转构的结束指针返回(其实就是原来的begin-1)
17.empty 判断vector是否为空
18.swap 与另一个vector交换数据

2.1.4、基本用法

#include < vector> 
using namespace std;

2.1.5、简单介绍

  1. Vector<类型>标识符
  2. Vector<类型>标识符(最大容量)
  3. Vector<类型>标识符(最大容量,初始所有值)
  4. Int i[5]={1,2,3,4,5} 
    Vector<类型>vi(I,i+2);//得到i索引值为3以后的值
  5. Vector< vector< int> >v; 二维向量//这里最外的<>要有空格。否则在比较旧的编译器下无法通过
#include <QCoreApplication>
#include <vector>
using namespace  std;
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    //! 创建一份向量存储容器int
    vector<int> obj;
    //! 通过push_back在数组后面添加数据
    for (int i= 0; i < 10; i++){
        obj.push_back(i);
    }
    //! 通过pop_back去除数组最后5个数据
    for (int i = 0; i < 5; i++){
        obj.pop_back();
    }
    //! size容器中实际数据个数
    for (int i = 0; i < obj.size(); i++){
        qDebug("%d",obj[i]);
    }

    qDebug("%d",obj.max_size());
    qDebug("%d",obj.at(1));

    return a.exec();
}
输出结果:
0
1
2
3
4
-1
1

2.2 列表list容器

列表list的特点:

1)双向链表组成。

 2)其数据由若干个节点构成,每个节点包括一个信息块(即实际存储的数据)、一个前驱指针和一个后驱指针。可以向前也可以向后访问,但不能随机访问;

3)根据其结构可知随机检索的性能很差,vector是直接找到元素的地址,而它需要从头开始按顺序依次查找,因此检索靠后的元素时非常耗时;

 4)可以不使用连续的内存空间,这样可以随意进行动态操作;

5)可以在内部的任意位置快速插入或删除,也可以在两端进行操作;

6)  不能进行内部随机访问,即不支持【】操作和vector.at();

7)相对于vector要占用更多内存。

//初始化
list<int> lst1; //创建空list
list<int> lst2(3); //创建含有三个元素的list
list<int> lst3(3,2); //创建含有三个元素的值为2的list
list<int> lst4(lst2); //使用lst2初始化lst4
 
//常用的操作方法
lst1.assign(lst2.begin(),lst2.end()); //分配值
lst1.push_back(10); //添加值
lst1.pop_back(); //删除末尾值
lst1.begin(); //返回首值的迭代器
lst1.end(); //返回末尾位置下一位的迭代器
lst1.clear();//清空值
bool isEmpty1 = lst1.empty(); //判断为空
lst1.erase(lst1.begin(),lst1.end()); //删除元素
lst1.front(); //返回第一个元素的引用
lst1.back(); //返回最后一个元素的引用
lst1.insert(lst1.begin(),3,2); //从指定位置插入3个值为2的元素
lst1.rbegin(); //返回第一个元素的前向指针
lst1.remove(2); //相同的元素全部删除
lst1.reverse(); //反转
lst1.size(); //含有元素个数
lst1.sort(); //排序
lst1.unique(); //删除相邻重复元素
#include <QCoreApplication>
#include <list>
using namespace  std;
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    //! 创建一个列表
    list<int> link;

    //! 增加元素
    for (int i=0;i<10;i++){
        link.push_front(i);
    }

    //! 读取元素
    list<int>::iterator p = link.begin();

    while(p != link.end()){
        qDebug("%d",*p);
        p++;
    }

    //! 删除一个元素
    link.remove(5);

    p = link.begin();
    while(p != link.end()){
        qDebug("%d",*p);
        p++;
    }
    return a.exec();
}

 2.3 双端队列deque容器

 双端队列deque类由双端队列组成,允许在队列两端进行操作。双端队列支持随机访问迭代器,也支持下标进行操作【】进行访问。

#include <QCoreApplication>
#include <queue>
using namespace  std;
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    //! 创建一个双端队列
    deque<int> d;
    
    //! 初始化
    d.push_back(1);
    d.push_back(2);
    d.push_back(3);
    d.push_front(10);//d.insert(d.begin()+1, 10);

    //! 读取内容
    for(int i = 0; i < d.size(); i ++)
        qDebug("%d",d[i]);

    deque<int>::iterator it;
    for(it = d.begin(); it != d.end(); it ++)
        qDebug("%d",*it);

    deque<int>::reverse_iterator rit;
    for(rit = d.rbegin(); rit != d.rend(); rit ++)
        qDebug("%d",*rit);

    return a.exec();
}
输出结果是:
10
1
2
3
10
1
2
3
3
2
1
10

三、关联容器

关联容器能通过关键字直接访问。

3.1 映射(map)

映射map就是标准模板库中的一个关联容器,它提供一对一的数据处理能力。map的元素是由key和value两个分量组成的对偶(key,value)。元素的键key是唯一的,给定一个key,就能唯一地确定与其相关联的另一个分量value。

例如:map(string,string)mapstu;

1.使用map要包含头文件 #include<map>

2.映射(map)和多重映射(multimap)是基于某一类新key的键集的存在,提供对TYPE类型的数据进行快速和高效的检索。multimap允许键值重复,map不允许。键和值的数据类型是不相同的。

3.map的构造函数和析构函数        

map c       产生一个空的map/multimap,其中不含任何元素
map c(op)       以op为排序准则,产生一个空的map/multimap
map c1(c2)      产生某个map/multimap的副本,所有元素均被复制
map c(beg,end)       以区间[beg,end]内的元素产生一个map/multimap
map c(beg,end,op)     以op为排序准则,利用[beg,end]内的元素生成一个map/multimap
c.~map()      销毁所有元素,释放内存
以上map可以是以下形式
map<key,elem>        一个map,以less<>(operator<)为排序准则
map<key,elem,op>     一个map,以op为排序准则
multimap<key,elem>    一个multimap,以less<>(operator<)为排序准则
multimap<key,elem,op>     一个multimap,以op为排序准则

4.map的主要成员函数       

iterator begin()       返回指向第一个元素的迭代器
iterator end()       返回指向末尾的迭代器(最后一个元素之后)
void clear()         清空容器
bool empty()       判断是否为空
insert(pair<key,value> &elem)        插入一个pair类型的元素,返回插入元素的位置
iterator insert(iterator pos,pair<key,value> &elem)         插入一个pair类型的元素,返回插入元素的位置,pos是插入操作的搜寻起点
void insert(iterator start,iterator end)         插入[start,end)之间的元素到容器中
void erase(iterator loc)         删除loc所指元素
void erase(iterator start,iterator end)       删除[start,end)之间的元素
size_type erase(constkeytype &key)       删除key值为key的元素,并返回被删除元素的个数
iterator find(const keytype &key)        返回一个迭代器,指向键值为key的元素,如果没有找到返回end()
size_type count(const keytype &key)   返回键值等于key的元素的个数
size_type size()        返回元素的个数
void swap(map &from)        交换两个map中的元素

4.元素的访问

  map<string,float>::iterator pos;
 

#include <iostream>
using namespace  std;
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    //! 声明一个map容器
    map<string,string> mapstu;

    map<string,string>::iterator it;
    pair<map<string,string>::iterator,bool> ins_pair;
    ins_pair = mapstu.insert(pair<string,string>("001","sx1"));//! 向容器插入元素
    if(!ins_pair.second){
        qDebug("插入失败");
    }
    ins_pair = mapstu.insert(pair<string,string>("002","sx2"));
    if(!ins_pair.second){
        qDebug("插入失败");
    }
    ins_pair = mapstu.insert(pair<string,string>("003","sx3"));
    if(!ins_pair.second){
        qDebug("插入失败");
    }

    for (it = mapstu.begin(); it != mapstu.end(); it++){
        qDebug("%s-%s",it->first.c_str(),it->second.c_str());
    }

    return a.exec();
}

3.2 集合(set)

关于set,必须说明的是set关联式容器。set作为一个容器也是用来存储同一数据类型的数据类型,并且能从一个数据集合中取出数据,在set中每个元素的值都唯一,而且系统能根据元素的值自动进行排序。应该注意的是set中数元素的值不能直接被改变。C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB树(Red-Black Tree)。RB树的统计性能要好于一般平衡二叉树,所以被STL选择作为了关联容器的内部结构。

 关于set有下面几个问题:

(1)为何map和set的插入删除效率比用其他序列容器高?

大部分人说,很简单,因为对于关联容器来说,不需要做内存拷贝和内存移动。说对了,确实如此。set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。结构图可能如下:

  A
   / \
  B C
 / \ / \
  D E F G

因此插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点也OK了。这里的一切操作就是指针换来换去,和内存移动没有关系。

(2)为何每次insert之后,以前保存的iterator不会失效?

iterator这里就相当于指向节点的指针,内存没有变,指向内存的指针怎么会失效呢(当然被删除的那个元素本身已经失效了)。相对于vector来说,每一次删除和插入,指针都有可能失效,调用push_back在尾部插入也是如此。因为为了保证内部数据的连续存放,iterator指向的那块内存在删除和插入过程中可能已经被其他内存覆盖或者内存已经被释放了。即使时push_back的时候,容器内部空间可能不够,需要一块新的更大的内存,只有把以前的内存释放,申请新的更大的内存,复制已有的数据元素到新的内存,最后把需要插入的元素放到最后,那么以前的内存指针自然就不可用了。特别时在和find等算法在一起使用的时候,牢记这个原则:不要使用过期的iterator。

(3)当数据元素增多时,set的插入和搜索速度变化如何?

如果你知道log2的关系你应该就彻底了解这个答案。在set中查找是使用二分查找,也就是说,如果有16个元素,最多需要比较4次就能找到结果,有32个元素,最多比较5次。那么有10000个呢?最多比较的次数为log10000,最多为14次,如果是20000个元素呢?最多不过15次。看见了吧,当数据量增大一倍的时候,搜索次数只不过多了1次,多了1/14的搜索时间而已。你明白这个道理后,就可以安心往里面放入元素了。

2.set中常用的方法

begin()        ,返回set容器的第一个元素

end()      ,返回set容器的最后一个元素

clear()          ,删除set容器中的所有的元素

empty()    ,判断set容器是否为空

max_size()   ,返回set容器可能包含的元素最大个数

size()      ,返回当前set容器中的元素个数

rbegin     ,返回的值和end()相同

rend()     ,返回的值和rbegin()相同
#include <QCoreApplication>
#include <set>
#include <string>
#include <iostream>
using namespace  std;
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    set<int>s;
    int n = 10;
    for(int i=1;i<=n;i++)
    {
        s.insert(i);
    }
    set<int>::iterator it;
    for(it=s.begin ();it!=s.end ();it++)
    {
        printf("%d\n",*it);
    }

    //s.end()没有值
    cout<<"s.begain()   "<<*s.begin ()<<endl;
    //lower_bound()--返回指向大于(或等于)某值的第一个元素的迭代器
    cout<<"lower_buond  3  "<<*s.lower_bound (3)<<endl;

    //upper_bound()--返回大于某个值元素的迭代器
    cout<<"upper_bound  3  "<<*s.upper_bound (3)<<endl;

    //find()--返回一个指向被查找到元素的迭代器
    cout<<"find()  3   "<<*s.find (3)<<endl;

    cout<<"s.size()  "<<s.size ()<<endl;

    return a.exec();
}

四、容器适配器

五、迭代器

迭代器(iterators)是STL的一个重要组成部分。每个容器都有自己的迭代器,可以把迭代器看做一个容器使用的特殊指针,可以用来存取容器内存储的数据。

格式:<容器名> <数据类型>iterator 迭代器变量

迭代器有5种,分别是输入、输出、向前、双向和随机存取。

1) 输入迭代器:只能向前移动,每次只能移动一步,只能读迭代器指向的数据,而且只能读一次。

2)输出迭代器:只能向前移动,每次只能移动一次,只能写迭代器指向的数据,而且只能写一次。

3)前向迭代器:不仅具有输入和输出功能,还具有多次读或者写功能;

4) 双向迭代器:以前向迭代器为基础加上向后移动的能力;

5)随机访问迭代器:为双向迭代器加上迭代器运算的能力,即有向前或向后跳转一个任意的距离的能力。

迭代器是一种检查容器内元素并遍历元素的数据类型。C++更趋向于使用迭代器而不是下标操作,因为标准库为每一种标准容器(如vector)定义了一种迭代器类型,而只用少数容器(如vector)支持下标操作访问容器元素。

5.1 .定义和初始化

 每种容器都定义了自己的迭代器类型,如vector:vector<int>::iterator iter; //定义一个名为iter的变量

 每种容器都定义了一对名为begin和en的函数,用于返回迭代器。下面对迭代器进行初始化操作:vector<int> ivec;
vector<int>::iterator iter1=ivec.bengin(); //将迭代器iter1初始化为指向ivec容器的第一个元素
vector<int>::iterator iter2=ivec.end(); //将迭代器iter2初始化为指向ivec容器的最后一个元素的下一个位置

注意end并不指向容器的任何元素,而是指向容器的最后元素的下一位置,称为超出末端迭代器。如果vector为空,则begin返回的迭代器和end返回的迭代器相同。一旦向上面这样定义和初始化,就相当于把该迭代器和容器进行了某种关联,就像把一个指针初始化为指向某一空间地址一样。

5.2 .常用操作

iter+n     //在迭代器上加(减)整数n,将产生指向容器中钱前面(后面)第n个元素的迭代器。新计算出来的迭代器必须指向容器中的元素或超出容器末端的下一个元素
iter-n

iter1+=iter2        //将iter1加上或减去iter2的运算结果赋给iter1。两个迭代器必须指向容器中的元素或超出容器末端的下一个元素
iter1-=iter2

iter1-iter2            //两个迭代器的减法,得出两个迭代器的距离。两个迭代器必须指向容器中的元素或超出容器末端的下一个元素

>,>=,<,<=        //元素靠后的迭代器大于靠前的迭代器。两个迭代器必须指向容器中的元素或超出容器末端的下一个元素
*iter                //对iter进行解引用,返回迭代器iter指向的元素的引用
iter->men            //对iter进行解引用,获取指定元素中名为men的成员。等效于(*iter).men
++iter                //给iter加1,使其指向容器的下一个元素
iter++
--iter                //给iter减1,使其指向容器的前一个元素
iter--
iter1==iter2        //比较两个迭代器是否相等,当它们指向同一个容器的同一个元素或者都指向同同一个容器的超出末端的下一个位置时,它们相等 
iter1!=iter2 

5.3、迭代器const_iterator

每种容器还定义了一种名为const_iterator的类型。该类型的迭代器只能读取容器中的元素,不能用于改变其值。之前的例子中,普通的迭代器可以对容器中的元素进行解引用并修改,而const_iterator类型的迭代器只能用于读不能进行重写。例如可以进行如下操作:

for(vector<int>::const_iterator iter=ivec.begin();iter!=ivec.end();++iter)
     cout<<*iter<<endl;       //合法,读取容器中元素值

for(vector<int>::const_iterator iter=ivec.begin();iter!=ivec.end();++iter)
    *iter=0;        //不合法,不能进行写操作

const_iterator和const  iterator是不一样的,后者对迭代器进行声明时,必须对迭代器进行初始化,并且一旦初始化后就不能修改其值。这有点像常量指针和指针常量的关系。例如:

vector<int>    ivec(10);
const    vector<int>::iterator    iter=ivec.begin();
*iter=0;    //合法,可以改变其指向的元素的值
++iter;    //不合法,无法改变其指向的位置

5.4 例子

#include <QCoreApplication>
#include <vector>
using namespace  std;
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    //! 创建一份向量存储容器int
    vector<int> obj;
    //! 通过push_back在数组后面添加数据
    for (int i= 0; i < 10; i++){
        obj.push_back(i);
    }
    //! 通过pop_back去除数组最后5个数据
    for (int i = 0; i < 5; i++){
        obj.pop_back();
    }
//    //! size容器中实际数据个数
//    for (int i = 0; i < obj.size(); i++){
//        qDebug("%d",obj[i]);
//    }

    for(vector<int>::iterator iter=obj.begin();iter != obj.end();++iter){
        qDebug("%d",*iter);
    }

    return a.exec();
}
输出结果是:
0
1
2
3
4

6. STL标准容器的选择

1. 除非有很好的理由选择其他容器, 否则应使用 vector.

2.如果有很多小的元素, 且空间的额外开销很重要, 则不要使用 list 或 forward_list.

3.如果程序要求随机访问元素, 应使用 vector 或 deque.

4.如果程序需要在头尾 插入/删除 元素,且不会在中间插入元素, 则使用 deque.

5.如果需要在中间插入元素, 应使用 list 或 forwar_list.

6.如果程序需要在中间插入元素, 且随后需要随机访问元素, 则可以考虑在输入时使用 list, 再拷贝到 vector 中

7.set 的 插入/删除 查找效率非常高, 但不支持随机访问

PS.如果实在不确定使用哪种元素, 那么可以在程序中只使用 vector 与list.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值