C++浓缩学习笔记(4)-STL

 文章主要引用:

阿秀的学习笔记 (interviewguide.cn)

牛客网 - 找工作神器|笔试题库|面试经验|实习招聘内推,求职就业一站解决_牛客网 (nowcoder.com)

一、基本组成

请说说 STL 的基本组成部分

参考回答

​ 标准模板库(Standard Template Library,简称STL)简单说,就是一些常用数据结构和算法的模板的集合。

广义上讲,STL分为3类:Algorithm(算法)、Container(容器)和Iterator(迭代器),容器和算法通过迭代器可以进行无缝地连接。

详细的说,STL由6部分组成:容器(Container)、算法(Algorithm)、 迭代器(Iterator)、仿函数(Function object)、适配器(Adaptor)、空间配制器(Allocator)。

答案解析

​ 标准模板库STL主要由6大组成部分:

  1. 容器(Container)

    ​ 是一种数据结构, 如list, vector, 和deques,以模板类的方法提供。为了访问容器中的数据,可以使用由容器类输出的迭代器。

  2. 算法(Algorithm)

    ​ 是用来操作容器中的数据的模板函数。例如,STL用sort()来对一 个vector中的数据进行排序,用find()来搜索一个list中的对象, 函数本身与他们操作的数据的结构和类型无关,因此他们可以用于从简单数组到高度复杂容器的任何数据结构上。

  3. 迭代器(Iterator)

    ​ 提供了访问容器中对象的方法。例如,可以使用一对迭代器指定list或vector中的一定范围的对象。 迭代器就如同一个指针。事实上,C++ 的指针也是一种迭代器。 但是,迭代器也可以是那些定义了operator*()以及其他类似于指针的操作符方法的类对象;

  4. 仿函数(Function object)

    仿函数(Functor)是C++中的一种重要概念,也被称为函数对象(Function Object)。它是一种类对象,可以像函数一样被调用,通常用于代替函数指针,实现函数的封装和参数化。大多数STL算法可以用一个函数对象作为参数。

  5. 适配器(Adaptor)

    简单的说就是一种接口类,专门用来修改现有类的接口,提供一中新的接口;或调用现有的函数来实现所需要的功能。主要包括3中适配器Container Adaptor、Iterator Adaptor、Function Adaptor。

  6. 空间配制器(Allocator)

    ​ 为STL提供空间配置的系统。其中主要工作包括两部分:

    (1)对象的创建与销毁;

    (2)内存的获取与释放。

请说说 STL 中常见的容器,并介绍一下实现原理

参考回答

​ 容器可以用于存放各种类型的数据(基本类型的变量,对象等)的数据结构,都是模板类,分为顺序容器、关联式容器、容器适配器三种类型,三种类型容器特性分别如下:

  1. 顺序容器

    ​ 容器并非排序的,元素的插入位置同元素的值无关。包含vector、deque、list,具体实现原理如下:

    (1)vector 头文件<vector>

    ​ 源代码不难发现,它就是使用 3 个迭代器(可以理解成指针)来表示的。动态数组。元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。

    (2)deque 头文件<deque>

    ​ 双向队列。元素在内存连续存放。随机存取任何元素都能在常数时间完成(仅次于vector)。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。

    (3)list 头文件<list>

    ​ 双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。

  2. 关联式容器

    ​ 元素是排序的;插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现。包含set、multiset、map、multimap,具体实现原理如下:

    (1)set/multiset 头文件<set>

    ​ set 即集合。set中不允许相同元素,multiset中允许存在相同元素。

    (2)map/multimap 头文件<map>

    ​ map与set的不同在于map中存放的元素有且仅有两个成员变,一个名为first,另一个名为second, map根据first值对元素从小到大排序,并可快速地根据first来检索元素。

    注意:map同multimap的不同在于是否允许相同first值的元素。

  3. 容器适配器

    ​ 封装了一些基本的容器,使之具备了新的函数功能,比如把deque封装一下变为一个具有stack功能的数据结构。这新得到的数据结构就叫适配器。包含stack,queue,priority_queue,具体实现原理如下:

    (1)stack 头文件<stack>

    ​ 栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最进插入序列的项(栈顶的项)。后进先出。

    (2)queue 头文件<queue>

    ​ 队列。插入只可以在尾部进行,删除、检索和修改只允许从头部进行。先进先出。

    (3)priority_queue 头文件<queue>

    ​ 优先级队列。内部维持某种有序,然后确保优先级最高的元素总是位于头部。最高优先级元素总是第一个出列。

STL 容器用过哪些,查找的时间复杂度是多少,为什么?

参考回答

​ STL中常用的容器有vector、deque、list、map、set、multimap、multiset、unordered_map、unordered_set等。容器底层实现方式及时间复杂度分别如下:

  1. vector

    采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为:

    插入: O(N)

    查看: O(1)

    删除: O(N)

  2. deque

    采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为:

    插入: O(N)

    查看: O(1)

    删除: O(N)

  3. list

    采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为:

    插入: O(1)

    查看: O(N)

    删除: O(1)

  4. map、set、multimap、multiset

    上述四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为:

    插入: O(logN)

    查看: O(logN)

    删除: O(logN)

  5. unordered_map、unordered_set、unordered_multimap、 unordered_multiset

    上述四种容器采用哈希表实现,不同操作的时间复杂度为: 插入: O(1),最坏情况O(N)

    查看: O(1),最坏情况O(N)

    删除: O(1),最坏情况O(N)

    注意:容器的时间复杂度取决于其底层实现方式。

说说 STL 容器动态链接可能产生的问题?

