STL入门

STL容器介绍

  • 概括:STL 提供有 3 类标准容器,分别是序列容器排序容器哈希容器,其中后两类容器有时也统称为关联容器

  • STL 容器种类和功能

  1. 序列容器:vector 向量容器list 列表容器deque 双端队列容器。——> 容器不是排序的。元素在容器中的位置同元素的值无关。将元素插入容器时,指定在什么位置,元素就会位于什么位置。
  2. 排序容器:set 集合容器、multiset多重集合容器、map映射容器以及multimap多重映射容器。——> 元素默认是由小到大排序好的。插入元素会插入到适当的位置。
  3. 哈希容器:unordered_set哈希集合、unordered_multiset哈希多重集合、unordered_map哈希映射以及 unordered_multimap哈希多重映射。——> 元素是未排序的,元素的位置由哈希函数确定。
  • 迭代器
    • 常用的迭代器按功能强弱分为输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器 5 种。

      1. 前向迭代器(forward iterator):++p,p++,*p,被复制 or 赋值,只能使用 == 和 != 运算符比较,可以互相赋值
      2. 双向迭代器(bidirectional iterator):除“前向功能”之外,还可以进行 --p or p-- 操作(即一次向后移动一个位置)
      3. 随机访问迭代器(random access iterator):
        • p+=i:使得 p 往后移动 i 个元素。
        • p-=i:使得 p 往前移动 i 个元素。
        • p+i:返回 p 后面第 i 个元素的迭代器。
        • p-i:返回 p 前面第 i 个元素的迭代器。
        • p[i]:返回 p 后面第 i 个元素的引用。
    • 不同容器的迭代器

      容器对应的迭代器类型
      arrayvectordeque随机访问迭代器
      listset/multisetmap/multimap双向迭代器
      forward_listunordered_map/unordered_multimapunordered_set/unordered_multiset前向迭代器
      stackqueuepriority_queue不支持迭代器

      注意:容器适配器 stackqueue 没有迭代器,它们包含有一些成员函数,可以用来对元素进行访问。

    • 迭代器的 4 种定义方式

      迭代器定义方式具体格式
      正向迭代器容器类名::iterator 迭代器名;
      常量正向迭代器容器类名::const_iterator 迭代器名;
      反向迭代器容器类名::reverse_iterator 迭代器名;
      常量反向迭代器容器类名::const_reverse_iterator 迭代器名;

      常量迭代器非常量迭代器的区别:除了用*迭代器名访问迭代器指向的元素,非常量迭代器还能修改其指向的元素!

      反向迭代器正向迭代器的区别:

      • 正向迭代器进行 ++ 操作时,迭代器会指向容器中的后一个元素;
      • 反向迭代器进行 ++ 操作时,迭代器会指向容器中的前一个元素。
    • eg:支持随机访问迭代器的vector,支持双向迭代器的list

      // 以下操作均允许:
      for (int i = 0; i < v.size(); ++i)  	cout << v[i] <<" ";		// 像普通数组一样访问vector容器中的元素
      vector<int>::iterator i;	// 创建一个正向迭代器(其他 3 种定义迭代器的方式也支持)  for (i = v.begin(); i != v.end(); ++i)	// 用 != 比较两个迭代器
          cout << *i << " ";
      for (i = v.begin(); i < v.end(); ++i)	// 用 < 比较两个迭代器!【注意!】
          cout << *i << " ";
      i = v.begin();
      while (i < v.end()) { 	//间隔一个输出
          cout << *i << " ";
          i += 2;		// 随机访问迭代器支持 "+= 整数"  的操作
      }
      /*---------        list(双向迭代器)       ---------*/
      // 以下操作允许:
      list<int>::const_iterator i;	//创建一个常量正向迭代器(其他 3 种定义迭代器的方式也支持)
      for(i = v.begin(); i != v.end(); ++i)
          cout << *i;
      // 以下操作不允许!!!
      for(i = v.begin(); i < v.end(); ++i)
          cout << *i;		// 双向迭代器不支持用 “<” 进行比较!!!【注意!】
      for(int i=0; i<v.size(); ++i)
          cout << v[i];	// 双向迭代器不支持用下标随机访问元素:
      

在这里插入图片描述

  • 补充:在 C++普通数组 也是容器。数组的迭代器 == 指针,而且 = 随机访问迭代器。例如,对于数组 int a[10],int * 类型的指针就是其迭代器( a、a+1、a+2 都是数组 a 的迭代器)。
  • 注意:迭代器的功能是遍历容器,在遍历的同时可以访问(甚至修改)容器中的元素,但迭代器 不能用来初始化 空的容器。

array

  • 创建并初始化:

    array<double, 10> values;	// 包含 10 个浮点型元素,但各个元素的值是不确定的(array 容器不会做默认初始化操作)
    array<double, 10> values {};	// 将所有的元素初始化为 0 或者和默认元素类型等效的值
    array<double, 10> values {0.5,1.0,1.5,,2.0};	// 像创建常规数组那样进行初始化(只初始化了前4个,剩余元素都会被初始化为 0.0)
    
  • 成员函数:begin()/end()[or cbegin()/cend()]、rbegin()/rend()[or crbegin()/crend()]
    在这里插入图片描述

      for (auto first = values.begin(); first != values.end(); ++first) {
          cout << *first << " ";   
      }
      for (auto first = values.rbegin(); first != values.rend(); ++first) {
          cout << *first << " ";
      }
    

底层实现

  • 底层:一段连续的线性内存空间(即:普通数组)

  • array普通数组的联系和区别:
    1. array 容器是在 C++ 普通数组的基础上,添加了一些成员函数和全局函数。在使用上,它比普通数组更安全,且效率并没有因此变差。
    2. 和 C++ 普通数组存储数据方式一样,array 容器存储的所有元素一定会位于 连续且相邻 的内存中。
    3. 代替普通数组,最直接的好处就是array模板类中已经为我们写好了很多实用的方法,可以大大提高我们编码效率。例如,array 容器提供的 at() 成员函数,可以有效防止越界操纵数组的情况;fill() 函数可以实现数组的快速初始化;swap() 函数可以轻松实现两个相同数组(类型相同,大小相同)中元素的互换。

    array<int, 5>a{1,2,3};
    cout << &a[2] << " " << &a[0] + 2 << endl;	//输出结果为:  004FFD58 004FFD58
    strcpy(&a[0], "csdn");	
    strcpy(a.data(), "csdn");	// strcpy() 在拷贝字符串的同时,会自动在最后添加 '\0'
    
  • 类型大小都相同的array容器:
    1. 可以直接直接做赋值
    2. 可以用任何比较运算符直接比较

  • 总之,读者可以这样认为,array 容器就是普通数组的“升级版”,使用普通数组能实现的,使用 array 容器都可以实现,而且无论是代码功能的实现效率,还是程序执行效率,都比普通数组更高。

