美文网首页
C++容器和STL, since 2020-12-08

C++容器和STL, since 2020-12-08

作者: Mc杰夫 | 来源:发表于2020-12-08 21:31 被阅读0次

    (2020.12.08 Tues)
    STL容器允许重复利用已有的实现构造自己特定类型下的数据结构,通过设置一些模板类,这些模板的参数允许用户指定容器中元素的数据类型,从而提高编程效率。

    (容器部分的内容可以对比python中的list, tuple, dict, set等数据结构。)
    容器主要由头文件<vector>, <list>, <deque>, <set>, <map>, <stack>和<queue>组成。

    数据结构 描述 实现头文件
    向量(vector) 连续存储的元素 <vector>
    列表(list) 由节点组成的双向链表,每个结点包含着一个元素 <list>
    双队列(deque) 连续存储的指向不同元素的指针所组成的数组 <deque>
    集合(set) 由节点组成的红黑树,每个结点都包含着一个元素,节点之间以某种作用于元素对的谓词排列,没有两个不同的元素能够拥有相同的次序 <set>
    多重集合(multiset) 允许存在两个次序相等的元素的集合 <set>
    栈(stack) 后进先出的值的排列 <stack>
    队列(queue) 先进先出的值的排列 <queue>
    优先队列(priority queue) 元素的次序是由作用于所存储的值对上的某种谓词决定的一种队列 <queue>
    映射(map) 由{键,值}对组成的集合,以某种作用于键对上的谓词排列 <map>
    多重映射(multimap) 允许键对有相等的次序的映射 <map>

    (2020.12.09 Wed)
    STL定义的通用容器分为顺序容器(sequence container)关联容器(associative container)容器适配器

    顺序容器

    顺序容器是一种各元素间有顺序关系的线性表,是线性结构的可序群集,其中的每一个元素有固定的位置,除非用删除或插入的操作改变变量的位置。顺序容器具有插入速度快但查找操作相对较慢的特征。C++ STL(标准模板库)提供3中顺序容器:vector、list和deque。vector和deque类时以数组为基础,list类是以双向链表为基础。

    顺序容器中各种类型的比较

    vector是动态顺序容器,有连续内存地址的数据结构。相比于数组,vector会消耗更多的内存以有效地动态增长。而相比于其他顺序容器(deques, list),vector能更快的索引元素(如同数组一样),而且能相对高效的在尾部插入和删除元素。在其他位置删除和插入元素,效率没有这些容器高。

    list是STL实现的双向链表,相比vector的连续线性空间,list复杂太多,它允许快速的插入和删除,但是随机访问却比较慢。它的数据有若干个节点构成,每个节点包括一个信息块、一个前驱指针和一个后驱指针,可以向前或向后进行访问,但不能随机访问。好处是每次插入或删除会配置或者释放一个元素空间,对空间的运用绝对精准。

    deque(double ended queues双向队列)和vector相似,但它允许在容器头部快速插入和删除,如同在尾部一样。

    向量vector

    使用vector的时候,需包含头文件: <vector>,一般还要加上'using namespace std;',如果不加,调用的时候必须加std::vector<..>这样的形式,加了std::表示运用的是std命令空间的vector容器。
    向量声明和初始化

    语句 作用
    vector<元素类型>向量对象名; 创建一个没有任何元素的空向量对象
    vector<元素类型>向量对象名(size); 创建一个大小为size的向量对象
    vector<元素类型>向量对象名(n,初始值); 创建一个大小为n的向量对象,并初始化
    vector<元素类型>向量对象名(begin, end); 创建一个向量对象,初始化该对象(begin, end)中的元素
    vector<int> a;
    vector<float> a(10); //初始大小为10的向量
    vector<float> a(10,1);//初始大小为10,初始值都是1的向量
    vector<int> b(a); //用向量a初始化向量b
    vector<int> b(a.begin(), a.begin()+3); //将a向量中第0到第2个作为向量b的初始值
    //可以用数组初始化向量
    int n[] = {1,2,3,4,5};
    vector<int> a(n,n+5); //n的前5个元素作为a的初值
    vector<in> a(&n[1], &n[4]); //n[1]到n[4]范围内的元素作为向量a的初值
    

    元素的输入和访问

    #include <iostream>
    #include <vector>
    using namespace std;
    int main() 
    {
        vector<int> a(10,1);
        int i;
        cout << 'initialisaiton:' << endl;
        cout << 'insert data:' << endl;
        cin >> a[2];
        cin >> a[5];
        cin >> a[8];
        cout << '赋值后遍历:'<<endl;
        for (i = 0; i < a.size(); i++)
        { 
            cout<<a[i]<<endl; //以a[n]的方式访问向量的第n个元素
        }
    }
    

    基本操作

    语句 作用
    a.insert(position, value) 将数值的一个拷贝插入到有position指定的位置上,并返回新元素的位置
    a.insert(position,n,value) 将数值的n个拷贝插入到position指定的位置上
    a.insert(position,beg,end) 将从beg到end-1之间的所有元素的拷贝插入到a中由position指定的位置上
    a.push_back(value) 在尾部插入
    a.pop_back() 删除最后元素
    a.resize(num) 将元素个数改为num
    a.resize(num,value) 将元素个数改为num,如果size()增加,默认的构造函数将这些新元素初始化(用value?)
    a.clear() 删除容器中所有元素
    a.erase(position) 删除由position指定的位置上的元素
    a.erase(beg, end) 删除beg到end-1之间的所有元素
    a.empty() 判断是否为空
    a.size(), a.max_size() 向量a的大小和最大容量
    a.capacity() 向量a的真实大小

    以迭代器访问向量

    vector<int> a(10,5);
    vector<int>::iterator iter;
    for (iter = a.begin(); it != a.end(); iter++)
    {
       cout<<*iter<<endl;
    }
    

    因为迭代器是一个定义在vector类中的typedef,所以必须使用容器名vector、容器元素类型和作用域符来使用iterator。

    迭代器是一个指针,用来存取容器中的数据元素,因此迭代器的操作和指针的操作相同。

    二维向量
    vector<vector<int>> a(3,vector<int>(4)); //相当于数组a[3][4],都是空
    vector<vector<int>> a(3, vector<int>(4,0)); //相当于a[3][4],都是0
    for(int m = 0; m < a.size(); m++) //a.size()获取行向量大小
    {
        for (int n = 0; n < a[3].size(); n++) //a[n].size()获取向量中具体每个向量的大小
        {
            cout<<a[m][n]<<endl;
         }
    }
    

    列表list

    STL实现的双向链表,相比vector的连续线性空间,list复杂太多,它允许快速的插入和删除,但是随机访问却比较慢。它的数据有若干个节点构成,每个节点包括一个信息块、一个前驱指针和一个后驱指针,可以向前或向后进行访问,但不能随机访问。好处是每次插入或删除会配置或者释放一个元素空间,对空间的运用绝对精准。列表的定义在头文件<list>中。
    其定义方式和vector相似

    list<int> lst1;
    list<int> lst2(3);
    list<int> lst3(3,2); //三个元素的list,初始值都是2
    list<int> lst4(lst2); 
    list<int> lst5(lst.begin(), lst.end()); //用lst初始化lst5
    lst.push_front(8); //头部加入元素
    lst.pop_front(); //头部元素删除
    lst.push_back(xx); //尾部加入元素
    lst.pop_back(); //尾部元素删除
    

    遍历list

    iterator begin(); //返回指向第一个元素的迭代器
    iterator end();
    reverse_iterator rbegin(); //返回指向第一个元素的逆向迭代器
    reverse_iterator rend();
    // 如下
    for (list<int>::reverse_iterator rit = lst.rbegin(); rit != lst.rend(); ++rit)
    {
        cout<<**rit<<endl;
    }
    

    列表容器的操作

    lst.empty(); //lst为空时返回true
    lst.size(); //返回list中元素的个数
    lst.max_size(); //返回lst最大能容纳的元素个数
    //为容器添加新内容
    lst.assign(InputIterator first, InputIterator last);
    lst.assign(size_type a, const value_type& val); //给lst赋值n个值为val的元素
    //
    list<int> lst;
    list<int> lst2;
    list<int> lst3;
    lst.assign(5,10); //添加5元素,值为10
    lst2.assign(lst.begin(), lst.end()); //用lst为lst2赋值
    int a[]={1,2,3,4};
    lst3.assign(a,a+4); //将a的元素都赋值给lst3
    

    插入元素
    (2020.12.10 Thur)
    list中插入元素需要成员函数insert()实现。分为若干版本。

    iterator insert(iterator position, const value& val); 
    //position是要插入这个list的迭代器,val是要插入的值,比如下面这个例子
    list<int> lst;
    list<int>::iterator it;
    it = lst.begin();
    lst.inset(it, 9);
    
    iterator insert(iterator position, size_type n, const value& val);
    //插入n个值为val的元素
    lst.insert(it, 2, 29); //在迭代器指针it所指的位置加入2个29
    
    template <class InputIterator>;
    void insert (iterator position, InputIterator first, InputIterator last);
    //first和last是选择的把值插入到这个list中的值所在的容器的迭代器
    list<int> lst;
    list<int>::iterator it;
    vector<int> v(2,39);
    lst.insert(it, v.begin(), v.end());
    

    删除元素

    iterator erase(iterator position);
    iterator erase(iterator first, iterator last); //删除[first, last)中的值,可以不用变量接收其返回值
    list<int> lst(5,10);
    list<int>::iterator it1,it2;
    it1 = lst.begin();
    it2 = lst.end();
    it1 = lst.erase(it1);
    lst.erase(it1, it2);
    lst.erase(lst.begin(), lst.end()); //清空lst
    

    双队列deque

    deque(double ended queue)双向队列,和vector相似,但是它允许在容器头部快速插入和删除,就像在尾部一样。
    deque结合了vector和list的优缺点,优化了的对序列两端元素进行添加和删除操作的基本序列容器。

    它允许较为快速的随机访问,但不像vector那样把所有的对象保存在一个连续的内存块,而是采用多个连续的存储块,并且在一个映射结构中保存对这些块及其顺序的跟踪。向deque两端添加或删除元素的开销很小,不需要重新分配空间,所以向末端添加元素比vector更高效。

    关联容器

    关联容器中的元素通过关键字来保存和访问,主要有映射(map)和集合(set)两类,支持通过键来高效的查找和读取元素。map的元素以键-值对(map-value)的形式组织,键用作元素在map类型下进行索引,而值则表示所存储和读取的数据。set仅包含一个键,并有效的支持关于某个键是否存在的查询。set和map类型的对象不允许为同一个键添加第二个元素。如果一个键必须有多个实例,则需使用多重映射(multimap)或多重集合(multiset)类型,其允许多个元素拥有相同的键。

    (2020.12.11 Fri)

    映射map

    map是STL的一个关联容器,提供一对一的数据处理能力,其元素由key和value两个分量足成的对偶(key, value)。key唯一,给定一个key就能唯一确定一个value。map的类型通常称为关联数组,和数组很相似,但下标是关键字key而非整数。map类型定义在头文件#include <map>中。

    map是有序的且不允许重复key(关键字)的关联容器。有序的实现时依靠关键字类型中的'<'来实现。

    #include <map>
    map<key_type, value_type> tmpMap; //初始化,定义一个空map
    map<key_type, value_type> tmpMap{
        {key1, value1},
        {key2, value2},
        ...
    };   //注意结尾有个分号;
    map<key_type, value_type> tmpMap(existingMap); //从已有的一个map复制过来
    map<key_type, value_type> tmpMap(x, y); //x, y分别是已有map对象的迭代器范围
    map1 = map2; //直接复制
    
    类型别名 说明
    key_type 关键字类型
    value_type pair<const key_type, mapped_type>
    mapped_type 关键字关联的类型
    map<int, string> myMap;
    myMap::value_type a; //a为pair<const int, string>类型
    myMap::key_type b; //b为int类型
    myMap::mapped_type c; //c为string类型
    
    pair类型

    pair标准类型被包含在头文件#include <utility>中,一个pair保存两个数据成员,pair是一个用来生成特定类型的模板。当创建一个pair时,用户必须提供两个类型名,pair的数据成员将具有对应的类型。一般来说,一个pair类型的对象,存储的是一个键值对(key-value)。

    pair<string, string> a; //保存两个string
    pair<string, size_t> b; //保存一个string和size_t
    pair<int, vector<int>> c; //
    pair<string, string> author{'hello', 'world'}; //初始化一个pair,名author,两个初值是hello world
    pair<string, string> author('hello', world'); //同上
    

    pair的数据成员是public,成员名为first和second,用户可以用普通的成员访问符'.'来进行访问。

    操作 说明
    pair<T1, T2> p; p的成员数据类型分别是T1和T2,默认初始化
    pair<T1, T2> p(v1,v2); 同上,使用v1,v2进行初始化
    pair<T1, T2> p = {v1, v2}; 等价上式
    make_pair(v1, v2); 返回一个v1和v2初始化的pair,其类型由v1和v2推断而来
    p.first 返回p的first成员
    p.second 返回p的second成员
    p1 relop p2 执行关系运算,利用数据类型中的关系运算
    p1 == p2 相等性判断,必须first和second同时满足要求
    p1!=p2 不等判断

    pair对象也可以作为函数的返回。

    map的使用

    插入数据

    #include <map>
    map<int, string> m;
    m.insert(pair<int, string>(1,'m_first')); //插入数据
    m.insert(pair<int, string>(2,'m_second'));
    map<int, string>::iterator iter;
    for (iter = m.begin(); iter!= m.end(); iter++)
    { 
        cour<< iter->first << ' '<< iter->second<< endl;
    }
    

    插入value_type数据,value_type类型代表的是这个容器中元素的类型

    map<string, string> m;
    m.insert(map<string, string>::value_type('001', 'm_first')); //插入第一个value_type数据
    m.insert(map<string, string>::value_type('002', 'm_second')); //插入第二个value_type数据
    

    用key键为map赋值

    map<string, string> m;
    m['both'] = 1;
    int a = m['both'];
    

    其他指令

    m.size(); //map m的数据数量
     //遍历,正向
    for (map<int, string>::iterator iter = m.begin(); iter!=m.end() ; iter++)
    {
        cout<<iter->first << ' ' << iter->second<< endl;
    }
     //遍历,反向
    for (map<int, string>::iterator iter = m.rbegin(); iter!=m.rend() ; iter++)
    {
        cout<<iter->first << ' ' << iter->second<< endl;
    }
    //计数和查找
    m.count(k); //m中key k的个数,0或者1
    m.find(k); //找到m中的k索引的元素,并返回指向该元素的迭代器
    //删除
    m.erase(k); //删除key为k的元素,返回size_type的类型,表示删除的元素个数
    m.erase(b, e); //删除迭代器b和e中间的元素,返回void型
    m.erase(p); //删除迭代器p指向的元素,返回void型
    
    set类容器

    set只是单纯的key的集合。当只想知道一个key是否存在时,使用set容器最合适。set容器支持大多数map的操作,但不支持下标操作,没有定义mapped_type类型。在set类型中,value_type不是pair类型,而是与key_type类型相同。set容器中存储的key也是唯一的。

    #include <set>
    vector<int> ivec;
    for (...)
    { ivec.push_back(...);}
    set<int> iset(ivec.begin(), ivec.end());
    cout<<iset.size()<<endl; //返回set的尺寸
    iset.insert('the'); // 添加元素
    iset.find('a');
    iset.count('a');
    

    容器适配器

    用基本容器实现新的容器,用于描述更高级的数据结构。

    容器适配器有三种stack, queue和priority_queue。默认情况下stack衍生自deque。queue也同样来自deque。

    种类 默认顺序容器 可用顺序容器 说明
    stack deque vector, list, deque
    queue deque list, deque 基础容器必须提供push_front()计算
    priority_queue vector vector, deque 基础容器必须提供随机访问功能
    #include <stack>
    #include <queue> //调用queue和priority_queue时需要引入
    

    stack的成员函数中,s.pop()是删除栈顶元素,s.push()是在栈顶插入元素,s.top()获得指向栈顶元素的引用。

    queue的成员函数中,q.push()向队尾插入一个元素,q.pop()从队首删除元素,q.front()返回指向队首元素的引用,q.back()指向队尾元素的引用。

    priority_queue与queue的不同之处在于,包含最大值的元素位于队首,且只能在队首执行操作。

    #include <queue>
    priority_queue<int> q;
    q.push(1); //插入一个元素
    q.pop(); //删除队首的元素,即最大的元素
    q.size();
    q.empty();
    q.top(); //队列中最大元素的引用
    

    容器的选择

    1. vector的特点
      • 指定一块如同数组一样的连续存储,但空间可以动态扩展。它可以像数组一样操作,并可以动态操作。通常体现在push_back()和pop_back()
      • 随机访问方便,像数组一样被访问
      • 节省空间,因连续存储,在存储数据的区域都是没有被浪费的,但是要明确一点,vector大多情况下并不是满存的,在未存储的区域实际是浪费
      • 在内部进行插入、删除操作效率非常低,基本上是被禁止的。vector被设计成只能在后端进行追加和删除操作,原因是vector内部的实现是按照顺序表的原理
      • 只能在vector的最后进行push和pop,不能在头进行这个操作
      • 动态添加的数据超过vector默认分配的大小时,需要对内存重新分配、拷贝和释放,这个操作消耗性能。达到最佳性能,最好创建vector时指定空间大小
    2. list的特点
      • 不使用连续的内存空间,可以随意进行动态操作
      • 可以再内部任何位置快速的插入和删除,可在两端进行push和pop
      • 不能进行内部的随机访问
      • 比vector占用更多内存
    3. deque的特点
      • 随机访问方便,支持[ ]操作符,性能没有vector好
      • 可在内部进行插入和删除操作,性能不及list
      • 可以在两端进行push和pop
    4. vector/list/deque三者比较
      vector查询性能最好,在末端增加数据的性能也好,除非它重新申请内存段;适合高效的随机存储。
      list的插入和删除元素的性能最好,查询性能差;适合大量的插入和删除操作且不关心随机存储的需求。
      deque介于两者之间,比list更好查询,比vector更好插入和删除。如果用户需要随机存储又关心两端数据的插入和删除,deque最佳。

    Reference

    1. 聚慕课教育研发中心 编著,C++从入门到项目实践(超值版),清华大学出版社
    2. 刘蕾编著,21天学通C++(第五版),电子工业出版社

    相关文章

      网友评论

          本文标题:C++容器和STL, since 2020-12-08

          本文链接:https://www.haomeiwen.com/subject/rtgqgktx.html