参考回答

  1. 可能产生 的问题

    ​ 容器是一种动态分配内存空间的一个变量集合类型变量。在一般的程序函数里,局部容器,参数传递容器,参数传递容器的引用,参数传递容器指针都是可以正常运行的,而在动态链接库函数内部使用容器也是没有问题的,但是给动态库函数传递容器的对象本身,则会出现内存堆栈破坏的问题

  2. 产生问题的原因 容器和动态链接库相互支持不够好动态链接库函数中使用容器时参数中只能传递容器的引用,并且要保证容器的大小不能超出初始大小,否则导致容器自动重新分配,就会出现内存堆栈破坏问题。

动态库函数传递容器的对象本身,则会出现内存堆栈破坏的问题的原因

因为容器对象的生命周期管理不当。具体来说,这种情况通常发生在以下情况下:

  1. 生命周期不匹配:当容器对象在调用动态库函数时已经超出有效生命周期范围,但动态库函数依然试图访问容器对象,就会导致内存堆栈破坏。这通常是因为容器对象是在调用方的栈上创建的,而动态库函数在创建时并不知道调用方栈上的对象会在什么时候被销毁

  2. 不同编译单元:容器对象在动态库函数和调用方之间可能位于不同的编译单元中,导致编译单元之间的内存布局不同。这可能会导致在动态库函数中访问容器对象时,出现内存偏移或其他错误。

  3. 不同的标准库实现:动态库和调用方可能使用不同的标准库实现,例如动态库可能使用动态链接器提供的标准库,而调用方使用静态链接器提供的标准库。这可能导致在两个库中使用不同的内存管理策略,进而引发内存破坏问题。

为避免这些问题,应该遵循以下几点:

  1. 动态库函数不应该直接传递容器对象本身,而是应该传递容器的引用或指针。这样可以避免容器对象的复制,同时由调用方负责容器对象的生命周期管理。

  2. 确保动态库函数在使用容器对象之前进行有效性检查,以避免访问已经超出生命周期的对象。

  3. 如果容器对象位于动态库和调用方之在间,确保编译时和链接时使用相同的标准库实现,以保持内存布局的一致性。

  4. 在使用容器对象时,考虑使用智能指针或其他现代C++特性来辅助管理对象的生命周期,以避免内存管理错误。

容器和动态链接库之间为什么会出现内存破坏

内存破坏(Memory corruption)通常是由于程序中发生了一系列错误,导致对内存的访问超出了合法范围,从而损坏了其他变量、数据结构或代码段的内存内容。容器和动态链接库之间出现内存破坏可能是因为以下原因:

1. 缓冲区溢出:当程序向容器中插入或操作数据时,如果数据的大小超过了容器预分配的内存空间(比如数组越界、堆栈溢出等),就会导致缓冲区溢出,覆盖到其他内存区域的内容,包括动态链接库的数据和代码。

2. 释放已释放的内存:在容器和动态链接库之间的操作中,可能会存在内存释放问题。比如在容器中保存指针,当容器析构时可能没有正确释放这些指针,导致动态链接库中的某些资源被重复释放,进而导致内存破坏。

3. 使用已释放的内存:类似于释放已释放的内存,当容器中保存了指针,并且在某些操作后指针所指向的内存已经被释放,但程序仍然继续使用这些已释放的内存,就会引发内存破坏。

4. 迭代器失效:在容器中进行插入或删除操作可能会导致迭代器失效。如果在失效的迭代器上继续进行访问,就有可能访问到非法的内存,从而导致内存破坏。

5. 多线程竞争:在多线程环境中,多个线程可能同时对容器和动态链接库进行操作,如果没有合适的同步措施,就会产生竞争条件,导致内存破坏。

要避免容器和动态链接库之间的内存破坏,需要编写高质量的代码,保证数据的合法性,正确管理内存和资源,以及合理地使用同步机制(如互斥锁)来避免多线程竞争问题。此外,使用现代的C++标准库中提供的智能指针和容器等工具可以帮助减少内存管理的错误和隐患。

二、顺序容器(vector、deque、list)

C++中vector的底层实现

在C++中,标准库提供的vector是一个动态数组(也称为动态表)的实现。底层实现一般是使用连续的内存块来存储元素,并且能够动态调整内存大小以适应元素的添加和删除。

vector的底层实现使用指针或智能指针来管理内存。当vector的容量不足以存储更多的元素时,它会自动重新分配更大的内存块,并将原来的元素拷贝到新的内存块中。这个过程称为扩容(resizing)。

常用函数:

clear(),清空元素时间复杂度O(N); insert()时间复杂度O(N); erase() 时间复杂度O(N);

注意:当释放或者删除(vec.clear())里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。

以下是一些vector的常见特点和操作,涉及其底层实现:

  1. 连续内存存储:vector中的元素在内存中是连续存储的,这也是vector的一个重要特点,它支持通过指针进行高效的随机访问。

  2. 容量和大小:vector有容量和大小两个属性。容量是指当前vector所分配的内存大小,而大小是指vector中实际存储的元素数量。当大小达到容量时,会触发扩容操作。

  3. 扩容策略:vector在进行扩容时,通常会分配更大的内存块,一般是当前容量的两倍,以减少频繁扩容的开销。

  4. 拷贝和移动语义:vector中的元素类型必须支持拷贝构造函数和拷贝赋值运算符。在C++11及以后的版本中,还支持移动构造函数和移动赋值运算符,以提高元素的添加和删除效率。

  5. 内存管理:vector会自动管理内存的分配和释放,用户无需手动释放内存。

需要注意的是,虽然vector的底层实现提供了高效的随机访问和动态调整内存大小的能力,但在某些特定场景下,可能存在更适合的数据结构,比如std::list用于高效的插入和删除操作。在选择数据结构时,需要根据具体的应用需求和操作频率来进行权衡。

简述 vector 的实现原理

参考回答

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

图 1 演示了以上这 3 个迭代器分别指向的位置。


 


图 1 vector实现原理示意图


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

