STL

STL六大组件

  • 容器:各种数据结构,从实现的角度来看,STL容器是一种class template

在这里插入图片描述

  • 空间配置器:负责动态空间的配置和管理

  • 迭代器:在23个设计模式中,有一种是迭代器模式(提供一种方法,使之能够依序访问某个容器所含的各个元素,而无需暴露该容器的内部表述方式),其行为类似于智能指针;STL的设计中,将数据容器和算法分开,彼此独立设计,通过迭代器(容器和算法的胶粘剂)将他们撮合在一起。

  • 算法:从实现角度来看,STL算法是一种function template

  • 仿函数:也叫函数对象,行为类似于函数;从实现角度来看,仿函数是重载了operator()的class或者class template 。
    仿函数分类:以操作数的个数划分:一元仿函数、二元仿函数; 以功能划分:算术运算、关系运算、逻辑运算。

  • 配接器:用来修饰容器或者迭代器接口

六大组件的关系:

常用容器的特点及适用情况

  • string:与vector相似的容器,专门用于存储字符。随机访问快,在尾位置插入/删除速度快
  • array:固定大小数组。支持快速随机访问,不能添加或者删除元素
  • vector:可变大小的数组。底层数据结构为数组,支持快速随机访问,在尾部之外的位置插入或者删除元素可能很慢
  • list:双向链表。底层数据结构为双向链表,支持双向顺序访问。在list任何位置插入/删除速度很快
  • forward_list:单向链表。支持单项顺序访问。在forward_list任何位置插入/删除速度很快
  • deque:双端队列。底层数据结构为一个中央控制器和多个缓冲区,支持快速随机访问,在头尾位置插入/删除速度很快
  • stack:栈。底层用deque实现,封闭头部,在尾部进行插入和删除元素
  • queue:队列。底层用deque实现
  • priority_queue:优先队列。底层用vector实现,堆heap为处理规则来管理底层容器的实现
  • set:集合。底层为红黑树,元素有序,不重复
  • multiset:底层为红黑树,元素有序,可重复
  • map:底层为红黑树,键有序,不重复
  • multimap:底层为红黑树,键有序,可重复
  • hash_set:底层为哈希表,无序,不重复
  • hash_multiset:底层为哈希表,无序,可重复
  • hash_map:底层为哈希表,无序,不重复
  • hash_multiap:底层为哈希表,无序,可重复

总结:(这里仅代表做这几种操作时效率比较高,可能其他容器也支持这几种操作)

  • 支持随机访问的容器:string,array,vector,deque
  • 支持在任意位置插入/删除的容器:list,forward_list
  • 支持在尾部插入元素:vector,string,deque

总体来说:unordered_map(就是上面所说的hash_map)比map的查找速度快,unordered_map的查找速度是常数级别,map的查找速度是(logn)级别。但是,不一定常数就比logn小,hash还有hash函数耗时。当考虑效率,特别是当元素达到一定数量级时,考虑unordered_map;但如果对内存要求特别严格,希望少消耗内存,当unordered_map对象比较多时,就不太好控制了,而且它的构造速度会比较慢

底层原理问题

vector底层存储机制

vector是一个动态数组,里面是一个指针指向一片连续的空间,当空间不够用时,会自动申请一块更大的空间(一般是增加当前容量的50%或者100%),然后把原来的数据拷贝过去,接着释放原来的空间;当释放或者删除里面的数据时,其存储空间不释放,仅仅是清空了里面的数据

list底层存储机制

list以节点为单位存放数据,节点的地址在内存中不一定连续,每次插入或者删除数据时,就配置或者释放一个元素的空间

deque底层存储机制

deque动态的以分段连续的空间组成,随时可以增加一段新的连续的空间并链接起来,不提供空间保留(reserve)功能。
deque采用一块map(不是STL的map容器)作为主控,其为一小块连续的空间,其中的每个元素都是指针,指向另一段较大的连续空间(缓冲区)

map底层机制

map以红黑树作为底层机制,红黑树是平衡二叉树的一种,在要求上比AVL树更宽泛。通过map的迭代器只能修改其实值,不能修改其键值,所以map的迭代器既不能是const也不是mutable
红黑树满足以下几个条件:

  • 每个节点不是红色就是黑色
  • 根节点是黑色
  • 红色节点的子节点必须是黑色(不能有连续的红节点)
  • 从根节点到NULL的任何路径所含的黑节点数目相同
  • 叶子节点是黑色的NULL节点(注:这里不是我们常说的二叉树中的叶节点,是它的子节点(NULL))

迭代器失效问题

迭代器和指针的区别

迭代器不是指针,是类模板,表现的像指针,知识模拟了指针的功能,重载了指针的一些操作符,->, * , ++, --等;
迭代器封装了指针,是一个可遍历STL容器内全部或者部分元素的对象,本质上封装了原声指针,比指针更高级,相当于只能指针

迭代器返回的是引用,而不是对象的值

迭代器的种类

