【C++ 系列笔记】06 C++ STL - 常用容器

STL

介绍

简介

(Standard Template Library)标准模板库

广义上分为三类:

  • 容器(container)
  • 算法(algorithm)
  • 迭代器(iterator)

六大组件:

  • 容器

    一种类模板,有各种数据结构。

  • 算法

    一种函数模板,有各种常用算法。

  • 迭代器

    一种类模板,迭代器是容器与算法之间的胶合剂。

  • 仿函数(伪函数)

    一种类模板,用来协助算法完成不同的策略。

  • 适配器(配接器)

    一种用来修饰容器或者仿函数或迭代器接口的东西。

  • 空间配置器

    一种类模板,负责空间的配置和管理。

容器

两类:

  • 序列式容器

    如 Vector、Deque、List 等,序列式容器强调值的顺序,每个元素都有物理意义上固定的系列关系。

  • 关联式容器

    如 Set/multiset、Map/multimap 等,关联式容器是非线性的容器,元素之间没有顺序关系,例如树结构。

    JSON 就是关联式容器。

算法

两类:

  • 质变算法

    质变算法是运算过程会改变元素内容的算法,例如拷贝,替换,删除。

  • 非质变算法

    非质变算法是运算过程不会改变元素内容的算法,例如查找、计数、遍历。

迭代器

迭代器是依顺序访问某个容器所含的各个元素的方法。

种类:

  • 输入迭代器

    提供对数据的只读访问,支持 ++、==、!=

  • 输出迭代器

    提供只写访问,支持 ++

  • 前向迭代器

    提供读写访问,并且可以向前推进迭代器,支持 ++、==、!=

  • 双向迭代器(使用较多)

    提供读写访问,并且可以前后推进迭代器,支持 ++、==

  • 随机访问迭代器(使用较多)

    提供读写访问,并可以跳跃方式访问容器的任意数据,功能最强,

    支持 ++、–、[n]、-n、<、<=、>、>=

针对实现来说,有几种类型的迭代器:

  • iterator 普通迭代器
  • reverse_iterator 逆序迭代器
  • const_iterator 只读迭代器

常用容器

string 容器

  • assign 方法

    string& assign(const char* s,int n);
    // 将字符串 s 的前 n 个字符赋值给当前字符串
    
    string& assign(const strubg& s,int start, int n);
    // 将字符串 s 从 start 开始的 n 个字符赋值给当前字符串,n 从 0 开始
    
  • 字符串存取

    char& operator[](int n);
    // 访问越界就崩了
    char& at(int n);
    // 访问越界会抛出异常
    
  • 字符串查找

    正找:

    int find(const string& str, int pos = 0) const;
    // 从 pos 开始查找,返回 str 在当前字符串中第一次出现的位置。
    
    int find(const string& str, int pos, int n) const;
    // 从 pos 开始查找,返回 str 的前 n 个字符在当前字符串中第一次出现的位置。
    

    倒找:

    int rfind(const string& str, int pos = npos) const;
    // 从 pos 开始查找,返回 str 在当前字符串中最后一次出现的位置。
    
    int rfind(const string& str, int pos, int n) const;
    // 从 pos 开始查找,返回 str 的前 n 个字符在当前字符串中最后一次出现的位置。
    

    找不到返回 -1

  • 字符串替换

    string& replace(int pos, int n, const string& str);
    // 替换从 pos 开始的 n 个字符为字符串 str
    
  • 字符串截取(子串)

    string substr(int pos = 0, int n = npos) const;
    // 返回从 pos 开始的 n 个字符组成的字符串
    

    应用:

    获取两文本中间

    string getMiddleStr(string source,string left, string right) {
          
          
      return source.substr(source.find(left),
                           source.find(right) - source.find(left));
    }
    
  • 字符串插入和删除

    string& insert(int pos, const string& s);
    // 将 s 插入到当前字符串的 pos 位置后
    
    string& erase(int pos, int n = npos);
    // 删除从 pos 开始的 n 个字符
    
  • C-style 字符串转换

    const char* p = str.c_str();
    

vector 容器

(单端数组、动态数组)

vector 内部维护一段线性空间,当空间不足时会额外开辟一段连续的更大的空间,然后进行数据的拷贝,最后丢弃原空间。