在此基础上,将 3 个迭代器两两结合,还可以表达不同的含义,例如:

  • _Myfirst 和 _Mylast 可以用来表示 vector 容器中目前已被使用的内存空间;
  • _Mylast 和 _Myend 可以用来表示 vector 容器目前空闲的内存空间;
  • _Myfirst 和 _Myend 可以用表示 vector 容器的容量。

​ vector底层实现原理为一维数组(元素在空间连续存放)。

  1. 新增元素

    ​ Vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素。插入新的数据分在最后插入push_back和通过迭代器在任何位置插入,这里说一下通过迭代器插入,通过迭代器与第一个元素的距离知道要插入的位置,即int index=iter-begin()。这个元素后面的所有元素都向后移动一个位置,在空出来的位置上存入新增的元素。

  2. 删除元素

    ​ 删除和新增差不多,也分两种,删除最后一个元素pop_back和通过迭代器删除任意一个元素erase(iter)。通过迭代器删除还是先找到要删除元素的位置,即int index=iter-begin();这个位置后面的每个元素都想前移动一个元素的位置。同时我们知道erase不释放内存只初始化成默认值。

    ​ 删除全部元素clear:只是循环调用了erase,所以删除全部元素的时候,不释放内存。内存是在析构函数中释放的。

     
  3. 迭代器iteraotr

    迭代器iteraotr是STL的一个重要组成部分,通过iterator可以很方便的存储集合中的元素.STL为每个集合都写了一个迭代器, 迭代器其实是对一个指针的包装,实现一些常用的方法,如++,--,!=,==,*,->等, 通过这些方法可以找到当前元素或是别的元素. vector是STL集合中比较特殊的一个,因为vector中的每个元素都是连续的,所以在自己实现vector的时候可以用指针代替。

     

vector 内存分配(百度)

vector底层实现原理为一维数组(元素在空间连续存放)在中分配内存 。         

扩容:Vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素。

(1)capacity:该值在容器初始化时赋值,指的是容器能够容纳的最大的元素的个数。还不能通过下标等访问,因为此时容器中还没有创建任何对象。

(2)size:指的是此时容器中实际的元素个数。可以通过下标访问0-(size-1)范围内的对象。

capacity()-size()则是剩余的可用空间大小。当size()和capacity()相等,说明vector目前的空间已被用完,如果再添加新元素,则会引起vector空间的动态增长。

由于动态增长会引起重新分配内存空间、拷贝原空间、释放原空间,这些过程会降低程序效率。因此,可以使用reserve(n)预先分配一块较大的指定大小的内存空间,这样当指定大小的内存空间未使用完时,是不会重新分配内存空间的,这样便提升了效率。只有当n>capacity()时,调用reserve(n)才会改变vector容量。

resize既分配了空间,也创建了对象;reserve表示容器预留空间,但并不是真正的创建对象,需要通过insert()或push_back()等创建对象。

(2)resize既修改capacity大小,也修改size大小;reserve只修改capacity大小,不修改size大小。

(3)两者的形参个数不一样。 resize带两个参数,一个表示容器大小,一个表示初始值(默认为0);reserve只带一个参数,表示容器预留的大小。这些内存空间可能还是“野”的,如果此时使用“[ ]”来访问,则可能会越界。而resize(size_type new_size)会真正使容器具有new_size个对象。

问题延伸:

​ resize 和 reserve 既有差别,也有共同点。两个接口的共同点它们都保证了vector的空间大小(capacity)最少达到它的参数所指定的大小。下面就他们的细节进行分析。

​ 为实现resize的语义,resize接口做了两个保证:

​ (1)保证区间[0, new_size)范围内数据有效,如果下标index在此区间内,vector[indext]是合法的;

​ (2)保证区间[0, new_size)范围以外数据无效,如果下标index在区间外,vector[indext]是非法的。

​ reserve只是保证vector的空间大小(capacity)最少达到它的参数所指定的大小n。在区间[0, n)范围内,如果下标是index,vector[index]这种访问有可能是合法的,也有可能是非法的,视具体情况而定。

​ 以下是两个接口的源代码:

void resize(size_type new_size) {   resize(new_size, T()); } void resize(size_type new_size, const T& x) {  if (new_size < size())    erase(begin() + new_size, end()); // erase区间范围以外的数据,确保区间以外的数据无效  else   insert(end(), new_size - size(), x); // 填补区间范围内空缺的数据,确保区间内的数据有效 }   #include<iostream> #include<vector> using namespace std; int main() {     vector<int> a;     cout<<"initial capacity:"<<a.capacity()<<endl;     cout<<"initial size:"<<a.size()<<endl;      /*resize改变capacity和size*/     a.resize(20);     cout<<"resize capacity:"<<a.capacity()<<endl;     cout<<"resize size:"<<a.size()<<endl;       vector<int> b;     /*reserve改变capacity,不改变resize*/     b.reserve(100);     cout<<"reserve capacity:"<<b.capacity()<<endl;     cout<<"reserve size:"<<b.size()<<endl;  return 0; }  /*      运行结果:   initial capacity:0  initial size:0  resize capacity:20  resize size:20  reserve capacity:100  reserve size:0 */ 


注意:如果n大于当前的vector的容量(是容量,并非vector的size),将会引起自动内存分配。所以现有的pointer,references,iterators将会失效。而内存的重新配置会很耗时间。

vector插入操作可能会导致迭代器失效?

对vector的任何操作一旦引起了空间的重新配置,就会进行扩容,指向原vector的所有迭代器会都失效了。

deque的实现原理

在C++标准库中,deque(双端队列)是由多个定长数组(或者说缓冲区)组成的双端队列数据结构。deque的底层实现使用多个连续的存储块(也称为分段)来存储元素,每个存储块是一个定长数组,可以看作是由多个定长数组组成的动态数组。

deque的底层实现的主要特点包括:

  1. 分段连续存储:deque中的元素被存储在一个或多个连续的存储块中。每个存储块都是一个定长数组,称为分段(chunk)。当deque需要扩容时,会动态地分配一个新的分段,形成一个新的存储块,从而允许在两端进行高效的插入和删除操作。

  2. 双头指针:deque一般维护两个指针,分别指向首部和尾部的分段。这样,无论从头部还是尾部进行插入或删除操作,都可以在常数时间内完成。

  3. 动态扩容:当deque的元素数量超过当前分段的容量时,会动态地分配一个新的分段,从而扩展deque的大小。这种动态扩容的策略可以使deque的大小在增长时保持较好的性能。

  4. 随机访问:与vector类似,deque也支持通过索引进行随机访问,并且具有常数时间的时间复杂度。

总的来说,deque在实现上兼顾了高效的头尾插入和删除操作,同时也支持随机访问。这使得deque成为一种非常实用的数据结构,特别适合需要在两端频繁进行插入和删除操作的场景。然而,由于deque的存储结构较为复杂,使得其在空间上可能略微有些开销,因此在具体使用时需要根据应用场景来选择合适的容器类型。

list的底层实现

在C++标准库中,list(链表)是由双向链表实现的数据结构。list的底层实现使用双向链表来存储元素,每个节点(节点通常是一个结构体或类)都包含了元素的以及指向前一个节点和后一个节点的指针。链表在内存中可不是连续分布。

list的底层实现的主要特点包括:

  1. 双向链表:list中的元素被存储在一个个节点中,每个节点都包含了元素的值以及指向前一个节点和后一个节点的指针。这样的双向链表结构允许在任意位置进行高效的插入和删除操作,只需要修改相邻节点的指针即可。

  2. 高效插入和删除:由于list是一个双向链表,插入和删除元素的时间复杂度是常数时间O(1),而不受容器大小的影响。这使得list在插入和删除操作频繁的场景下非常高效。

  3. 无随机访问:由于list的元素不是连续存储的,不能通过索引进行随机访问。如果需要随机访问元素,list的性能相对较差,需要遍历链表找到目标元素。

  4. 动态分配:每个元素都需要一个节点进行存储,因此在内存上可能会有一些额外的开销。每次插入或删除元素都需要动态分配和释放内存,这也是与vector等连续存储容器的一个区别。

总的来说,list在实现上使用双向链表来存储元素,因此适用于需要频繁在任意位置进行插入和删除操作的场景。但是需要注意的是,由于list不支持随机访问,如果需要频繁按索引访问元素,可能会更适合使用其他支持随机访问的容器,比如vectordeque。在选择数据结构时,需要根据具体的应用需求和操作频率来进行权衡。

// 单链表
struct ListNode {
    int val;  // 节点上存储的元素
    ListNode *next;  // 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {}  // 节点的构造函数
};

说说 vector 和 list 的区别,分别适用于什么场景?

参考回答

​ vector和list区别在于底层实现机理不同,因而特性和适用场景也有所不同。

vector:一维数组

​ 特点:元素在内存连续存放,动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后内存也不会释放。

​ 优点:和数组类似开辟一段连续的空间,并且支持随机访问,所以它的查找效率高其时间复杂度O(1)。

​ 缺点:由于开辟一段连续的空间,所以插入删除会需要对数据进行移动比较麻烦,时间复杂度O(n),另外当空间不足时还需要进行扩容。

list:双向链表

​ 特点:元素在堆中存放,每个元素都是存放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问。

​ 优点:底层实现是循环双链表,当对大量数据进行插入删除时,其时间复杂度O(1)。

​ 缺点:底层没有连续的空间,只能通过指针来访问,所以查找数据需要遍历其时间复杂度O(n),没有提供[]操作符的重载。

应用场景

​ vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。

​ list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。

C++ 的 vector 和 list中,如果删除末尾的元素,其指针和迭代器如何变化?若删除的是中间的元素呢?

参考回答

  1. 迭代器和指针之间的区别

    迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,重载了指针的一些操作符,-->、++、--等。迭代器封装了指针,是一个”可遍历STL( Standard Template Library)容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,--等操作。

    迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身。

  2. vector和list特性

    vector特性 动态数组。元素在内存连续存放。随机存取任何元素都在常数时间完成。在尾端增删元素具有较大的性能(大部分情况下是常数时间)。

    list特性 双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。

  3. vector增删元素

    对于vector而言,删除某个元素以后,该元素后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器。

  4. list增删元素

    对于list而言,删除某个元素,只有“指向被删除元素”的那个迭代器失效,其它迭代器不受任何影响。

STL 中 vector 与 list 具体是怎么实现的?常见操作的时间复杂度是多少?

参考回答

  1. vector 一维数组(元素在内存连续存放)

    ​ 是动态数组,在中分配内存,元素连续存放,有保留内存,如果减少大小后,内存也不会释放;如果新增大小当前大小时才会重新分配内存。

    ​ 扩容方式: a. 倍放开辟2倍的内存

    ​ b. 旧的数据开辟到新的内存

    ​ c. 释放旧的内存

    ​ d. 指向新内存

  2. list 双向链表(元素存放在堆中)

    ​ 元素存放在中,每个元素都是放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点,使得它的随机存取变得非常没有效率,因此它没有提供[ ]操作符的重载。但是由于链表的特点,它可以很有效的支持任意地方的删除和插入操作。

    ​ 特点:a. 随机访问不方便

    ​ b. 删除插入操作方便

  3. 常见时间复杂度

    (1)vector插入、查找、删除时间复杂度分别为:O(n)、O(1)、O(n);

    (2)list插入、查找、删除时间复杂度分别为:O(1)、O(n)、O(1)。

说说 push_back 和 emplace_back 的区别

参考回答

​ 如果要将一个临时变量push到容器的末尾,push_back()需要先构造临时对象,再将这个对象拷贝到容器的末尾,而emplace_back()则直接在容器的末尾构造对象,这样就省去了拷贝的过程。

答案解析

​ 参考代码:

#include <iostream> #include <cstring> #include <vector> using namespace std;   class A { public:     A(int i){         str = to_string(i);         cout << "构造函数" << endl;      }     ~A(){}     A(const A& other): str(other.str){         cout << "拷贝构造" << endl;     }   public:     string str; };   int main() {     vector<A> vec;     vec.reserve(10);     for(int i=0;i<10;i++){         vec.push_back(A(i)); //调用了10次构造函数和10次拷贝构造函数,     // vec.emplace_back(i);  //调用了10次构造函数一次拷贝构造函数都没有调用过     } }

三、关联式容器(set、multiset、map、multimap)

set的底层实现

在C++标准库中,`std::set`是由红黑树(Red-Black Tree)实现的有序集合(set)数据结构。红黑树是一种自平衡的二叉搜索树,用于实现有序的关联容器,如`std::set`和`std::map`。

`std::set`的底层实现的主要特点包括:

1. 红黑树:`std::set`中的元素被存储在红黑树中。红黑树是一种自平衡的二叉搜索树,在红黑树中,每个节点被标记为红色或黑色,并且满足以下性质:
   - 每个节点要么是红色,要么是黑色。
   - 根节点是黑色的。
   - 所有叶子节点(NIL节点,也称为空节点)都是黑色的。
   - 如果一个节点是红色的,则它的两个子节点都是黑色的。
   - 从任意节点到其每个叶子节点的简单路径上,包含相同数量的黑色节点。

   这些性质保证了红黑树的平衡性,使得插入、删除和查找操作的时间复杂度都是对数时间O(log n),其中n是集合中的元素数量。

2. 自动排序:由于红黑树是一种二叉搜索树,其中的元素按照键值大小进行排序。因此,`std::set`中的元素是按照键值有序排列的,默认按照键值的升序进行排序。

3. 动态分配:每个元素都需要一个节点进行存储,因此在内存上可能会有一些额外的开销。每次插入或删除元素都需要动态分配和释放节点内存。

总的来说,`std::set`在底层使用红黑树来实现有序的集合数据结构,可以高效地插入、删除和查找元素,并保持元素的有序性。由于红黑树的自平衡特性,`std::set`的操作时间复杂度为O(log n),使得它在需要高效有序集合的场景下非常实用。

map的底层实现

在C++标准库中,`std::map`是由红黑树(Red-Black Tree)实现的有序关联容器。红黑树是一种自平衡的二叉搜索树,用于实现有序的关联容器,如`std::map`和`std::set`。

`std::map`的底层实现的主要特点包括:

1. 红黑树:`std::map`中的元素被存储在红黑树中。红黑树是一种自平衡的二叉搜索树,在红黑树中,每个节点被标记为红色或黑色,并且满足以下性质:
   - 每个节点要么是红色,要么是黑色。
   - 根节点是黑色的。
   - 所有叶子节点(NIL节点,也称为空节点)都是黑色的。
   - 如果一个节点是红色的,则它的两个子节点都是黑色的。
   - 从任意节点到其每个叶子节点的简单路径上,包含相同数量的黑色节点。

   这些性质保证了红黑树的平衡性,使得插入、删除和查找操作的时间复杂度都是对数时间O(log n),其中n是map中的元素数量。

2. 键-值对存储:`std::map`中的元素是由键值对(key-value pair)组成的。每个元素都有一个唯一的键(key)和与之对应的值(value)。

3. 自动排序:由于红黑树是一种二叉搜索树,其中的元素按照键值大小进行排序。因此,`std::map`中的元素是按照键值有序排列的,默认按照键值的升序进行排序。

4. 动态分配:每个键-值对都需要一个节点进行存储,因此在内存上可能会有一些额外的开销。每次插入或删除键-值对都需要动态分配和释放节点内存。

总的来说,`std::map`在底层使用红黑树来实现有序的关联容器,可以高效地插入、删除和查找键-值对,并保持键的有序性。由于红黑树的自平衡特性,`std::map`的操作时间复杂度为O(log n),使得它在需要高效有序关联容器的场景下非常实用。

map中[ ]与find的区别?

  1. map的下标运算符[ ]的作用是:将关键码(key)作为下标去执行查找,并返回对应的值(value);(1) 若key存在,则访问取得value;(2) 若该key不存在,访问仍可成功,取得value对象默认构造的值。用[]访问,但key不存在时,c++会利用该key及默认构造的value,组成{key, value}对,插入map中。若value为string对象,则构造空串;value为int,构造0;

  2. map的find函数:用关键码执行查找,找到了返回该位置的迭代器;如果不存在这个关键码,就返回尾迭代器

说说 map 和 unordered_map 的区别?底层实现

参考回答

​ map和unordered_map的区别在于他们的实现基理不同

  1. map实现机理

    ​ map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树有自动排序的功能,因此map内部所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来。

  2. unordered_map实现机理

    unordered_map内部实现了一个哈希表(也叫散列表),通过把关键码值映射到Hash表中一个位置来访问记录,查找时间复杂度可达O(1),其中在海量数据处理中有着广泛应用。因此,元素的排列顺序是无序的。

请你来说一下 map 和 set 有什么区别,分别又是怎么实现的?

参考回答

  1. set是一种关联式容器,其特性如下:

    (1)set以RBTree作为底层容器

    (2)所得元素的只有key没有value,value就是key

    (3)不允许出现键值重复

    (4)所有的元素都会被自动排序

    (5)不能通过迭代器来改变set的值,因为set的值就是键,set的迭代器是const的

  2. map和set一样是关联式容器,其特性如下:

    (1)map以RBTree作为底层容器

    (2)所有元素都是键+值存在

    (3)不允许键重复

    (4)所有元素是通过键进行自动排序的

    (5)map的键是不能修改的,但是其键对应的值是可以修改的

    综上所述,map和set底层实现都是红黑树;map和set的区别在于map的值不作为键,键和值是分开的。

四、容器适配器。(stack,queue,priority_queue)

stack的底层实现

在C++标准库中,`std::stack`是一个容器适配器(Container Adapter),它是对底层容器的封装,用于实现栈(stack)数据结构。所有元素必须符合先进后出规则,所以栈不能遍历不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。

`std::stack`的底层实现并不是直接通过数据结构来实现的,底层容器是可插拔的 ,是依赖于其他的底层容器来完成实际的存储和操作。在C++中,默认情况下,`std::stack`使用`std::deque`(双端队列)作为其默认底层容器,但也可以通过模板参数来选择其他容器,如`std::vector`或`std::list`。

底层实现的主要特点如下:

1. 容器封装:`std::stack`并不直接实现栈的数据结构,而是通过封装底层容器(如`std::deque`)的操作来实现栈的功能。它将底层容器的操作适配为栈的操作,提供了栈特有的接口,如`push()`、`pop()`和`top()`等。

2. 高效的操作:由于`std::stack`是基于底层容器实现的,因此在底层容器的操作是高效的。`std::deque`作为默认的底层容器,它支持高效的头部和尾部插入/删除操作,这使得栈的压入(push)和弹出(pop)操作都具有常数时间复杂度。

3. 容器适配器:`std::stack`是一个容器适配器,它提供了一种简单的接口来使用不同的底层容器实现栈。这使得在使用栈的场景下,可以根据具体需求选择最适合的底层容器,或者自定义底层容器来实现更特定的功能。

总的来说,`std::stack`是一个基于底层容器的容器适配器,用于实现栈数据结构。它提供了栈的常用接口,并通过封装底层容器的操作来实现栈的功能。通过选择不同的底层容器,可以在不同场景下获得更好的性能和功能。

栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。

栈里面的元素在内存中是连续分布的么?

答:(1)栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中不是连续分布。(2)缺省情况下,默认底层容器是deque,那么deque的在内存中的数据分布不连续的;deque是可以两边扩展的,而且deque里元素并不是严格的连续分布的。

queue的底层实现

在C++标准库中,`std::queue`是一个容器适配器(Container Adapter),用于实现队列(queue)数据结构。`std::queue`的底层实现并不是直接通过数据结构来实现的,而是依赖于其他的底层容器来完成实际的存储和操作。队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器,

默认情况下,`std::queue`使用`std::deque`(双端队列)作为其默认底层容器,但也可以通过模板参数来选择其他容器,如`std::list`或`std::vector`。

`std::queue`的底层实现的主要特点如下:

1. 容器封装:`std::queue`并不直接实现队列的数据结构,而是通过封装底层容器(如`std::deque`)的操作来实现队列的功能。它将底层容器的操作适配为队列的操作,提供了队列特有的接口,如`push()`、`pop()`和`front()`等。

2. 高效的操作:由于`std::queue`是基于底层容器实现的,因此底层容器的操作是高效的。`std::deque`作为默认的底层容器,支持高效的头部和尾部插入/删除操作,这使得队列的入队(enqueue)和出队(dequeue)操作都具有常数时间复杂度。

3. 容器适配器:`std::queue`是一个容器适配器,它提供了一种简单的接口来使用不同的底层容器实现队列。这使得在使用队列的场景下,可以根据具体需求选择最适合的底层容器,或者自定义底层容器来实现更特定的功能。

总的来说,`std::queue`是一个基于底层容器的容器适配器,用于实现队列数据结构。它提供了队列的常用接口,并通过封装底层容器的操作来实现队列的功能。通过选择不同的底层容器,可以在不同场景下获得更好的性能和功能。

priority-queue的底层实现

在C++标准库中,`std::priority_queue`是一个容器适配器(Container Adapter),用于实现优先队列(priority queue)数据结构。`std::priority_queue`的底层实现并不是直接通过数据结构来实现的,而是依赖于其他的底层容器来完成实际的存储和操作。

默认情况下,`std::priority_queue`使用`std::vector`作为其默认底层容器,但也可以通过模板参数来选择其他容器。

`std::priority_queue`的底层实现的主要特点如下:

1. 容器封装:`std::priority_queue`并不直接实现优先队列的数据结构,而是通过封装底层容器(如`std::vector`)的操作来实现优先队列的功能。它将底层容器的操作适配为优先队列的操作,提供了优先队列特有的接口,如`push()`、`pop()`和`top()`等。

2. 堆结构:在实现优先队列时,`std::priority_queue`通常使用堆结构(通常是二叉堆)来维护元素的优先级。堆是一种特殊的二叉树结构,具有以下性质:
   - 对于每个节点x,它的父节点的值总是大于等于(或小于等于,具体取决于是最大堆还是最小堆)其子节点的值。
   - 堆总是一个完全二叉树,除了最底层外,其他层的节点都被填满,且最底层的节点都尽量靠左排列。

   使用堆来实现优先队列可以保证在任意时刻,队首元素(即优先级最高的元素)总是能够在常数时间内访问。

3. 高效的操作:由于`std::priority_queue`是基于底层容器实现的,因此底层容器的操作是高效的。使用堆结构来维护优先队列的性质,使得入队(push)和出队(pop)操作的时间复杂度为O(log n),其中n是优先队列中的元素数量。

4. 容器适配器:`std::priority_queue`是一个容器适配器,它提供了一种简单的接口来使用不同的底层容器实现优先队列。这使得在使用优先队列的场景下,可以根据具体需求选择最适合的底层容器,或者自定义底层容器来实现更特定的功能。

总的来说,`std::priority_queue`是一个基于堆结构的容器适配器,用于实现优先队列数据结构。它提供了优先队列的常用接口,并通过封装底层容器的操作来实现优先队列的功能。通过选择不同的底层容器和比较函数,可以在不同场景下获得更好的性能和功能。

五、其他数据结构

树Tree(遍历方式)

1.满二叉树;

2.完全二叉树 (堆结构):(1).底部从左到右是连续的;(2).上部是满二叉树;

3.二叉搜索树:(1).左子树所有节点小于中间节点;(2).右子树所有节点大于中间节点;

4.平衡二叉搜索树 (红黑树) :左子树与右子树的高度绝对值不超过1;查询和插入O(logN);

map, set, multi-map, multi-set底层实现都是红黑树;key有序;

堆Heap

  1. 堆结构就是用数组为表现形式完全二叉树。 (底部从左到右连续,上部是满二叉树);

  2. 堆是一棵完全二叉树, 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。

  3. 优先级队列结构就是堆结构。priority_queue,其头文件是<queue>。 堆顶优先级最大。

  4. 在一个大顶堆(小顶堆)中,加入一个数或者删除一个数,仍保持大顶堆(小顶堆),时间O(logn). 遍历数组为O(n), 则堆排序时间复杂度O(nlogn).

  5. 堆排序: ——时间复杂度O(nlogn), 空间复杂度O(1), 不稳定;

    1.先让整个数组变为大顶堆。(新加的数与头结点比较,若大于则交换,反之不动),相当于一直插入新的数,并保持大根堆。​2、把堆的最大值和堆末尾值交换,然后减少堆的大小后,再去调整堆,重复上述过程,直至堆的 大小减小为0,排序完成。补充:把最大值和堆末尾值交换后,新堆大小除去最后一个数,即除去最大值,再将其重新变为大根堆,过程:若存在左右孩子,比较根节点和值大的孩子,若小于孩子值则交换。​ 3.堆的大小减为0后,排序完成。

时间复杂度O(nlogn). 堆排序和快排都不稳定,归并排序稳定。

  1. 堆排序扩展题:已知一个几乎有序的数组,几乎是指,如果把数组排好顺序的话,每个元素移动的距离不可以超过k,且k相对于数组来说较小,请选择一个合适的排序算法针对这个数据进行排序。

    假如k = 6,用小根堆-优先级队列 ,将前6个数放进优先级队列中,这时最小值弹出--即数组最小值确定,接下来将第7个数放入并弹出最小值,确定排序第二个小的数,然后重复上述过程,直至遍历完数组。 时间复杂度O(nlogk)

哈希表

​ 哈希表是根据关键码的值而直接进行访问的数据结构,采用的函数映射的思想。哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里。哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。

哈希冲突,不能够保证每个元素的key与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值, 一般可采用拉链法、线性探测法解决冲突;

常见的三种哈希结构

当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。

  • 数组
  • set (集合)
  • map(映射)

这里数组就没啥可说的了,我们来看一下set。

在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:

集合 底层实现 是否有序 数值是否可以重复 能否更改数值 查询效率 增删效率
std::set 红黑树 有序 O(log n) O(log n)
std::multiset 红黑树 有序 O(logn) O(logn)
std::unordered_set 哈希表 无序 O(1) O(1)

std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。

映射 底层实现 是否有序 数值是否可以重复 能否更改数值 查询效率 增删效率
std::map 红黑树 key有序 key不可重复 key不可修改 O(logn) O(logn)
std::multimap 红黑树 key有序 key可重复 key不可修改 O(log n) O(log n)
std::unordered_map 哈希表 key无序 key不可重复 key不可修改 O(1) O(1)

std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。

当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。

那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。

虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,std::set、std::multiset 使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。

补充:

1.map中key是否可以是自定义类型,如果可以需要满足什么条件?

​ 可以。 但是应该1. 重载 < 运算符。 或者 2.为map提供比较器。因为map中key是有序的。

 ​2.C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树(红黑树),所以map、set的增删操作时间时间复杂度是logn。unordered_map、unordered_set底层实现是哈希表,时间复杂度O(1)。

六、空间配置器

请你来介绍一下 STL 的空间配置器(allocator)

参考回答

​ 一般情况下,一个程序包括数据结构和相应的算法,而数据结构作为存储数据的组织形式,与内存空间有着密切的联系。在C++ STL中,空间配置器便是用来实现内存空间(一般是内存,也可以是硬盘等空间)分配的工具,他与容器联系紧密,每一种容器的空间分配都是通过空间分配器alloctor实现的。

答案解析

  1. 两种C++类对象实例化方式的异同

    ​ 在c++中,创建类对象一般分为两种方式:一种是直接利用构造函数,直接构造类对象,如 Test test();另一种是通过new来实例化一个类对象,如 Test *pTest = new Test;那么,这两种方式有什么异同点呢?

    我们知道,内存分配主要有三种方式:

    (1) 静态存储区分配:内存在程序编译的时候已经分配好,这块内存在程序的整个运行空间内都存在。如全局变量,静态变量等。

    (2) 空间分配:程序在运行期间,函数内的局部变量通过栈空间来分配存储(函数调用栈),当函数执行完毕返回时,相对应的栈空间被立即回收。主要是局部变量。

    (3)空间分配:程序在运行期间,通过在堆空间上为数据分配存储空间,通过malloc和new创建的对象都是从堆空间分配内存,这类空间需要程序员自己来管理,必须通过free()或者是delete()函数对堆空间进行释放,否则会造成内存溢出。

    那么,从内存空间分配的角度来对这两种方式的区别,就比较容易区分:

    (1)对于第一种方式来说,是直接通过调用Test类的构造函数来实例化Test类对象的,如果该实例化对象是一个局部变量,则其是在栈空间分配相应的存储空间。 (2)对于第二种方式来说,就显得比较复杂。这里主要以new类对象来说明一下。new一个类对象,其实是执行了两步操作:首先,调用new在堆空间分配内存,然后调用类的构造函数构造对象的内容;同样,使用delete释放时,也是经历了两个步骤:首先调用类的析构函数释放类对象,然后调用delete释放堆空间。

  2. C++ STL空间配置器实现

    很容易想象,为了实现空间配置器,完全可以利用newdelete函数并对其进行封装实现STL的空间配置器,的确可以这样。但是,为了最大化提升效率,SGI STL版本并没有简单的这样做,而是采取了一定的措施,实现了更加高效复杂的空间分配策略。由于以上的构造都分为两部分,所以,在SGI STL中,将对象的构造切分开来,分成空间配置和对象构造两部分。

    ​ 内存配置操作: 通过alloc::allocate()实现 内存释放操作: 通过alloc::deallocate()实现 对象构造操作: 通过::construct()实现 对象释放操作: 通过::destroy()实现

    ​ 关于内存空间的配置与释放,SGI STL采用了两级配置器:一级配置器主要是考虑大块内存空间,利用malloc和free实现;二级配置器主要是考虑小块内存空间而设计的(为了最大化解决内存碎片问题,进而提升效率),采用链表free_list维护内存池(memory pool),free_list通过union结构实现,空闲的内存块互相挂接在一块,内存块一旦被使用,则被从链表中剔除,易于维护。

七、迭代器

迭代器用过吗?什么时候会失效?

参考回答

​ 用过,常用容器迭代器失效情形如下。

  1. 对于序列容器vector,deque来说,使用erase后,后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器。

  2. 对于关联容器map,set来说,使用了erase后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。

  3. 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的迭代器,因此上面两种方法都可以使用。

说一下STL中迭代器的作用,有指针为何还要迭代器?

参考回答

  1. 迭代器的作用

    (1)用于指向顺序容器和关联容器中的元素

    (2)通过迭代器可以读取它指向的元素

    (3)通过非const迭代器还可以修改其指向的元素

  2. 迭代器和指针的区别

    迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能重载了指针的一些操作符,-->、++、--等。迭代器封装了指针,是一个”可遍历STL( Standard Template Library)容器内全部或部分元素”的对象,本质封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,--等操作。

    迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身。

  3. 迭代器产生的原因

    ​ Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。

答案解析

  1. 迭代器

    ​ Iterator(迭代器)模式又称游标(Cursor)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。 或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。 由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、vector、stack等容器类及ostream_iterator等扩展Iterator。

​ 2. 迭代器示例:

#include <vector> #include <iostream> using namespace std;  int main() {  vector<int> v; //一个存放int元素的数组,一开始里面没有元素  v.push_back(1);  v.push_back(2);  v.push_back(3);  v.push_back(4);  vector<int>::const_iterator i; //常量迭代器  for (i = v.begin(); i != v.end(); ++i) //v.begin()表示v第一个元素迭代器指针,++i指向下一个元素   cout << *i << ","; //*i表示迭代器指向的元素  cout << endl;   vector<int>::reverse_iterator r; //反向迭代器  for (r = v.rbegin(); r != v.rend(); r++)   cout << *r << ",";  cout << endl;  vector<int>::iterator j; //非常量迭代器  for (j = v.begin();j != v.end();j++)   *j = 100;  for (i = v.begin();i != v.end();i++)   cout << *i << ",";  return 0; }  /*      运行结果:   1,2,3,4,   4,3,2,1,   100,100,100,100, */    


 说说 STL 迭代器是怎么删除元素的

参考回答

​ 这是主要考察迭代器失效的问题。

  1. 对于序列容器vector,deque来说,使用erase后,后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器;

  2. 对于关联容器map,set来说,使用了erase后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可;

  3. 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的迭代器,因此上面两种方法都可以使用。

答案解析

​ 容器上迭代器分类如下表(详细实现过程请翻阅相关资料详细了解):

容器 容器上的迭代器类别
vector 随机访问
deque 随机访问
list 双向
set/multiset 双向
map/multimap 双向
stack 不支持迭代器
queue 不支持迭代器
priority_queue 不支持迭代器

八、仿函数

函数对象\仿函数

仿函数(Functor)是C++中的一种重要概念,也被称为函数对象(Function Object)。它是一种类对象,可以像函数一样被调用,通常用于代替函数指针,实现函数的封装和参数化。大多数STL算法可以用一个函数对象作为参数。

所谓函数对象其实就是一个行为类似函数的对象,它可以不需要参数,也可以带有若干参数。

有0个,1个,2个传入参数的函数对象,分别成为产生器,一元函数,二元函数。

标准函数对象头文件<functional> 标准函数对象是内联函数

1.函数对象(仿函数)是一个类,不是一个函数。

2.函数对象(仿函数)重载了“()”操作符使得它可以像函数一样被调用。

仿函数的特点:

  • 类对象:仿函数是一个类,可以包含成员变量和成员函数。
  • 重载操作符():仿函数类必须重载操作符(),使其可以像函数一样被调用。
  • 参数化:可以通过构造函数或成员变量在创建仿函数对象时传递参数,实现函数行为的参数化。

#include <iostream> 
// 仿函数类 
class Adder { 
public: 
    Adder(int value) : _value(value) {} 
    int operator()(int x) { return x + _value; } 
private: int _value; }; 
int main() { 
    Adder add5(5); // 创建一个加5的仿函数对象 
    int result = add5(10); // 调用仿函数对象 
    std::cout << result << std::endl; // 输出 15 
    return 0; 
}

仿函数在STL(标准模板库)中被广泛使用,例如在算法中作为排序、查找等的比较函数。它提供了更灵活的函数对象操作,比传统函数指针更具有扩展性和可定制性。

猜你喜欢

转载自blog.csdn.net/shisniend/article/details/131908949