根据移动特性与实施的操作,迭代器被分为五类:

  • Input Iterator:只读迭代器,该迭代器所指的对象,不允许用户改变
  • Output Iterator:唯写
  • Forward Iterator:允许写入型算法在该迭代器所形成的区间上进行读写操作
  • Bidirectional Iterator:可双向移动
  • Random Acess Iterator:前三种迭代器支持operator++,第四种迭代器再加上operator--,第五种涵盖所有指针算术能力,包括 p + n, p - n, p[n], p1 - p2, p1 < p2

为什么vector的插入操作会导致迭代器失效?

vector动态增加空间时,并不是在原空间之后增加新的空间,而是以原来大小的两倍或者原空间加上实际所需的空间的大小另外配置一片较大的空间,释放原来的空间。由于操作改变了空间,所以原来的迭代器失效

vector每次insert或者erase之后,以前保存的迭代器会不会失效?

  • 在进行insert时,如果在p位置插入新的元素。当容器有剩余空间,不需要重新分配空间时,p之前的迭代器都有效,p之后的迭代器都失效;当容器重新分配了内存空间,那么所有的迭代器都失效
  • 进行erase时,erase的位置在p处,p之前的迭代器都有效且p指向下一个元素位置(如果p在尾元素处,p指向无效end无效),p之后的迭代器都无效。

deque插入和删除元素,以前保存的迭代器是否失效?

  • 在中间插入或者删除元素,将使deque所有的迭代器、引用、指针失效
  • 在首部或者尾部插入元素可能会使迭代器失效(缓冲区空间已满,需重新分配内存),但不会引起指针或者引用失效
  • 在首部或者尾部删除元素,只会使指向被删除的元素迭代器失效

vector,list,deque,map在erase(iter)后迭代器如何变化

  • vector和deque是序列式容器,其内存分别是连续空间和分段式连续空间,删除迭代器iter后,其后面的迭代器都失效了,此时iter指向被删除元素的下一个位置
  • list删除迭代器iter时,其后面的迭代器不会失效,将前面和后面连接起来即可
  • map删除iter时,只是当前删除的迭代器失效,其后面的迭代器依然有效

容器间的对比

vector 插入删除操作和list有什么区别

  • vector插入删除操作需要进行元素的移动,如果vector所存储的对象很大或者构造函数比较复杂,则开销较大,如果是简单的小数据效率优于list
  • list插入删除操作需要遍历当前数据,但在首部插入效率很高

什么情况下用list,什么情况下用vector

  • 当数据对象简单,对象数量变化不大,需要频繁的随机访问时,用vector
  • 当数据对象复杂,对象的数量频繁变化,频繁的进行插入和删除操作时,用list

deque和vector的区别

  • vector是单向开口的连续区间,deque是双向开口的连续区间(可以在头尾两端进行插入和删除操作)
    deque提供随机访问迭代器,但是迭代器比vector复杂很多
  • deque没有提供空间保留功能,也就是没有capacity这个概念,而vector提供了空间保留功能。即vector有capacity和reserve函数,deque 和 list一样,没有这两个函数。

vector,list,deque对比

  • vector数据在内存中连续排列,所以随机存取元素的速度最快。但是在除尾部以外的位置删除或者添加元素的时候速度很慢
  • list数据是链式存储,不能随机存取。其优势在于在任意位置添加和删除元素
  • deque通过链接若干片连续的数据实现的,均衡了以上两容器的特点

hash_map和map的区别?什么时候用map,什么时候用hash_map

  • 构造函数:hash_map需要hash function以及等于函数,map需要比较函数
  • 存储结构:hash_map以hashtable为底层,map以红黑树为底层
  • 查找速度:总体来说,hash_map查找速度比map快,而且查找速度基本和数据量的大小无关,属于常数级别;map的查找速度是(logn)级别。并不一定常数级别就比(logn)小,hash_map的hash function也会耗时

二者如何选择:

  • 如果考虑效率,特别是元素达到一定的数量级时,用hash_map
  • 如果考虑内存,或者元素比较少时,用map

hashtable,hash_set,hash_map的区别

  • hash_set以hashtable为底层,不具有排序功能,能快速查找,其键值就是实值
  • hash_map以hashtable为底层,不具有自动排序功能,能快速查找,每一个元素同时拥有键值和实值

map和set的区别

  • 相同点:map和set都是c++的关联容器,底层都是红黑树实现的
  • 元素: map的元素是key-value(键值—实值)对,关键字起到索引的作用,值表示与索引相关联的数据;set的元素是键值,没有实值
  • 迭代器:map的迭代器既不是const也不是mutable,map允许修改value实值,不允许修改key键值;set的迭代器是const的,不允许修改键值。其原因在于map和set是根据关键字来保证其有序性的,如果允许修改键值,那么首先要删除该键,调节平衡,然后再插入修改后的键值,调节平衡,这样一来破坏了map和set的结构,导致iterator失效。
  • 下标操作:map支持下标操作,用关键字作为下标访问关键字对应的值,如果关键字不存在,他会自动将该关键字插入;set不支持下标操作

map和set的相关问题

为什么map和set插入和删除效率比其他容器高?

不需要内存的拷贝和移动

为什么map和set每次insert后,以前保存的迭代器不会失效?