他被称为单端数组,是因为其一端封闭,数据仅在另一端成长,这导致其头插数据的开销非常大,这个问题可由 deque 容器解决,deque 容器是双端数组,而且其拥有更小的扩容开销。

  • 头文件

    #include <vector>
    
  • 获得一个容器(构造函数)

    vector<type> v;
    // 默认构造
    
    vector<type> v(type* begin, type* end);
    // 将 begin 到 end 指向的内存范围拷贝给自身
    

    注意!end 指向的是尾元素的下一个位置。

    vector<type> v(size_t n, type elem);
    // 将 n 个 elem 拷贝给自身
    
  • 数据操作

    • 拷贝

      void assign(type* begin, type* end);
      // 将 begin 到 end 指向的内存范围拷贝给自身
      
      void assign(size_t n, type elem);
      // 将 n 个 elem 拷贝给自身
      
    • 交换

      void swap(vector& v2);
      // 互换两容器的内容
      

      swap 的应用:收缩内存

      vector 在缩小长度的时候并不会缩小容量(这个结论后面会通过代码检验),那么饿我们想要收缩容量,可以使用 swap。

      vector<int>(oldV).swap(oldV);
      

      resize 后,尾指针将移动至新范围的尾部,此时通过拷贝构造实例化的新对象便仅拷贝新范围内的数据,然后通过 swap 来交换容器的指针,就拿到收缩了内存的容器。

      此后匿名对象会自行销毁。

    • 是否为空

      bool empty();
      
    • 获取容量

      size_t capacity()
      
    • 获取大小

      size_t size()
      
    • 改变大小

      void resize(size_t newSize, type val);
      // 若容器变大,则多余位置由 val 填充
      
      void resize(size_t newSize);
      // 若容器变大,则多余位置由 0 填充
      
    • 预留内存

      void reserve(const size_t newcapacity);
      
    • 数据存取

      直接访问:

      type& operator[](size_t n);
      // 访问越界就崩了
      type& at(size_t n);
      // 访问越界会抛出异常
      

      获取首尾元素:

      type& front();
      // 返回容器的首个元素的引用
      
      type& back();
      // 返回容器的尾元素的引用
      

      尾插尾删:

      void push_back(type& ele);
      void pop_back();
      

      数据插入:

      iterator insert(iterator where, size_t count, type val);
      // 从 where 处前插 count 个值 val
      
      iterator insert(iterator where, type val);
      // 从 where 处前插 1 个值 val
      
      iterator insert(iterator where, iterator first, iterator last);
      // 将 first 到 last 范围内的数据插入至 where
      

      数据删除:

      iterator erase(iterator first, iterator last);
      // 删除迭代器从 first 到 last 之间的元素
      
      iterator erase(iterator where);
      // 删除迭代器指向的元素
      
      void clear();
      //删除容器中所有元素
      
  • 迭代器

    • 先拿到迭代器

      // 首指针
      vector<int>::iterator itBegin = v.begin();
      // 尾指针(最后一个元素的下一个位置)
      vector<int>::iterator itEnd = v.end();
      
    • 方法一

      vector<int>::iterator itBegin = v.begin();
      vector<int>::iterator itEnd = v.end();
      while (itBegin != itEnd) {
              
              
        cout << *itBegin++ << endl;
      }
      
    • 方法二

      for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
              
              
        clog << *it << endl;
      }
      
    • 方法三:算法

      #include <algorithm>
      // ...
      void callback(int value) {
              
              
        cout << value;
      }
      // ...
      for_each(v.begin(), v.end(), callback);
      
    • 方法四:

      for (auto& ele : v) {
              
              
        cout << ele << endl;
      }
      

      这是基于范围的 for 循环,C++11 的语句。

      另:

      为了能跟写 python 和 js 一样爽,我实现了一个 range 类。

      class range {
              
              
       private:
        long* startp;
        long* endp;
      
       public:
        long* begin() {
              
               return this->startp; }
        long* end() {
              
               return this->endp; }
        range(long min, long max) {
              
              
          if (min > max) {
              
              
            long temp = min;
            min = max;
            max = temp;
          }
          this->startp = new long[max - min + 2];
          this->endp = startp + max - min + 1;
          for (long ele = min; ele <= max; ele++) {
              
              
            startp[ele - min] = ele;
          }
        }
        ~range() {
              
               delete[] startp; }
      };
      

      当我们想要循环指定次数时再也不用写一长串了,只需要简单的:

      for (auto& ele : range(10, 20)) {
              
              
        cout << ele << endl;
      }
      
    • 逆序遍历:

      拿到迭代器 rbegin()rend()

      // 首指针
      vector<int>::reverse_iterator itBegin = v.rbegin();
      // 尾指针(最后一个元素的下一个位置)
      vector<int>::reverse_iterator itEnd = v.rend();
      

      迭代方法不变,例如:

      vector<int>::reverse_iterator itBegin = v.rbegin();
      vector<int>::reverse_iterator itEnd = v.rend();
      while (itBegin != itEnd) {
              
              
        cout << *itBegin++ << endl;
      }
      
    • 随机访问迭代器

      vector 的迭代器是随机访问迭代器,要判断某迭代器是否是随机访问迭代器,可用下述代码测试:

      iterator it = foo.begin();
      it = it + 3;
      

      编译通过说明是随机访问迭代器,不通过说明不是。

  • vector 的空间分配策略:

    vector 维护的是一段线性空间,当空间不足时会开辟一段连续的更大的空间,然后进行数据的拷贝。

    扩展空间时,扩展量大致是当前空间的 1.5 倍。

    不会缩小空间。

    测试代码:

    #include <iostream>
    #include <vector>
    using namespace std;
    int main() {
          
          
      vector<int> v;
      int oldCapacity = 0;
      // 添加数据
      for (auto& val : range(1, 100000)) {
          
          
        v.push_back(val);
        if (oldCapacity != v.capacity()) {
          
          
          cout << v.capacity() << endl;
        }
        oldCapacity = v.capacity();
      }
        
      cout << endl;
      // 删除数据
      for (auto& val : range(1, 100000)) {
          
          
        v.pop_back();
        if (oldCapacity != v.capacity()) {
          
          
          cout << v.capacity() << endl;
        }
        oldCapacity = v.capacity();
      }
    
      system("pause");
      return EXIT_SUCCESS;
    }
    

    输出:

    1
    2
    3
    4
    6
    9
    13
    19
    28
    42
    63
    94
    141
    211
    316
    474
    711
    1066
    1599
    2398
    3597
    5395
    8092
    12138
    18207
    27310
    40965
    61447
    92170
    138255

    请按任意键继续. . .

  • 注意!

    vector 分配空间的策略如此,当容器扩展后,所有迭代器将失效。