常用成员函数

  • 访问元素
  /*-------	 访问单个	-------*/
  array<int, 5> values{1,2,3,4,5};
  values[4] = values[3] + 2.O*values[1];	// 通过 "容器名[]" 的方式   
  values.at (4) = values.at(3) + 2.O*values.at(1);	// 避免越界访问
  get<3>(values);	// Output values[3],即:通过get<n> 模板函数获取到容器的第 n 个元素   
  *( values.data() + 1);	// Output values[1],通过data()得到指向容器首个元素的指针
  /*-------	 访问多个	-------*/   
  // 利用for循环遍历访问

vector

  • 创建并初始化

    vector<double> values;	// 空的 vector 容器,容器中没有元素,没有为其分配空间。
    vector<int> values {2, 3, 5, 7, 11, 13, 17, 19};	// 创建的同时指定初始值以及元素个数
    vector<double> values(20);	// 开始时就有 20 个元素,它们的默认初始值都为 0。
    vector<double> values(20, 1.0);	// 为这20个元素可以指定一个其它值
    // 利用圆括号()中的第2个参数
    int num = 20;
    double value = 1.0;
    vector<double> values(num, value);
    vector<double>value2(values);
    // 用一对指针或者迭代器来指定初始值的范围
    int array[] = {1,2,3,4,5,6};
    vector<int>values(array, array + 4);	// values 将保存{1,2,3,4}
    vector<int>value2(begin(values), begin(values)+3);	// value2保存{1,2,3}
    
  • 注意:vector 容器在申请更多内存的同时,容器中的所有元素可能会被复制或移动到新的内存地址,这会导致之前创建的迭代器失效

    vector<int>values{1,2,3};
    cout << "values 容器首个元素的地址:" << values.data() << endl;	// values 容器首个元素的地址:0096DFE8
    auto first = values.begin();
    auto end = values.end();
    values.reserve(20);	// 增加 values 的容量!!!
    cout << "values 容器首个元素的地址:" << values.data() << endl;	// values 容器首个元素的地址:00965560
    /* first = values.begin();	// 重新初始化一遍
    end = values.end(); */
    while (first != end) {	// 若不重新初始化,则运行直接崩溃!
        cout << *first;	
        ++first;
    }
    

底层实现

  • 底层:一段连续的线性内存空间(即:普通数组) 使用 3 个迭代器(可以理解成指针)来表示的:

    //_Alloc 表示内存分配器,此参数几乎不需要我们关心
    template <class _Ty, class _Alloc = allocator<_Ty>>
    class vector{
        ...
    protected:
        pointer _Myfirst;
        pointer _Mylast;
        pointer _Myend;
    };
    

    其中,_Myfirst 指向的是vector容器对象的起始字节位置;_Mylast 指向当前最后一个元素的末尾字节;_Myend 指向整个vector容器所占用内存空间的末尾字节。

    在这里插入图片描述

    如上图:通过这3个迭代器,就可以表示出一个已容纳 2 个元素,容量为 5 的 vector 容器。

    • 将 3 个迭代器两两结合,还可以表达不同的含义,例如:

      • _Myfirst_Mylast 可以用来表示 vector 容器中 目前已被使用的内存空间

      • _Mylast_Myend 可以用来表示 vector 容器 目前空闲的内存空间

      • _Myfirst_Myend 可以用表示 vector 容器的 容量

    • 对于空的 vector 容器,由于没有任何元素的空间分配,因此 _Myfirst_Mylast_Myend 均为 null

  • 扩容 的过程需要经历以下 3 步:——> 解释了vector容器扩容后,与其相关的指针、引用以及迭代器可能会失效的原因。
    1. 完全弃用现有的内存空间,重新申请更大的内存空间;
    2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
    3. 最后将旧的内存空间释放。