因为插入操作只是节点的指针的交换,节点并没有改变,节点的内存没有改变,指向内存的指针也不会改变

当数据元素增多时(从10000增加到20000),map和set的查找速度会怎样?

二者的底层是基于红黑树来实现的,查找的时间复杂度为logn,数据量从10000增加到20000,查找的次数从log10000 = 14 增加到 log20000 = 15,只是增加了1次

为什么map和set不能像vector一样有个reserve函数来预分配数据

map和set内部存储的已经不是元素本身了,而是包含元素的一个节点。他们内部使用的配置器不是在声明的时候传入的alloc而是转换后的alloc。

list自带排序函数的原理

说明:STL中的sort方法,接收的输入迭代器是支持随机访问的,双向list链表容器的访问方式是双向迭代器,不能够用STL的排序算法。
在list中自己定义的排序算法,有点像是归并排序,它的排序过程是:

  • 首先将前两个元素合并
  • 再将后两个元素合并
  • 然后将这四个元素合并
  • 重复上述过程,得到8个,16个,……,子序列
  • 最终将所有的子序列合并得到的就是排序后的序列

时间复杂度:O(nlogn)

实现方式:

  • 定义了类似搬运作用的链表carry以及中转站作用的链表数组counter
  • 链表数组counter[i]里面存储的元素最多是2^(i + 1),如果存储的数据超过该数字,则把counter[i]里面的数据都合并到counter[i + 1]链表中
  • carry负责取出原始链表的第一个数据,以及交换数据
  • 代码中的fill,表示当前的counter[fill]可处理数据的个数为2^(fill),指示当前情况下的最后一个存放链表的位置的下一个位置

不允许有遍历行为(不提供迭代器)的容器有哪些?

  • queue:只能获取头部元素,不能获取其他地方的元素
  • stack:只能获取顶端的元素
  • heap:所有的元素必须遵循遍历规则,不能遍历

vector中erase方法和algorithm中remove方法的区别

  • vector中erase方法是真正删除了元素,迭代器不能访问了
  • remove只是将元素移动到容器的最后面,迭代器还是可以访问到。因为remove只是通过迭代器访问容器,并不知道容器的内部结构,所以无法进行真正的删除。

例如序列[0,1,0,2,0,3,0,4],如果执行remove()希望移除所有的0,执行结果将是[1,2,3,4, 0,3,0,4],每一个和0不想等的元素被拷贝到前四个位置上,第四个位置以后的元素不动

reserve和resize的区别

  • resize(size_type n, value_type val = value_type()):改变的是当前容器内元素的数量,也就是改变的size()。如果n小于当前容器的元素数量,则容器中只会取前n个元素,多余的会被移除;否则会在当前元素的最后插入n - size()个元素,元素的值为其传入的参数,如果未传入,则是默认的
  • reserve(size_type n):改变的是容器的容量,也就是capacity()。如果n大于当前的容量,就会分配空间扩增容量;否则,将不会做任何处理

STL源码中的hash表的实现

hashtable是采用开链法来完成的,(vector+list)

底层键值序列采用vector实现,vector的大小取的是质数,且相邻质数的大小约为2倍关系,当创建hashtable时,会自动选取一个接近所创建大小的质数作为当前hashtable的大小;
对应键的值序列采用单向list实现;
当hashtable的键vector的大小重新分配的时候,原键的值list也会重新分配,因为vector重建了相当于键增加了,那么原来的值对应的键可能就不同于原来分配的键,这样就需要重新确定值的键。

STL中unordered_map和map的区别

  • 底层实现不同:
    unordered_map底层实现是一个哈希表,元素无序
    map底层实现是红黑树,其内部所有的元素都是有序的,因此对map的所有操作,其实都是对红黑树的操作
  • 优缺点:
    unordered_map:查找效率高;但是建立哈希表比较耗费时间
    map:内部元素有序,查找和删除操作都是logn的时间复杂度;但是维护红黑树的存储结构需要占用一定的内存空间
  • 适用情况:
    对于要求内部元素有序的使用map,对于要求查找效率的用unordered_map

STL中vector的实现

vector是一个动态数组,底层实现是一段连续的线性内存空间。
扩容的本质:当vector实际所占用的内存空间和容量相等时,如果再往其中添加元素需要进行扩容。其步骤如下:

  • 首先,申请一块更大的存储空间,一般是增加当前容量的50%或者100%,和编译器有关;
  • 然后,将旧内存空间的内容,按照原来的顺序放到新的空间中
  • 最后,将旧内存空间的内容释放掉,本质上其存储空间不会释放,只是删除了里面的内容。

从vector扩容的原理也可以看出:vector容器释放后,与其相关的指针、引用以及迭代器会失效的原因。

vector使用的注意点及其原因,频繁对vector调用push_back()对性能的影响和原因

主要是在插入元素方面:插入元素需要考虑元素的移动问题和是否需要扩容的问题
频繁的调用push_back()也是扩容的问题对性能的影响

posted @ 2022-03-03 10:35  DearLeslie  阅读(290)  评论(0编辑  收藏  举报