deque 容器

(双端数组、没有容量)

deque 是双端数组,其成长方向有两个,这使得其前后插入数据的开销是一个常数,解决了 vector 头插开销令人难以接受的问题。

deque 容器的扩容也与 vector 非常不同,vector 是不断地申请新空间,不断地对数据进行拷贝迁移。

而 deque 则是将新申请到的空间串接在一端,其内部维护着这些分段空间,维持着整体连续的假象。这也造成了其代码实现要比 vector 或 list 多得多,迭代器的架构也非常复杂。

  • 头文件

    #include <deque>
    
  • 获得一个容器(构造函数)

    deque<type> v;
    // 默认构造
    
    deque<type> v(type* begin, type* end);
    // 将 begin 到 end 指向的内存范围拷贝给自身
    

    注意!end 指向的是尾元素的下一个位置。

    deque<type> v(size_t n, type elem);
    // 将 n 个 elem 拷贝给自身
    
  • 数据操作

    • 拷贝

      void assign(type* begin, type* end);
      // 将 begin 到 end 指向的内存范围拷贝给自身
      
      void assign(size_t n, type elem);
      // 将 n 个 elem 拷贝给自身
      
    • 交换

      void swap(deque& v2);
      // 互换两容器的内容
      
    • 是否为空

      bool empty();
      
    • 获取大小

      size_t size()
      
    • 改变大小

      void resize(size_t newSize, type val);
      // 若容器变大,则多余位置由 val 填充
      
      void resize(size_t newSize);
      // 若容器变大,则多余位置由 0 填充
      
    • 数据存取

      直接访问:

      type& operator[](size_t n);
      // 访问越界就崩了
      type& at(size_t n);
      // 访问越界会抛出异常
      

      获取首尾元素:

      type& front();
      // 返回容器的首个元素的引用
      
      type& back();
      // 返回容器的尾元素的引用
      

      双端插入:

      void push_back(type& ele);
      void push_front(type& ele);
      void pop_back();
      void pop_front();
      

      数据插入:

      iterator insert(iterator where, size_t count, type val);
      // 从 where 处前插 count 个值 val
      
      iterator insert(iterator where, type val);
      // 从 where 处前插 1 个值 val
      
      iterator insert(iterator where, iterator first, iterator last);
      // 将 first 到 last 范围内的数据插入至 where
      

      数据删除:

      iterator erase(iterator first, iterator last);
      // 删除迭代器从 first 到 last 之间的元素
      
      iterator erase(iterator where);
      // 删除迭代器指向的元素
      
      void clear();
      // 删除容器中所有元素
      
  • 迭代器

    使用方法与 vector 相同,均为随机访问迭代器。

