STL超全总结和梳理

本文详细介绍了STL中的各种容器,如vector、list、deque、set、map等的特性和底层实现。讨论了不同容器的插入、删除、查找效率,并提到了hash_map与map的差异,以及内存管理如reserve和resize的区别。还分析了迭代器的类型和容器对迭代器的影响,以及内存配置器的工作机制。
摘要由CSDN通过智能技术生成
  • 常用容器的特点及适用情况

    • string:与vector相似的容器,专门用于存储字符。随机访问快,在尾位置插入/删除速度快
    • array:固定大小数组。支持快速随机访问,不能添加或者删除元素
    • vector:可变大小的数组。底层数据结构为数组,支持快速随机访问,在尾部之外的位置插入或者删除元素可能很慢
    • list:双向链表。底层数据结构为双向链表,支持双向顺序访问。在list任何位置插入/删除速度很快
    • s_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的查找速度快,hash_map的查找速度是常数级别,map的查找速度是(logn)级别。但是,不一定常数就比logn小,hash还有hash函数耗时。当考虑效率,特别是当元素达到一定数量级时,考虑unordered_map;但如果对内存要求特别严格,希望少消耗内存,当hash_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动态增加空间时,并不是在原空间之后增加新的空间,而是以原来大小的两倍或者原空间加上实际所需的空间的大小另外配置一片较大的空间,释放原来的空间。由于操作改变了空间,所以原来的迭代器失效
    • vector每次insert或者erase之后,以前保存的迭代器会不会失效?
      在进行insert时,如果在p位置插入新的元素。当容器有剩余空间,不需要重新分配空间时,p之前的迭代器都有效p之后的迭代器都失效;当容器重新分配了内存空间,那么所有的迭代器都失效;进行erase时,erase的位置在p处,p之前的迭代器都有效且p指向下一个元素位置(如果p在尾元素处,p指向无效end无效),p之后的迭代器都无效。
    • 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个元素,多余的会被移除/销毁,相当于capacity减少;否则会在当前元素的最后插入n - size()个元素,元素的值为其传入的参数,如果未传入,则是默认的。调整容器的大小,使其包含n个元素。请注意,此函数通过插入或擦除容器中的元素来更改容器的实际内容。
      • reserve(size_type n):改变的是容器的容量,也就是capacity()。如果n大于当前的容量,就会分配空间扩增容量;否则,将不会做任何处理。如果一个vector使用默认的capacity,那么在push_back操作的时候,会根据添加元素的数量,动态的自动分配空间;如果声明vector的时候,显式的使用capacity(size_type n)来指定vector的容量,那么在push_back的过程中(元素数量不超过n),vector不会自动分配空间,即不会再去检查!当push_back的元素数量大于n的时候,会重新分配一个大小为2n的新空间,其中n是reserve函数指定的大小,再将原有的n的元素和新的元素放入新开辟的内存空间中。
  • deque插入和删除元素,以前保存的迭代器是否失效?

    • 中间插入或者删除元素,将使deque所有的迭代器、引用、指针失效
    • 在首部或者尾部插入元素可能会使迭代器失效(缓冲区空间已满,需重新分配内存),但不会引起指针或者引用失效,在首部或者尾部删除元素,只会使指向被删除的元素迭代器失效
  • list删除迭代器iter时,其后面的迭代器不会失效,将前面和后面连接起来即可;map删除iter时,只是当前删除的迭代器失效,其后面的迭代器依然有效

  • 容器间的对比

    • deque和vector

      • vector是单向开口的连续区间,deque是双向开口的连续区间(可在头尾两端进行插入和删除操作)
      • deque提供随机访问迭代器,但是迭代器比vector复杂很多
      • **deque没有提供空间保留功能,也就是没有capacity这个概念,**而vector提供了空间保留功能。即vector有capacity和reserve函数,deque 和 list一样,没有这两个函数。
    • hash_map和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每次insert后,以前保存的迭代器不会失效?因为插入操作只是节点指针的交换,节点并没有改变,节点的内存没有改变,指向内存的指针也不会改变
      • 当数据元素增多时(从10000增加到20000),map和set的查找速度会怎样?二者的底层是基于红黑树来实现的,查找的时间复杂度为logn,数据量从10000增加到20000,查找的次数从log10000 = 14 增加到 log20000 = 15,只是增加了1次
      • 为什么map和set不能像vector一样有个reserve函数来预分配数据map和set内部存储的已经不是元素?本身了,而是包含元素的一个节点。他们内部使用的配置器不是在声明的时候传入的alloc而是转换后的alloc
  • hashtable是采用开链法来完成的,(vector+list)

    • 底层键值序列采用vector实现,vector的大小取的是质数,且相邻质数的大小约为2倍关系,当创建时会自动选取一个接近所创建大小的质数作为当前hashtable的大小;
    • 对应键的值序列采用单向list实现;
    • 当hashtable的键vector的大小重新分配的时候,原键的值list也会重新分配,因为vector重建了相当于键增加了,那么原来的值对应的键可能就不同于原来分配的键,这样就需要重新确定值的键。
  • 配置器所有的方法和成员都是静态的,那么他们放在静态区。只有程序结束后,才释放内存,这样会导致在程序运行的过程中,自由链表一直占用内存,自己的进程可以使用,其他的进程却用不了

  • 空间配置器的机制

    在C++中动态分配内存和释放内存分别用newdelete这两个关键字。

    class Foo {...};
    Foo* pf = new Foo; //配置内存,然后构造函数
    delete pf;//将对象析构然后释放内存
    
    • new内含两阶段操作:调用::operator new配置内存;调用Foo::Foo()构造对象的内容
    • delete内含两阶段操作:调用Foo::~Foo()将对象析构;调用::operator delete释放内存
    • ::operator new()和::operator delete()底层是调用malloc()和free()这两个函数来完成内存的配置和释放。
    • STL allocator将以上两阶段的操作由以下几个函数来完成
      • 内存配置:alloc:allocate()负责
      • 内存释放:allo::deallocate()负责
      • 对象构造:::construct()负责
      • 对象析构:::destory()负责
  • 双层级配置器:考虑到分配小的空间时可能会造成内存碎片问题,以及小块内存的频繁的申请和释放的性能问题,SGI STL设计了双层级配置器,默认使用第二级配置器。

    • 第一级配置器直接使用**malloc()和free()**进行内存空间的分配和释放
    • 第二级配置器视情况采取不同的策略
      • 当配置区块超过128bytes时,调用第一级配置器;当配置区块小于128bytes时,采用memory pool(内存池)的方式,通过空闲链表来管理内存,第二级配置器会自动将内存的需求量上调为8的倍数,并维护16个自由链表,自由链表是一个指针数组,数组大小为16,每个数组的元素代表所挂区块的大小,free_list[0] = 8, free_list[1] = 16,以此类推(8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128)如果该节点下面挂有未使用的内存,则摘下来直接使用这部分内寸。否则调用**refill(size_t n)**去内存池中申请。
      • 向内存池中申请时STL默认一次申请20个,将多余空间挂在自由链表上。refill使用chunk_alloc(size_t n, size_t& nobjs)函数去内存池中申请。如果申请成功,回到refill函数。
        • 如果nobjs = 1,表明内存池只够分配一个,返回这个地址就可以;如果大于一个,需要将剩余的挂到自由链表上
        • 如果chunk_alloc(size_t n, size_t& nobjs)失败,如果内存池剩余的空间足够 nobjs * n这么大,直接分配返回就OK。如果剩余的空间leftAlloc的范围是n<=leftAlloc<=nobjs*n,就分配 nobjs = (leftAlloc) / n个空间返回就可以。(相当于拆东墙补西墙)
        • 如果剩余的空间连一个n都不够,需要向heap申请内存,申请之前需要将内存池中剩余的内存挂在到自由链表上。如果申请成功,就再调用chunk_alloc进行分配。如果失败,去看自由链表中有没有比n大的空间,如果有就将这块空间放到内存池中,再调用chunk_alloc进行分配;否则调用一级配置器,交给内存不足处理机制处理
          在这里插入图片描述在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值