常用成员函数

  • 访问元素(补充部分

      /*-------	 访问单个	-------*/
      vector<int> values{1,2,3,4,5};
      values.front() = 10;	//修改首元素
      values.back() = 20;		//修改尾元素
      cout << *(values.data() + 2) << endl;	//输出容器中第 3 个元素的值
      *(values.data() + 1) = 10;	//修改容器中第 2 个元素的值
      cout << *(values.data() + 1) << endl;
    
  • 尾部添加一个元素(emplace_back()push_back()

    vector<int> values{};
    values.push_back(1);	// == values.emplace_back(1);
    

emplace_back()push_back() 的区别:就在于底层实现的机制不同。——> 后同!

  • push_back() 在实现时,首先创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);——> 先调用构造函数,再调用移动构造函数(若移动构造,但拷贝构造,则调用拷贝构造)

  • emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。——> 只调用一个构造函数——> emplace_back() 的执行效率比 push_back() 高!

  • 指定位置插入一个或多个元素(insert()emplace()

    区别:emplace()在插入元素时,是在容器的指定位置直接构造元素,而不是insert()那样先单独生成,再将其复制(或移动)到容器中。——> 在实际使用中,推荐优先使用 emplace()注意:emplace() 每次只能插入一个元素,而不能是多个!!!

  • 删除元素

    函数功能
    pop_back()【常和swap()搭配】先调用 swap() 函数交换要删除的**目标元素和容器最后一个元素的位置,然后使用 pop_back() 删除该目标元素**。
    erase(pos)删除指定位置**pos**处的元素,并返回指向被删除元素下一个位置元素的迭代器。该容器的大小(size)减 1,但容量(capacity)不会发生改变。
    erase(begin,end)删除位于迭代器 **[beg,end)**指定区域内的所有元素,并返回指向被删除区域下一个位置元素的迭代器。该容器的大小(size)减小,但容量(capacity)不会发生改变。
    remove() 【常和erase(begin,end)搭配】删除容器中所有和指定元素值相等的元素,并返回指向最后一个元素下一个位置的迭代器。值得一提的是,调用该函数不会改变容器的大小和容量。
    clear()删除容器中所有的元素,使其变成空。该函数会改变大小(size变为 0),但不改变其容量(capacity)。
    vector<int>demo{ 1,2,3,4,5 };
    //交换要删除元素和最后一个元素的位置
    swap(*(std::begin(demo)+1), *(std::end(demo)-1));	//等同于 swap(demo[1],demo[4])
    demo.pop_back();	// 删除目标元素
    //交换要删除元素和最后一个元素的位置
    auto iter = std::remove(demo.begin(), demo.end(), 3);
    demo.erase(iter, demo.end());	// 删掉这些 "无用" 的元素,减小其容器大小
    

    注意:erase方法不仅使所指向被删除的迭代器失效,而且使被删元素 之后的所有迭代器失效list除外),所以不能使用erase(iter++)的方式,但是erase方法的返回值是 下一个有效迭代器,故可以如下使用:iter = demo.erase(iter);


2.16

  • 如何避免vector容器进行不必要的扩容?——> 利用 reserve() 函数
    • 参考网址:https://blog.csdn.net/qq_41895747/article/details/103977885

2.17

  • vector swap()成员方法还可以这样用!——> 能够去除上一节的 多余容量 问题

    • 参考网址:https://blog.csdn.net/weixin_42510222/article/details/117272496

    • 可以套用如下的语法格式: vector(x).swap(x); 其中x指当前要操作的容器。该代码执行流程可细分为以下 3 步:

      1. 先执行vector(x),此表达式会调用vector模板类中的拷贝构造函数,从而创建出一个临时的vector容器(后续称其为 tempvector)。值得一提的是,tempvector 临时容器并不为空,因为我们将x作为参数传递给了拷贝构造函数,该函数会将x容器中的所有元素拷贝一份,并存储到 tempvector 临时容器中。

        尤其注意:vector 模板类中的拷贝构造函数 只会为拷贝的元素分配存储空间。即:tempvector临时容器中 没有空闲的存储空间,其容量等于存储元素的个数。

      2. 然后借助 swap() 成员方法对 tempvector 临时容器和x容器进行调换,此过程不仅会交换 2 个容器存储的元素,还会交换它们的容量。即:经过swap()操作,x容器具有了 tempvector 临时容器存储的所有元素和容量,同时 tempvector 也具有了原x容器存储的所有元素和容量。

      3. 当整条语句执行结束时,临时的 tempvector 容器会被销毁,其占据的存储空间都会被释放。注意,这里释放的其实是原x容器占用的存储空间。

    • 同理:如何清空vector的容量?答:vector().swap(x);——> 中间会生成一个空的vector的临时对象。


2.18(关键)

  • 切忌,vector<bool>不是存储bool类型元素的vector容器!——> 尽量避免使用 vector<bool>,改用 deque<bool> or bitset or vector<int> 来代替。

  • 原因:
    1. 严格意义上讲,vector<bool> 并不是一个 STL 容器;
    2. vector<bool> 底层存储的并不是 bool 类型值。

  • 替代方案:

    • 那么,如果在实际场景中需要使用 vector<bool> 这样的存储结构,该怎么办呢?很简单,可以选择使用 deque<bool> 或者 bitset 来替代 vector<bool>
  • 分析:

    对于是否为 STL 容器,C++ 标准库中有明确的判断条件,其中一个条件是:如果cont是包含对象TSTL容器,且该容器中重载了 [ ] 运算符(即支持 operator[]),则以下代码必须能够被编译:

    T *p = &cont[0];
    

    所以,若 vector<bool> 是一个 STL 容器,则下面这段代码是可以通过编译的:

    // 创建一个 vector<bool> 容器
    vector<bool>cont{0,1};
    // 试图将指针 p 指向 cont 容器中第一个元素
    bool *p = &cont[0];	// 很遗憾!该代码无法通过编译!
    

    解答:vector<bool> 底层采用了独特的存储机制:为了节省空间,vector<bool> 底层在存储各个 bool 类型值时,每个 bool 值都只使用一个比特位(二进制位)来存储。即:在 vector<bool> 底层 一个字节可以存储 8 个 bool 类型值。在这种存储机制的影响下,operator[ ] 势必就需要返回一个指向单个比特位的引用,但显然这样的引用是不存在的,因为bool类型一般占用一个字节长度,所以返回的应该是指向单个字节的引用,等号左右两边出现冲突—> 同样对于 指针 来说,其指向的最小单位是字节无法另其指向单个比特位。—> vector<bool>不是一个 STL 容器!


deque

  • 创建并初始化

    // 拷贝普通数组,创建deque容器
    int a[] = { 1,2,3,4,5 };
    std::deque<int> d(a, a + 5);
    // 适用于所有类型的容器
    std::array<int, 5> arr{ 11,12,13,14,15 };
    std::deque<int> d(arr.begin()+2, arr.end());	//拷贝arr容器中的{13,14,15}
    
  • 当向 deque 容器添加元素时deque 容器会直接申请更多的内存空间,同时其包含的所有元素可能会被复制或移动到新的内存地址(原来占用的内存会释放),这会导致 之前创建的迭代器失效

    deque<int>d;
    d.push_back(1);
    auto first = d.begin();
    cout << *first << endl;
    d.push_back(1);		//添加元素,会导致 first 失效
    cout << *first << endl;	//  报错!!!需要在之前加上:first = d.begin();等重新生成迭代器的操作!
    

底层实现

  • 底层:一个中央控制器(map数组)和多个缓冲区(不同区域的连续空间

  • vector 容器采用连续的线性空间不同deque 容器存储数据的空间是由一段一段等长的连续空间构成,各段空间之间并不一定是连续的,可以位于在内存的不同区域。为了管理这些位于 不同区域的连续空间,deque 容器用数组(数组名假设map)存储着 各个不同区域的连续空间的首地址。也就是说,map 数组中存储的都是 指针,指向那些真正用来存储数据的 各个连续空间。通过建立 map 数组,deque 容器申请的这些分段的连续空间就能实现“整体连续”的效果。换句话说,当 deque 容器需要在 头部或尾部 增加存储空间时,它会申请一段新的连续空间,同时在 map 数组的 开头或结尾 添加指向该空间的指针,由此该空间就串接到了 deque 容器的 头部或尾部

    问:如果 map 数组满了怎么办?答:再申请一块更大的连续空间供 map 数组使用,将原有数据(很多指针)拷贝到新的 map 数组中,然后释放旧的空间。

  • deque虽然也提供随机访问的迭代器,但是其迭代器并不是普通的指针,其复杂程度比vector高很多,因此除非必要,否则一般使用vector而非deque。deque迭代器的“++”、“--”操作是远比vector迭代器繁琐,其主要工作在于缓冲区边界,如何从当前缓冲区跳到另一个缓冲区

  • 除了维护 map 数组,还需要维护 start、finish 这 2 个 deque 迭代器。以下为 deque 容器的定义:

    // _Alloc为内存分配器
    template<class _Ty, class _Alloc = allocator<_Ty>>
    class deque{
        ...
    protected:
        iterator start;
        iterator finish;
        map_pointer map;
    ...
    }
    // deque的迭代器数据结构如下:
    struct __deque_iterator
    {
        ...
        T* cur;		// 迭代器所指缓冲区当前的元素
        T* first;	// 迭代器所指缓冲区第一个元素
        T* last;	// 迭代器所指缓冲区最后一个元素
        map_pointer node;	// 指向map中的node
        ...
    }
    

    其中,start 迭代器记录着 map 数组中首个连续空间的信息,finish 迭代器记录着 map 数组中最后一个连续空间的信息。需要注意: 和普通 deque 迭代器不同,start 迭代器中的 cur 指针指向的是连续空间中首个元素;而 finish 迭代器中的 cur 指针指向的是连续空间最后一个元素的下一个位置。
    在这里插入图片描述

  • dequevector的最大差异

    1. deque允许于常数时间内 对头端 进行元素的插入或移除操作(因为它不像vector一样把所有对象保存在一个连续的内存块,而是多个连续的内存块。并且在一个映射结构中保存对这些块以及顺序的跟踪)
      在这里插入图片描述

    2. deque没有capacity(空间、容量)观念,因为它是动态地以 分段连续空间 组合而成,可以随时增加一段新的空间链接起来(无需重新分配空间,而是随时进行原地扩充,没有固定的capacity)。换句话说,像vector那样“因旧空间不足而【重新配置】一块更大空间,然后复制元素,再释放旧空间”这样的事情在deque中是不会发生的。也因此,deque没有必要提供所谓的空间预留(reserved)功能。

    3. deque相较于vector复杂度很高,所以例如对deque进行排序操作,为了最高效率,可将deque先完整复制到一个vector身上,将vector排序后(利用STLsort算法),再复制回deque。——> 常用技巧!!!

  • 注意:deque容器中存储元素并不能保证所有元素都存储到连续的内存空间中。

常用成员函数

  • 访问元素(补充部分

    deque<int> d{ 1,2,3,4,5 };
    d.front() = 10;	//修改首元素
    d.back() = 20;	//修改尾元素
    

    注意:和 vector 容器不同,deque 容器没有提供 data() 成员函数,同时 deque 容器在存储元素时,也无法保证其会将元素存储在连续的内存空间中,因此尝试使用指针去访问 deque 容器中指定位置处的元素,是非常危险的。

  • 添加和删除元素

    // ------- insert 用法
    deque<int> d{ 1,2 };
    //第一种格式用法
    d.insert(d.begin() + 1, 3);	//{1,3,2}
    //第二种格式用法
    d.insert(d.end(), 2, 5);	//{1,3,2,5,5}
    //第三种格式用法
    array<int, 3> test{ 7,8,9 };
    d.insert(d.end(), test.begin(), test.end());	//{1,3,2,5,5,7,8,9}
    //第四种格式用法
    d.insert(d.end(), { 10,11 });	//{1,3,2,5,5,7,8,9,10,11}
    

list

  • 又称:双向链表容器。第一个元素的前向指针总为 null,因为它前面没有元素;同样,尾部元素的后向指针也总为 null

  • 缺点:不能像 arrayvector 那样,通过位置直接访问元素。要访问 list 容器中的第 6 个元素,它不支持容器对象名[6]这种语法格式,正确的做法:从容器中第一个元素或最后一个元素开始遍历容器,直到找到该位置。

  • 应用场景:对序列进行大量添加或删除元素的操作(时间复杂度为O(1)),而直接访问元素的需求却很少的情况。
    在这里插入图片描述

  • 创建并初始化

    //拷贝普通数组,创建list容器
    int a[] = { 1,2,3,4,5 };
    std::list<int> values(a, a+5);
    //拷贝其它类型的容器,创建 list 容器
    std::array<int, 5>arr{ 11,12,13,14,15 };
    std::list<int>values(arr.begin()+2, arr.end());//拷贝arr容器中的{13,14,15}
    
  • 注意:
    1. 遍历容器时比较迭代器之间的关系,用的是 != 运算符,因为它 不支持 < 等运算符。(list使用双向迭代器,而不是前面容器的随机访问迭代器)
    2. list 容器在进行插入(insert())、接合(splice())等操作时,都不会造成原有的 list 迭代器失效,甚至进行删除操作时,也只有指向被删除元素的迭代器失效,其他迭代器 不受 任何影响。

底层实现

  • list容器的底层实现:双向循环链表 ——> list 容器实际上就是一个带有头节点的双向循环链表
    在这里插入图片描述

    其中:a)为双向链表和 b)双向循环链表。

    注意:使用链表存储数据,并不会将它们存储到一整块连续的内存空间中。恰恰相反,各元素占用的存储空间(又称为节点)是独立的、分散的,它们之间的线性关系通过指针来维持。

  • listvector、和deque的区别:
    1. list不再能够像vector一样以普通指针作为迭代器,因为其节点不保证在存储空间中连续存在;
    2. list不像vector那样有可能在空间不足时做重新配置、数据移动的操作,所以插入操作都不会造成原有的list迭代器失效;
    3. list是一个环状双向链表,所以它 只需要一个指针

常用成员函数

  • 添加(插入)元素方法

    // ----- insert() 用法
    std::list<int> values{ 1,2 };
    //第一种格式用法
    values.insert(values.begin() , 3);	//{3,1,2}
    //第二种格式用法
    values.insert(values.end(), 2, 5);	//{3,1,2,5,5}
    //第三种格式用法
    std::array<int, 3>test{ 7,8,9 };
    values.insert(values.end(), test.begin(), test.end());	//{3,1,2,5,5,7,8,9}
    //第四种格式用法
    values.insert(values.end(), { 10,11 });	//{3,1,2,5,5,7,8,9,10,11}
    
    // ----- splice() 用法:将其他 list 容器存储的多个元素添加到当前 list 容器的指定位置处。
    //创建并初始化 2 个 list 容器
    list<int> mylist1{ 1,2,3,4 }, mylist2{10,20,30};
    list<int>::iterator it = ++ mylist1.begin(); //指向 mylist1 容器中的元素 2
    
    //调用第一种语法格式
    mylist1.splice(it, mylist2); 	// mylist1: 1 10 20 30 2 3 4
    							  // mylist2:
    							  // it 迭代器仍然指向元素 2
    
    //调用第二种语法格式,将 it 指向的元素 2 移动到 mylist2.begin() 位置处
    mylist2.splice(mylist2.begin(), mylist1, it);   // mylist1: 1 10 20 30 3 4
                                                    // mylist2: 2
                                                    // it 迭代器仍然指向元素 2
    
    //调用第三种语法格式,将 [mylist1.begin(),mylist1.end())范围内的元素移动到 mylist2.begin() 位置处                  
    mylist2.splice(mylist2.begin(), mylist1, mylist1.begin(), mylist1.end());	//mylist1:
    																	  //mylist2: 1 10 20 30 3 4 2
    

    补充:splice() 成员方法移动元素的方式:将存储该元素的节点从 list 容器底层的链表中 摘除,然后再 链接 到当前 list 容器底层的链表中。故:当使用 splice() 成员方法将 x 容器中的元素添加到当前容器的同时,该元素会从 x 容器中 删除

  • 对于vectordequelist都提供了empty()成员方法和size()成员方法,都可用来判断容器是否为空。

     // 如下代码等价:
     if(cont.size() == 0)
     if(cont.empty())
    

    问题:实际场景推荐使用哪一种呢?

    ——> 答:建议使用 empty() 成员方法。因为无论是哪种容器,使用此方法都可以保证在 O(1) 时间复杂度内完成对“容器是否为空”的判断;但对于 list 容器来说,使用size()成员方法判断“容器是否为空”,可能要消耗 O(n) 的时间复杂度。—> 该结论还适用之后更多容器!!!

    ——> 原因:和 list 模板类提供了 独有的 splice() 成员方法有关。链表的优势在于插入很快(无需经过拷贝就可以直接链接到其它链表中,且整个过程只需要消耗 O(1) 的时间复杂度),所以设计将 splice() 方法的时间复杂度设计为 O(1)。但此时size()的执行效率就不可能达到 O(1),因为需要添加更新 size 变量(直接返回给size()方法)的代码,但更新方式无疑是通过 遍历链表 来实现的,而遍历的复杂度一般为 O(n)

  • 删除元素

    // unique() : 删除容器中相邻的重复元素,只保留一份。
    static bool cmp_1(double a, double b) {
    	return (int)a == (int)b;
    }
    class cmp_2 {
    public:
    	bool operator()(double a, double b) {		// 函数对象参数列表不能用auto
    		return (int)a == (int)b;
    	}
    };
    list<double> mylist{ 1,1.2,1.2,3,4,4.5,4.6 };
    //删除相邻重复的元素,仅保留一份
    mylist.unique();	// {1, 1.2, 3, 4, 4.5, 4.6}
    // 重定义规则:lamba函数、函数对象、函数指针
    mylist.unique([](const auto& a, const auto& b) {	// {1, 3, 4}
        return (int)a == (int)b;
    });
    mylist.unique(cmp_1);	// {1, 3, 4}
    mylist.unique(cmp_2());	// {1, 3, 4}
    
    // remove_if() : 删除容器中满足条件(重定义规则:lamba函数、函数对象、函数指针)的元素。
    std::list<int> mylist{ 15, 36, 7, 17, 20, 39, 4, 1 };
    // 删除 mylist 容器中能够使 lamba 表达式成立的所有元素。
    mylist.remove_if([](int value) {return (value < 10); }); // {15 36 17 20 39}
    // 其他两种方式此处不再列出,同上
    

补充:forward_list(单链表)

-----------------------

有序 关联式容器的底层:「红黑树」

pair

  • 创建并初始化

    // 1) 默认构造函数,即创建空的 pair 对象
    pair();
    // 2) 直接使用 2 个元素初始化成 pair 对象
    pair (const first_type& a, const second_type& b);
    // 3) 拷贝(复制)构造函数,即借助另一个 pair 对象,创建新的 pair 对象
    template<class U, class V> pair (const pair<U,V>& pr);
    // 4) 移动构造函数
    template<class U, class V> pair (pair<U,V>&& pr);
    // 5) 使用右值引用参数,创建 pair 对象
    template<class U, class V> pair (U&& a, V&& b);
    
    // 调用构造函数 1),也就是默认构造函数
    pair <string, double> pair1;
    // 调用第 2) 种构造函数
    pair <string, string> pair2("STL教程","http://stl/");  
    // 调用第 3) 种,拷贝(复制)构造函数
    pair <string, string> pair3(pair2);
    // 调用第 4) 种,移动构造函数
    pair <string, string> pair4(make_pair("C++教程", "http://cplus/"));
    // 调用第 5) 种构造函数(参数是:匿名 string 对象【临时对象】)
    pair <string, string> pair5(string("Python教程"), string("http://python/"));  
    
    // 补充:手动为 pair1 对象【赋值】
    pair1.first = "Java教程";
    pair1.second = "http:///java/";
    

    创建 pair4 对象时,调用了 make_pair() 函数,返回值(是一个 临时对象)作为参数传递给 pair() 构造函数时,其调用的是 移动构造函数,而不是 拷贝构造函数。——> 得到如下等价形式:

    pair <string, string> pair4 = make_pair("C++教程", "http://cplus/");
    pair <string, string> pair4(make_pair("C++教程", "http://cplus/"));
    
  • pair运算规则:对于进行比较的 2 个 pair 对象,先比较 pair.first 元素的大小,如果相等则继续比较 pair.second 元素的大小。

    注意:对于进行比较的 2 个 pair 对象,其对应的键和值的 类型必须相同,否则将没有可比性!

常用成员函数

  • 交换成员函数swap()

    pair <string, int> pair1("pair", 10);                   
    pair <string, int> pair2("pair2", 20);
    // 交换 pair1 和 pair2 的键值对。前提:这 2 个 pair 对象的键和值的类型要相同!!!
    pair1.swap(pair2);
    

map

  • 默认情况下,map容器选用less<T>排序规则(其中T表示键的数据类型),其会根据键的大小对所有键值对做升序排序。

  • 注意:使用 map 容器存储的各个键值对,键的值既不能重复也不能被修改(键的类型会用 const 修饰)。

  • 创建并初始化

    map<string, int>myMap{ make_pair("C语言教程",10),make_pair("STL教程",20) };
    // 通过调用 map 容器的拷贝(复制)构造函数,即可成功创建一个和 myMap 完全一样的 newMap 容器。
    map<string, int>newMap(myMap);
    
    // 当有临时的 map 对象作为参数,传递给要初始化的 map 容器时,此时就会调用 移动构造函数。
    map<string, int> disMap() {
    	map<string, int> temp{ {"C语言教程",10},{"STL教程",20} };
    	return temp;
    }
    map<string, int>newMap_2(disMap());
    
    // 修改默认排序规则创建
    map<string, int, less<string> >myMap_2{ {"C语言教程",10},{"STL教程",20} };
    map<string, int, greater<string> >myMap_3{ {"C语言教程",10},{"STL教程",20} };
    
  • map容器配备的是 双向迭代器(bidirectional iterator)。所以map容器迭代器只能进行 ++p、p++、--p、p--、*p 操作,并且迭代器之间 只能使用 == 或者 != 运算符 进行比较。

  • 获取键对应值

    1. 利用[ ]运算符直接访问数组中元素

      重要注意:

      只有当 map 容器中确实存有包含该指定键的键值对,借助重载的 [ ] 运算符才能成功获取该键对应的值;

      反之,若当前 map 容器中没有包含该指定键的键值对,则此时使用 [ ] 运算符将不再是访问容器中的元素,而变成了向该 map 容器中 增添一个键值对。其中,该键值对的 [ ] 运算符中指定的键,其 对应值 取决于 map 容器规定键值对中值的数据类型:若是基本数据类型,则值为 0;若是 string 类型,其值为 ""空字符串 】(即使用该类型的 默认值 作为键值对的值)。

    2. 使用 at() 成员方法【推荐!既简单又安全!】

      注意点:与[ ]的不同之处在于,如果在当前容器中 查找失败,该方法不会向容器中添加新的键值对,而是直接抛出 out_of_range 异常。

常用成员函数

  • 【迭代器有关】 成员函数

    成员函数功能
    find(key)在 map 容器中查找键为 key 的键值对,如果成功找到,则返回指向该键值对的双向迭代器;反之,则返回和 end() 方法一样的迭代器。另外,如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
    lower_bound(key)返回一个指向当前 map 容器中第一个大于或等于 key 的键值对的双向迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
    upper_bound(key)返回一个指向当前 map 容器中第一个大于 key 的键值对的迭代器。如果 map 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
    equal_range(key)该方法返回一个 pair 对象(包含 2 个双向迭代器),其中 pair.first 和 lower_bound() 方法的返回值等价,pair.second 和 upper_bound() 方法的返回值等价。也就是说,该方法将返回一个范围,该范围中包含的键为 key 的键值对(map 容器键值对唯一,因此该范围最多包含一个键值对)。
    count(key)在当前 map 容器中,查找键为 key 的键值对的个数并返回。注意,由于 map 容器中各键值对的键的值是唯一的,因此该函数的返回值最大为 1。
  • lower_bound(key)upper_bound(key) 成员方法:——> 更多用于multimap容器,map容器很少用到。

    • lower_bound(key) 返回的是指向第一个键不小于(>=)key的键值对的迭代器;
    • upper_bound(key) 返回的是指向第一个键大于(>)key的键值对的迭代器;
  • equal_range(key) 成员方法:——> 更多用于multimap容器,因为map容器键值对唯一,因此该范围最多包含一个键值对(即:=key的那个键值对)

    该方法可以看做是lower_bound(key)upper_bound(key)的结合体,该方法会返回一个pair对象,其中的 2 个元素都是迭代器类型,其中 pair.first 实际上就是 lower_bound(key) 的返回值,而 pair.second 则等同于 upper_bound(key) 的返回值。

    map<string, string>myMap_1{ {"STL教程","http://c.biancheng.net/stl/"},
                               	{"C语言教程","http://c.biancheng.net/c/"},
                               	{"Java教程","http://c.biancheng.net/java/"} };
    //创建一个 pair 对象,来接收 equal_range() 的返回值
    pair <map<string, string>::iterator, map<string, string>::iterator> myPair = myMap_1.equal_range("C语言教程");
    //通过遍历,输出 myPair 指定范围内的键值对
    for (auto iter = myPair.first; iter != myPair.second; ++iter) {
        cout << iter->first << " " << iter->second << endl;
    }	// 输出:C语言教程 http://c.biancheng.net/c/
    
  • insert() 成员方法:

    • 无需指定插入位置,直接插入
    // 1、引用传递一个键值对
    pair<iterator,bool> insert (const value_type& val);
    // 2、以右值引用的方式传递键值对
    template <class P>
    	pair<iterator,bool> insert (P&& val);
    

    其中:val参数表示键值对变量,同时该方法会返回一个pair对象,其中pair.first表示一个迭代器pair.second为一个 bool类型变量

    • 如果成功插入val,则该迭代器指向新插入的valbool值为 true
    • 如果插入val失败,则表明当前 map 容器中存有和val的键相同的键值对(该键值对用p表示),此时返回的迭代器指向pbool值为false
    //创建一个空 map 容器
    map<string, string> mymap;
    //创建一个真实存在的键值对变量
    pair<string, string> STL = { "STL教程","http://c.biancheng.net/stl/" };
    //创建一个接收 insert() 方法返回值的 pair 对象
    pair<map<string, string>::iterator, bool> ret;
    
    //插入 STL,由于 STL 并不是临时变量!因此会以第一种方式传参
    ret = mymap.insert(STL);
    
    //插入失败样例(已存在STL的键值对)
    ret = mymap.insert(STL);
    
    //以右值引用的方式传递临时的键值对变量
    ret = mymap.insert({ "C语言教程","http://c.biancheng.net/c/" }); //该插入方式的等价形式如下:
    //等价形式-1、调用 pair 类模板的构造函数
    ret = mymap.insert(pair<string,string>{ "C语言教程","http://c.biancheng.net/c/" });
    //等价形式-2、调用 make_pair() 函数(推荐!安全又效率高!)
    ret = mymap.insert(make_pair("C语言教程", "http://c.biancheng.net/c/"));
    //等价形式-3、利用 value_type(迭代器所指对象的型别)
    ret = mymap.insert(map<string, string>::value_type("C语言教程", "http://c.biancheng.net/c/"));
    // 打印结果:
    cout << ret.first->first << '\t' << ret.first->second << endl;
    //等价形式-4、利用下标插入(不安全)
    mymap["C语言教程"] = "http://c.biancheng.net/c/"
    

    重要结论:

    1. 插入 成功,则返回的 pair 对象中包含一个指向 新插入 的键值对的迭代器和值为 1bool 变量
    2. 插入 失败,也会返回一个 pair 对象,其中包含一个指向 map 容器中键为 已存在 的键值对和值为 0bool 变量。
    3. 无论是局部定义的键值对变量还是全局定义的键值对变量,都采用 普通引用传递 的方式;而对于 临时 的键值对变量,则以 右值引用 的方式传参。
  • 指定位置插入新键值对

    // 1、以普通引用的方式传递 val 参数
    iterator insert (const_iterator position, const value_type& val);
    // 2、以右值引用的方式传递 val 键值对参数
    template <class P>
        iterator insert (const_iterator position, P&& val);
    

    其中:val 为要插入的键值对变量。注意和第 1 种方式的语法格式不同,这里返回的是 迭代器,而不再是 pair 对象

    • 插入 成功insert() 方法会返回一个指向 map 容器中已插入键值对的迭代器(新键值对键会插入到该迭代器指向的键值对的【前面】);
    • 插入 失败insert() 方法同样会返回一个迭代器,该迭代器指向 map 容器中和 val 具有相同键的那个键值对。
      //指定要插入的位置
      map<string, string>::iterator it = mymap.begin();
      //向 it 位置以普通引用的方式插入 STL
      auto iter1 = mymap.insert(it, STL);
      //插入失败样例
      auto iter3 = mymap.insert(it, STL);
      //向 it 位置以右值引用的方式插入临时键值对
      auto iter2 = mymap.insert(it, pair<string, string>("C语言教程", "http://c.biancheng.net/c/"));
      
  • 插入指定区域内的所有键值对

    template <class InputIterator>
      void insert (InputIterator first, InputIterator last);
    

    其中:firstlast 都是迭代器,它们的组合[first,last)可以表示某 map 容器中的指定区域,该区域包括 first 迭代器指向的元素,但不包含 last 迭代器指向的元素。

    //创建一个空 map 容器
    map<string, string>copymap;
    //指定插入区域
    map<string, string>::iterator first = ++mymap.begin();
    map<string, string>::iterator last = mymap.end();
    //将<first,last>区域内的键值对插入到 copymap 中
    copymap.insert(first, last);
    
  • 允许一次插入多个键值对

    void insert ({val1, val2, ...});
    

    其中:vali 都表示的是键值对变量(pair对象)。

    //创建空的 map 容器
    std::map<std::string, std::string>mymap;
    //向 mymap 容器中添加 3 个键值对
    mymap.insert({ {"STL教程", "http://c.biancheng.net/stl/"},
                  { "C语言教程","http://c.biancheng.net/c/" },
                  { "Java教程","http://c.biancheng.net/java/" } });
    
  • emplace()emplace_hint() 方法

    • emplace() 方法的语法

      template <class... Args>
        pair<iterator,bool> emplace (Args&&... args);
      

      注释详见:与insert()方法 形式-1 相同

    • emplace_hint() 方法的语法

      template <class... Args>
        iterator emplace_hint (const_iterator position, Args&&... args);
      

      注释详见:与insert()方法 形式-2 相同


3.7

  • map容器operator[]insert()效率对比
  • 结论:如果要 更新已存在map元素,operator[]更好,但如果要 增加一个新元素insert()方法则更有优势。

3.9

  • C++ map容器3种插入键值对的方法,谁的效率更高?
  • 详见:https://blog.csdn.net/qq_44004011/article/details/115359611
  • 结论:C++11新增的emplace()emplace_hint()都比insert效率高
    1. 使用 insert() 向 map 容器中插入键值对的过程是,先创建该键值对,然后再将该键值对复制或者移动到 map 容器中的指定位置;
    2. 使用 emplace()emplace_hint() 插入键值对的过程是,直接在 map 容器中的指定位置构造该键值对。

multimap

  • 注意:和 map 容器相比,multimap 没有提供 at() 成员方法,也没有重载 [] 运算符。

set

  • 从语法上讲 set 容器并没有强制对存储元素的类型做 const 修饰,即 set 容器中存储的元素的值是可以修改的。但是,C++ 标准为了防止用户修改容器中元素的值,对所有可能会实现此操作的行为做了限制,使得在正常情况下,用户是无法做到修改 set 容器中元素的值的。对于初学者来说,切勿尝试直接修改 set 容器中已存储元素的值,这很有可能破坏 set 容器中元素的有序性,最正确的修改 set 容器中元素值的做法是:先删除该元素,然后再添加一个修改后的元素。

  • 创建并初始化

    set<string> myset{ "http:/java/","http:/stl/","http:/python/" };
    set<string> copyset(myset);	// 等同于:set<string> copyset = myset
    
    set<string> retSet() {
        set<string> myset{ "http:/java/","http:/stl/","http:/python/" };
        return myset;
    }
    // 注意,由于 retSet() 函数的返回值是一个临时 set 容器,因此在初始化 copySet 容器时,
    // 其内部调用的是 set 类模板中的移动构造函数,而非拷贝构造函数。
    set<string> copySet(myset);	// 等同于:set<string> copySet = retSet();
    
    // 修改默认排序规则创建
    set<string> copyset(++myset.begin(), myset.end());
    
  • set容器配备的是 双向迭代器(bidirectional iterator)。所以set容器迭代器只能进行 ++p、p++、--p、p--、*p 操作,并且迭代器之间 只能使用 == 或者 != 运算符 进行比较。

常用成员函数

  • 【迭代器有关】 成员函数

    成员函数功能
    find(val)在 set 容器中查找值为 val 的元素,如果成功找到,则返回指向该元素的双向迭代器;反之,则返回和 end() 方法一样的迭代器。另外,如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
    lower_bound(val)返回一个指向当前 set 容器中第一个大于或等于 val 的元素的双向迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
    upper_bound(val)返回一个指向当前 set 容器中第一个大于 val 的元素的迭代器。如果 set 容器用 const 限定,则该方法返回的是 const 类型的双向迭代器。
    equal_range(val)该方法返回一个 pair 对象(包含 2 个双向迭代器),其中 pair.first 和 lower_bound() 方法的返回值等价,pair.second 和 upper_bound() 方法的返回值等价。也就是说,该方法将返回一个范围,该范围中包含的值为 val 的元素(set 容器中各个元素是唯一的,因此该范围最多包含一个元素)。
    count(val)在当前 set 容器中,查找值为 val 的元素的个数,并返回。注意,由于 set 容器中各元素的值是唯一的,因此该函数的返回值最大为 1。
    • map分析一样:equal_range(val) 函数的返回值是一个 pair 类型数据,其包含 2 个迭代器,表示 set 容器中 和指定参数 val 相等的元素所在的区域,但由于 set 容器中存储的元素各不相等,因此该函数返回的这 2 个迭代器所表示的范围中,最多只会包含 1 个元素(即:=val的那个元素)。——> 更适用于 multiset 容器!
  • insert() 成员方法:

    • 无需指定插入位置,直接插入

      //创建并初始化set容器
      set<string> myset;
      // 准备接受 insert() 的返回值
      pair<set<string>::iterator, bool> retpair;
      // 采用【普通引用】传值方式
      string str = "http://c.biancheng.net/stl/";
      retpair = myset.insert(str);
      // 采用【右值引用】传值方式
      retpair = myset.insert("http://c.biancheng.net/python/");
      
      • 当向set容器添加元素成功时,该迭代器指向set容器新添加的元素,bool类型的值为 true
      • 如果添加失败,即证明原 set 容器中已存有相同的元素,此时返回的迭代器就指向容器中相同的此元素,同时 bool 类型的值为 false
    • 指定位置插入新键值对

      //创建并初始化set容器
      set<string> myset;
      //准备接受 insert() 的返回值
      set<string>::iterator iter;
      //采用【普通引用】传值方式
      string str = "http://c.biancheng.net/stl/";
      iter = myset.insert(myset.begin(),str);
      //采用【右值引用】传值方式
      iter = myset.insert(myset.end(),"http://c.biancheng.net/python/");
      
      • 当向set容器添加元素成功时,该迭代器指向容器中新添加的元素;
      • 当添加失败时,证明原set容器中已有相同的元素,该迭代器就指向set容器中相同的这个元素。
    • 插入指定区域内的所有键值对

      //创建一个同类型的空 set 容器
      set<std::string> otherset;
      //利用 myset 初始化 otherset
      otherset.insert(++myset.begin(), myset.end());
      
      • 同样的:组合[first = ++myset.begin(),last = myset.end())可以表示另一set容器中的一块区域,该区域包括first迭代器指向的元素,但不包含last迭代器指向的元素。
    • 允许一次插入多个键值对

  • emplace()emplace_hint() 方法 ——> 和map分析一样,不但能实现向set容器添加新元素的功能,其实现效率也比insert()成员方法更高。

  • 删除数据:

    • erase() 方法:

      // 删除 set 容器中值为 val 的元素
      size_type erase (const value_type& val);
      // 删除 position 迭代器指向的元素
      iterator  erase (const_iterator position);
      // 删除 [first,last) 区间内的所有元素
      iterator  erase (const_iterator first, const_iterator last);
      
      • 第 1 种格式,其返回值为一个整数,表示成功删除的元素个数;
      • 后 2 种格式,返回值都是一个迭代器,其指向的是set容器中删除元素之后的第一个元素。(若要删除的元素就是set容器的最后一个元素,则该方法返回的迭代器就指向新set容器中 最后一个元素之后的位置等价于end()方法返回的迭代器
      // 创建并初始化 set 容器
      set<int>myset{1,2,3,4,5};
      // 1) 调用第一种格式的 erase() 方法
      int num = myset.erase(2); // 删除元素 2,myset={1,3,4,5}
      // 2) 调用第二种格式的 erase() 方法
      set<int>::iterator iter = myset.erase(myset.begin()); // 删除元素 1,myset={3,4,5}
      // 3) 调用第三种格式的 erase() 方法
      set<int>::iterator iter2 = myset.erase(myset.begin(), --myset.end());// 删除元素 3,4,myset={5}
      
    • clear()方法:语法格式为:void clear();——> 删除set容器中存储的所有元素


3.17

  • STL关联式容器自定义排序规则(2种方法)——> 更具体的可以详见:priority_queue自定义排序方法

    1. 利用 函数对象 实现自定义排序规则

      //定义函数对象类
      class cmp {
      public:
          //重载 () 运算符
          bool operator ()(const string &a,const string &b) {
              //按照字符串的长度,做升序排序(即存储的字符串从短到长)
              return  (a.length() < b.length());
          }
      };
      
    2. 利用 重载关系运算符 实现自定义排序

    3. 补充方法一:利用 函数指针 实现自定义排序

    4. 补充方法二:利用 lambda表达式 实现自定义排序

3.18

  • 如何修改关联式容器中键值对的键?
  • 总结:mapmultimap容器中元素的键是无法直接修改的,但借助const_cast,我们可以直接修改setmultiset容器中元素的非键部分。

multiset

-----------------------

  • 无序 关联式容器的底层:「哈希表」
  • 关联式容器的底层实现采用的树存储结构,更确切的说是红黑树结构;
  • 无序容器的底层实现采用的是哈希表的存储结构。
  • 和关联式容器相比,无序容器具有以下 2 个特点:
    1. 无序容器内部存储的键值对是无序的,各键值对的存储位置取决于该键值对中的键,
    2. 和关联式容器相比,无序容器擅长通过指定键查找对应的值(平均时间复杂度为 O(1));但对于使用迭代器遍历容器中存储的元素,无序容器的执行效率则不如关联式容器。
  • 应用场景:若涉及【大量使用迭代器遍历容器】的操作,建议首选关联式容器;反之,若更多的操作是【通过键获取对应的值】,则应首选无序容器。

hashtable

  • C++ STL 标准库中,不仅是 unordered_map 容器,所有无序容器的底层实现都采用的是哈希表存储结构。更准确地说,是用 “链地址法”(又称“开链法”) 解决数据存储位置发生冲突的哈希表(“哈希冲突”),如下图:
    在这里插入图片描述

    其结构就是一个 buckets(vector)+ 多个 list,每个vector元素代表一个keylistkey相同的节点集合。(之所以选择vector为存放桶buckets元素的基础容器,主要是因为vector容器本身具有动态扩容能力,无需人工干预。)

  • 哈希表存储结构还有一个重要的属性,称为 负载因子(load factor)。该属性同样适用于无序容器,用于衡量容器存储键值对的空/满程序,即负载因子越大,意味着容器越满,即各链表中挂载着越多的键值对,这无疑会降低容器查找目标键值对的效率;反之,负载因子越小,容器肯定越空,但并不一定各个链表中挂载的键值对就越少。无序容器中,负载因子的计算方法为:

    负载因子 = 容器存储的总键值对 / 桶数   
    

    默认情况下,无序容器的最大负载因子为 1.0。如果操作无序容器过程中,使得最大复杂因子超过了默认值,则容器会自动增加桶数,并重新进行哈希,以此来减小负载因子的值。需要注意:此过程会导致容器迭代器失效,但指向单个键值对的引用或者指针仍然有效。——> 这也就解释了:为什么在操作无序容器过程中,键值对的存储顺序有时会“莫名”的发生变动。

  • 如何 向前操作 ?首先尝试从目前所指的节点出发,前进一个位置(节点),由于节点被安置于list内,所以利用节点的next指针即可轻易完成前进操作,如果目前正巧是list的尾端,就跳至下一个bucket身上,那正是指向下一个list的头部节点。

    注意:hashtable的迭代器没有后退操作,所以 没有重载 -- 操作符,也没有定义逆向迭代器 reverse iterator

  • 基于hashtableunordered_setunordered_map:查找速度更快,因为元素个数大于bucket的数量就扩容,所以一个元素查找次数一般是 1~2次,比红黑树快,但红黑树有优秀的排序的功能。所以在使用setmap时,若追求高效的 执行速度,就基于hashtable,若需要 自动排序,就基于红黑树。


补充:红黑树

网址:http://data.biancheng.net/view/85.html

  • 本质是一棵 二叉查找树(BST),此外加上两个特点:

    1. 树中的每个结点增加了一个用于存储颜色的标志域;
    2. 通过对任意一条从根到叶子的路径上各个节点着色方式的限制,确保没有一条路径会比其他路径长两倍,整棵树要接近于“平衡”的状态。
  • 性质:——> 决定是否为红黑树!

    1. 树中的每个结点颜色不是红的,就是黑的;

    2. 根结点的颜色是黑的;

    3. 所有为 nil 的叶子结点的颜色是黑的;(注意:叶子结点说的只是为空(nil 或 NULL)的叶子结点!)

    4. 如果此结点是红的,那么它的两个孩子结点全部都是黑的;

    5. 对于每个结点,从该结点到到该结点的所有子孙结点的所有路径上包含有相同数目的黑结点;

      img

  • 红黑树与AVL比较:

    1. AVL是 严格平衡 的,频繁的插入和删除,会引起频繁的rebalance,导致效率降低;红黑树是 弱平衡 的,算是一种折中,插入最多旋转2次,删除最多旋转3次。——> 证明:https://blog.csdn.net/m0_37707561/article/details/122967286

    2. 对于一棵具有 n 个结点的红黑树,树的高度至多为:2lg(n + 1)。——> 由此可推出红黑树进行查找操作时的时间复杂度为O(lgn),因为对于高度为 h 的二叉查找树的运行时间为O(h),而包含有 n 个结点的红黑树本身就是最高为 lgn(简化之后)的查找树(h = lgn),所以红黑树的时间复杂度为O(lgn)

      证明:https://blog.csdn.net/ThePythonFucker/article/details/123346415?spm=1001.2014.3001.5502

    3. 红黑树在查找、插入删除的复杂度都是O(logn),且性能稳定,所以STL里面很多结构包括map底层都是使用的红黑树。

  • 红黑树与BST比较:

    1. 节点成本。
    2. 平衡二叉树的时间复杂度是O(logn),红黑树的时间复杂度为O(lgn),两者都表示的都是时间复杂度为对数关系(lg函数为底是10的对数,用于表示时间复杂度时可以忽略)。
    3. 二叉查找树的时间复杂度会受到其树深度的影响,而红黑树可以保证在最坏情况下的时间复杂度仍为O(lgn)。当数据量多到一定程度时,使用红黑树比二叉查找树的效率要高。
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

相约~那雨季

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值