stack 容器

stack(栈),是一个栈结构的容器,不提供迭代器,也不提供遍历的方法。

stack 容器允许新增元素,移除元素,取得栈顶元素。

  • 头文件

    #include <stack>
    
  • 获得一个容器(构造函数)

    stack<type> v;
    // 默认构造
    
  • 数据操作

    • 是否为空

      bool empty();
      
    • 获取大小

      size_t size()
      
    • 数据存取

      void push(type& elem);
      // 压栈
      
      void pop();
      // 弹栈
      
      type& top();
      // 返回栈顶元素的引用
      
  • 迭代器

    stack 没有迭代器

queue 容器

queue (队列),是一个队列结构的容器,不提供迭代器,也不提供遍历的方法。

queue 容器允许从一端新增元素,从另一端移除元素。

  • 头文件

    #include <queue>
    
  • 获得一个容器(构造函数)

    queue<type> q;
    // 默认构造
    
  • 数据操作

    • 是否为空

      bool empty();
      
    • 获取大小

      size_t size()
      
    • 数据存取

      void push(type& elem);
      // 往队尾添加元素
      
      void pop();
      // 从队头移除第一个元素
      
      type& back();
      // 返回最后一个元素
      
      type& front();
      // 返回第一个元素
      
  • 迭代器

    queue 没有迭代器

list 容器

(双向循环链表)

list 容器是双向循环链表,插入和移除数据的开销是常数。

它提供的迭代器是双向迭代器,而不是随机访问迭代器。由于链表的结构特殊性,指向有效数据的迭代器永远不会失效。

  • 头文件

    #include <list>
    
  • 获得一个容器(构造函数)

    list<type> l;
    // 默认构造
    
    list<type> l(type* begin, type* end);
    // 将 begin 到 end 指向的内存范围拷贝给自身
    

    注意!end 指向的是尾元素的下一个位置。

    list<type> l(size_t n, type elem);
    // 将 n 个 elem 拷贝给自身
    
  • 数据操作

    • 拷贝

      void assign(type* begin, type* end);
      // 将 begin 到 end 指向的内存范围拷贝给自身
      
      void assign(size_t n, type elem);
      // 将 n 个 elem 拷贝给自身
      
    • 交换

      void swap(list& l);
      // 互换两容器的内容
      
    • 是否为空

      bool empty();
      
    • 获取大小

      size_t size()
      
    • 改变大小

      void resize(size_t newSize, type val);
      // 若容器变大,则多余位置由 val 填充
      
      void resize(size_t newSize);
      // 若容器变大,则多余位置由 0 填充
      
    • 数据存取

      获取首尾元素:

      type& front();
      // 返回容器的首个元素的引用
      
      type& back();
      // 返回容器的尾元素的引用
      

      双端插入:

      void push_back(type& ele);
      void push_front(type& ele);
      void pop_back();
      void pop_front();
      

      数据插入:

      iterator insert(iterator where, size_t count, type val);
      // 从 where 处前插 count 个值 val
      
      iterator insert(iterator where, type val);
      // 从 where 处前插 1 个值 val
      
      iterator insert(iterator where, iterator first, iterator last);
      // 将 first 到 last 范围内的数据插入至 where
      

      数据删除:

      iterator erase(iterator first, iterator last);
      // 删除迭代器从 first 到 last 之间的元素
      
      iterator erase(iterator where);
      // 删除迭代器指向的元素
      
      size_t remove(type& elem);
      // 删除容器中所有与 elem 值匹配的元素。
      // 注意!对于自定义类型,需要重载 == 号
      bool operator==(const Type& elem);
      
      void clear();
      // 删除容器中所有元素
      

      链表操作:

      void reverse();
      // 反转链表
      
      void sort();
      // 排序,从小到大
      
      void sort(funType* callback);
      // 排序,通过回调函数来排序
      

      注意!所有不提供随机访问迭代器的容器都不允许使用 STL 提供的算法。

  • 迭代器

    同期提供双向迭代器

set/multiset 容器

(关联式容器)

set 容器

set 容器的特点是,所有元素都会根据元素的键值自动排序,且不允许出现重复的键值。(可以插入重复的键值,但没用)

虽然提到键值,但其不像 map 容器一样是键值对,它的元素即是键也是值。

它的迭代器是 const_iterator,不允许修改,因为其元素关系到组织排序。

其指向有效数据的迭代器永远不会失效。

multiset 容器

multiset 容器的特点和用法与 set 容器几乎完全相同,唯一的不同点在于它允许键值重复,而 set 不允许。

两容器的底层结构是红黑树(一种平衡二叉树)。

  • 头文件

    #include <set>
    
  • 获得一个容器(构造函数)

    set<type> s;
    multiset<type> ms;
    // 默认构造
    
  • 数据操作

    • 交换

      void swap(list& l);
      // 互换两容器的内容
      
    • 是否为空

      bool empty();
      
    • 获取大小

      size_t size()
      
    • 改变大小

      void resize(size_t newSize, type val);
      // 若容器变大,则多余位置由 val 填充
      
      void resize(size_t newSize);
      // 若容器变大,则多余位置由 0 填充
      
    • 数据存取

      • 对祖:

        对组是库定义的一个模板类的实例,其中储存有两个数据,用于同时返回两个数据。

        类型为:

        pair<type1, type2>
        

        对于一个对祖,可以通过以下属性来获取其中存储的数据:

        p.first;
        p.second;
        

        分别对应 type1 和 type2。

        创建对祖:

        pair<int, string> p(10, "string");
        

        或者:

        pair<int, string> p = make_pair(10, "string");
        
      • 数据插入:

        pair<iterator, bool> insert(type elem);
            // 向容器中插入一个值
        
      • 数据删除:

        iterator erase(iterator first, iterator last);
            // 删除迭代器从 first 到 last 之间的元素
            
            iterator erase(iterator where);
            // 删除迭代器指向的元素
            
            size_t erase(type elem);
            // 删除容器中值为 elem 的元素
            
            void clear();
            // 删除容器中所有元素
        
      • set 查找操作:

        iterator find(type& key);
            // 查找 key 是否存在,返回迭代器,未找到范围 set.end()
            
            size_t count(type& key);
            // 返回 key 的元素个数
            
            iterator lower_bound(type& key);
            // 本意是返回第一个小于等于 key 的迭代器,但实际上结果与 find 相同
            
            iterator upper_bound(type& key);
            // 返回第一个大于 key 的迭代器。
            
            pair<iterator, iterator> equal_range(type& key);
            // 返回一个 pair,包含 lower_bound 和 upper_bound 的返回值
            
            // pair 类型为 
            pair<set<type>::iterator, set<type>::iterator> p;
            // 可以通过 p 拿到两个迭代器:
            p.first, p.second;
        

        注意!所有不提供随机访问迭代器的容器都不允许使用 STL 提供的算法。

      • 指定容器的插入规则:

        容器默认插入顺序是从小到大,如果想要自己指定插入顺序,则需要在实例化容器时额外**提供一个模板参数**。

        这个类型实参里需要提供一个重载的方法,实现一个仿函数,提供排序规则。

        例如:

        class rule {
                  
                  
              public:
               bool operator()(const type& a, const type* b) const {
                  
                  
                   return a > b;
               }
           };
        

        实例化容器时:

        set<type, rule> s;
        

        所以,对于自定义数据类型,需要提供一个 < 号常函数重载(因为 < 是默认排序)。

         bool operator<(const type& obj) const {
                  
                  
                 return /* ... */ ;
             }
        

        或者提供一个排序规则。

        set<type, rule> s;
        
  • 迭代器

    容器提供双向迭代器。

map/multimap 容器

(关联式容器)

map 与 JSON 的抽象结构相似,是键值对。插入时会根据键值排序。

元素的类型是刚才提到的 pair<T1, T2>。第一个是键,第二个是值。

与 set/multiset 容器相同,multimap 容器允许出现相同的键值,且其底层实现也是红黑树。

  • 头文件

    #include <map>
    
  • 获得一个容器(构造函数)

    map<type1, type2> m;
    // 默认构造
    
  • 数据操作

    • 交换

      void swap(map& m);
      // 互换两容器的内容
      
    • 是否为空

      bool empty();
      
    • 获取大小

      size_t size()
      
    • 数据存取

      • 直接访问:

        type& operator[](size_t n);
        // 访问越界就崩了
        type& at(size_t n);
        // 访问越界会抛出异常
        
      • 数据插入:

        pair<iterator, bool> insert(pair<type1, type2> p);
        

        示例:

        // 1.pair
        m.insert(pair<int, string>(3, "string"));
        
        // 2.make_pair
        m.insert(make_pair(3, "string"));
        
        // 3.(一般不这样用)
        m.insert(map<int, string>::value_type(3, "string"));
        
        // 4.(伪数组) 注意! multimap 不允许这种操作
        m[1] = "string";
        // 这种方法存在一个问题
        // 当 m[1] 不存在时
        m[1]; /* 等价于 */ m[1] = NULL;
        // 对于某些类型来说,就等于 type(NULL);
        
      • 数据删除:

        iterator erase(iterator first, iterator last);
        // 删除迭代器从 first 到 last 之间的元素
        
        iterator erase(iterator where);
        // 删除迭代器指向的元素
        
        iterator erase(type1& key);
        // 删除容器中键为 key 的元素
        
        void clear();
        // 删除容器中所有元素
        
      • 查找操作:

        iterator find(type1& key);
        // 查找 key 是否存在,返回迭代器,未找到范围 set.end()
        
        size_t count(type1& key); 
        // 返回 key 的元素个数
        
        iterator lower_bound(type1& key);
        // 本意是返回第一个小于等于 key 的迭代器,但实际上结果与 find 相同
        
        iterator upper_bound(type1& key);
        // 返回第一个大于 key 的迭代器。
        
        pair<iterator, iterator> equal_range(type1& key);
        // 返回一个 pair,包含 lower_bound 和 upper_bound 的返回值
        
      • 指定容器的插入规则:

        与 set/multiset 容器相同,实例化时额外提供一个伪函数的实现,就可以了。

        map<type1, type2, rule> m;
        
  • 迭代器

    容器提供双向迭代器。

容器总结

迭代器类型

  • 双向迭代器

    map/multimap、set/multiset、list

  • 随机访问迭代器

    string、vector、deque、

  • 不提供迭代器

    stack、queue

底层实现

  • 单端数组

    vector

  • 双端数组

    deque

  • 双向循环链表

    list

  • 二叉树

    set/multiset、map/multimap

性能

vector deque list set/multiset map/multimap
搜寻速度 特慢
迭代器 随机访问 随机访问 双向 双向 双向
优点 查找效率比较高 均衡的动态增删 + 数据访问 动态增删能力强 查找效率极高 查找效率极高
弱点 头部数据操作效率低 中间数据操作效率低 查找效率低 插入效率低 插入效率低

deque 有较好的动态增删能力,但牺牲了中间数据的操作效率。

list 有着效率非常高的动态增删能力,但牺牲了查找效率。

set 和 map 以插入效率为代价来维护底层的红黑树,从而增强了在大容量数据中的查找性能。

另外,在遍历 vector 时,使用 [] 效率很高,而遍历 deque 时则应使用迭代器。

猜你喜欢

转载自blog.csdn.net/qq_16181837/article/details/106731492