STL容器

三大组件介绍

1. 容器

几乎可以说,任何特定的数据结构都是为了实现某种特定的算法。STL容器就是将运用最广泛的一些数据结构实现出来。
常用的数据结构:数组(array) , 链表(list), tree(树),栈(stack), 队列(queue), 集合(set),映射表(map) , 根据数据在容器中的排列特性,这些数据分为序列式容器关联式容器 两种。

序列式容器 强调值的排序,序列式容器中的每个元素均有固定的位置,除非用删除或插入的操作改变这个位置。Vector容器、Deque容器、List容器等。

关联式容器 是非线性的树结构,更准确的说是二叉树结构。各元素之间没有严格的物理上的顺序关系,也就是说元素在容器中并没有保存元素置入容器时的逻辑顺序。关联式容器另一个显著特点是:在值中选择一个值作为关键字key,这个关键字对值起到索引的作用,方便查找。Set/multiset容器 Map/multimap容器

2. 算法

算法,问题的解法,以有限 的步骤,解决逻辑或数学上的问题。我们所编写的每个程序都是一个算法,其中的每个函数也都是一个算法,毕竟它们都是用来解决或大或小的逻辑问题或数学问题。STL收录的算法经过了数学上的效能分析与证明,是极具复用价值的,包括常用的排序,查找等等。特定的算法往往搭配特定的数据结构,算法与数据结构相辅相成。

算法分为:质变算法和非质变算法

质变算法:是指运算过程中会更改区间内的元素的内容。例如拷贝,替换,删除等等
非质变算法:是指运算过程中不会更改区间内的元素内容,例如查找、计数、遍历、寻找极值等等

3. 迭代器

迭代器(iterator)是一种抽象的设计概念,现实程序语言中并没有直接对应于这个概念的实物。 在Design Patterns一书中提供了23种设计模式的完整描述, 其中iterator模式定义如下:提供一种方法,使之能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。

迭代器的设计思维-STL的关键所在,STL的中心思想在于将容器(container)和算法(algorithms)分开,彼此独立设计,最后再一贴胶着剂将他们撮合在一起。

从技术角度来看,容器和算法的泛型化并不困难,c++的class template和function template可分别达到目标,如果设计出两这个之间的良好的胶着剂,才是大难题。

STL容器

string容器

C风格字符串char *(以空字符结尾的字符数组)太过复杂难于掌握,不适合大程序的开发,所以C++标准库定义了一种string类,定义在头文件。

  • String和c风格字符串对比:

    • Char*是一个指针,String是一个类
    • string封装了char*,管理这个字符串,是一个char*型的容器。
    • String封装了很多实用的成员方法
    • 查找find,拷贝copy,删除delete 替换replace,插入insert
    • 不用考虑内存释放和越界
    • string管理char*所分配的内存。每一次string的复制,取值都由string类负责维护,不用担心复制越界和取值越界等。

vector容器

Array是静态空间,一旦配置了就不能改变,要换大一点或者小一点的空间,首先配置一块新的空间,然后将旧空间的数据搬往新空间,再释放原来的空间。

Vector是动态空间,随着元素的加入,它的内部机制会自动扩充空间以容纳新元素。因此vector的运用对于内存的合理利用与运用的灵活性有很大的帮助。

Vector所采用的数据结构非常简单,线性连续空间,它以两个迭代器_Myfirst和_Mylast分别指向配置得来的连续空间中目前已被使用的范围,并以迭代器_Myend指向整块连续内存空间的尾端。为了降低空间配置时的速度成本,vector实际配置的大小可能比客户端需求大一些,以备将来可能的扩充,这边是容量的概念。换句话说,一个vector的容量永远大于或等于其大小,一旦容量等于大小,便是满载,下次再有新增元素,整个vector容器就得另觅居所。

动态扩展: 动态增加大小并不是在原空间之后续接新空间(因为无法保证之后尚有可供分配的空间),而是找更大的内存空间,然后将原数据拷贝新空间,释放原空间(每次再分配原大小两倍的内存空间)。因此,对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。重新分配的空间为原来的两倍

Vector维护一个线性空间,所以不论元素的型别如何,普通指针都可以作为vector的迭代器,因为vector迭代器所需要的操作行为,如operaroe*, operator->, operator++, operator–, operator+, operator-, operator+=, operator-=, 普通指针天生具备。Vector支持随机存取,而普通指针正有着这样的能力。所以vector提供的是随机访问迭代器(Random Access Iterators).

deque容器

Vector容器是单向开口的连续内存空间(vector 容器也可以在头尾两端插入元素,但是在其头部操作效率奇差),deque则是一种双向开口的连续线性空间(头尾两端分别做元素的插入和删除操作),每段数据空间内部是连续的,而每段数据空间之间则不一定连续。

deque内部有个中控器,维护每段缓冲区中的内容,缓冲区中存放真实数据中控器维护的是每个缓冲区的地址,使得使用deque时像一片连续的内存空间。deque采用一块所谓的map(注意,不是STL的map容器)作为主控。这里所谓map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指针,指向另一段(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的储存空间主体

  • Deque容器和vector容器最大的差异

    • 一、在于deque允许使用常数项时间对头端进行元素的插入和删除操作。
    • 二、在于deque没有容量的概念,因为它是动态的以分段连续空间组合而成,随时可以增加一段新的空间并链接起来,换句话说,像vector那样,”旧空间不足而重新配置一块更大空间,然后复制元素,再释放旧空间”这样的事情在deque身上是不会发生的。也因此,deque没有必须要提供所谓的空间保留(reserve)功能.
    • 三、deque容器也提供了Random Access Iterator,但是它的迭代器并不是普通的指针,其复杂度和vector不是一个量级,这当然影响各个运算的层面。因此,除非有必要,我们应该尽可能的使用vector,而不是deque。对deque进行的排序操作,为了最高效率,可将deque先完整的复制到一个vector中,对vector容器进行排序,再复制回deque.
  • deque容器实现原理

    • Deque容器是连续的空间,至少逻辑上看来如此,连续现行空间总是令我们联想到array和vector,array无法成长,vector虽可成长,却只能向尾端成长,而且其成长其实是一个假象,事实上(1) 申请更大空间 (2)原数据复制新空间 (3)释放原空间 三步骤,如果不是vector每次配置新的空间时都留有余裕,其成长假象所带来的代价是非常昂贵的。

    • Deque是由一段一段的定量的连续空间构成。一旦有必要在deque前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在deque的头端或者尾端。Deque最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。

    • 既然deque是分段连续内存空间,那么就必须有中央控制,维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。Deque代码的实现远比vector或list都多得多。

    • Deque采取一块所谓的map(注意,不是STL的map容器)作为主控,这里所谓的map是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是deque的存储空间的主体。

    • deque除了维护一个指向map的指针外,也维护start、finish两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素(的下一位置)。此外,也必须记住目前的map大小,因为一旦map的所提供的空间不足,它将需要重新配置一个更大的空间,依然是经过三个步骤:配置更大的、拷贝原来的、释放原空间。

stack容器

stack是一种先进后出(First In Last Out,FILO)的数据结构,它只有一个出口,stack容器允许新增元素,移除元素,取得栈顶元素,但是除了最顶端外,没有任何其他方法可以存取stack的其他元素。换言之,stack不允许有遍历行为。有元素推入栈的操作称为:push,将元素推出stack的操作称为pop.

stack没有迭代器
Stack所有元素的进出都必须符合”先进后出”的条件,只有stack顶端的元素,才有机会被外界取用。Stack不提供遍历功能,也不提供迭代器。

底层实现:deque
在STL中栈的的默认容器是双端队列 deque,也可以使用 list 和vector 自定义队列,因为 list 和 vector 都提供了删除最后一个元素的操作(出栈)。

queue容器

Queue是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口,queue容器允许从一端新增元素,从另一端移除元素。

queue没有迭代器
Queue所有元素的进出都必须符合”先进先出”的条件,只有queue的顶端元素,才有机会被外界取用。Queue不提供遍历功能,也不提供迭代器。

底层实现:deque
在STL中队列queue的默认容器是双端队列 deque,也可以使用 list 自定义队列,但是vector不行,因为vector不能提供删除第一个元素这个操作。

priority_queue优先队列
普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先出队的行为特征。
优先队列实现了类似的功能(其实底层就是用堆实现的)。
STL默认使用 <操作符来确定对象之间的优先级关系(也就是从大到小排序,默认大根堆)
优先队列的底层是用堆实现的。 在优先队列中默认存放数据的容器是vector,在声明时也可以用deque(双向队列)
没有迭代器,不提供遍历功能

list容器

链表是一种物理存储单元上非连续、非顺序 的存储结构,链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。数据元素的逻辑顺序是通过链表中的指针链接次序实现的。相较于vector的连续线性空间,list就显得负责许多,它的好处是每次插入或者删除一个元素,就是配置或者释放一个元素的空间。因此,list对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插入或元素的移除,list永远是常数时间。

  • list器实现原理
    • 数据结构:环状双向链表
    • 采用动态存储分配,不会造成内存浪费和溢出
    • 插入(insert)和接合(splice)操作都不会造成原来list的迭代器失效
    • 删除(erase)操作仅仅使“指向被删除元素”的迭代器失效,其它迭代器不受影响
    • 随机访问比较慢,空间和时间额外耗费较大
    • 任何位置元素的插入和删除,list是常数时间

链表较长(单元数量很多)又无法提前知道数量,只能一个个 push_back,这时如果用 vector 效率受影响,因为 vector 增长过程中会不时的重新分配内存,每重新分配一次就要把 vector 中已有的数据都 copy 一遍。
链表中按值保存一个自定义结构,结构中有资源指针(不是智能指针),虽然结构的析构函数会释放资源,但这时只能用 list,如果用 vector,出现 vector 重新分配内存后就会出错。如果实在想用 vector,可以给自定义结构写拷贝构造函数或在 vector 里保存结构的智能指针。
链表中间位置频繁插入删除元素,用 list。

  • List容器的迭代器

    • List容器不能像vector一样以普通指针作为迭代器,因为其节点不能保证在同一块连续的内存空间上。

    • List迭代器必须有能力指向list的节点,并有能力进行正确的递增、递减、取值、成员存取操作。所谓”list正确的递增,递减、取值、成员取用”是指,递增时指向下一个节点,递减时指向上一个节点,取值时取的是节点的数据值,成员取用时取的是节点的成员。

    • 由于list是一个双向链表 ,迭代器必须能够具备前移、后移的能力,所以list容器提供的是Bidirectional Iterators.

    • List有一个重要的性质,插入操作和删除操作都不会造成原有list迭代器的失效。这在vector是不成立的,因为vector的插入操作可能造成记忆体重新配置,导致原有的迭代器全部失效,甚至List元素的删除,也只有被删除的那个元素的迭代器失效,其他迭代器不受任何影响。

set/multiset容器

Set的所有元素都会根据元素的键值自动被排序。Set的元素不像map那样可以同时拥有实值和键值,set的元素即是键值又是实值。Set不允许两个元素有相同的键值。

  • Set器实现原理
    • 数据结构:底层使用平衡的搜索树——红黑树 实现
    • set中的元素都是排序好的
    • set中的元素都是唯一的,没有重复的
    • 插入删除操作时仅仅需要指针操作节点即可完成,不涉及到内存移动和拷贝
    • set中元素都是唯一的,而且默认情况下会对元素自动进行升序排列
    • 支持集合的交(set_intersection),差(set_difference) 并(set_union),对称差 (set_symmetric_difference) 等一些集合上的操作
    • set内部元素也是以键值对的方式存储的,只不过它的键值与实值相同
    • set中不允许存放两个实值相同的元素
    • 我们不可以通过set的迭代器改变set元素的值,因为set元素值就是其键值,关系到set元素的排序规则。如果任意改变set元素值,会严重破坏set组织。换句话说,set的iterator是一种const_iterator,即迭代器是被定义成constiterator的,说明set的键值是不允许更改的,并且不允许通过迭代器进行修改set里面的值
    • set拥有和list某些相同的性质,当对容器中的元素进行插入操作或者删除操作的时候,操作之前所有的迭代器,在操作完成之后依然有效,被删除的那个元素的迭代器必然是一个例外。

⭐为什么底层不用hash:
首先set,不像map那样是key-value对,它的key与value是相同的。关于set有两种说法,第一个是STL中的set,用的是红黑树;

第二个是hash_set,底层用得是hash table。红黑树与hash table最大的不同是,红黑树是有序结构,而hash table不是。但不是说set就不能用hash,如果只是判断set中的元素是否存在,那么hash显然更合适,因为set 的访问操作时间复杂度是log(N)的,而使用hash底层实现的hash_set是近似O(1)的。

然而,set应该更加被强调理解为“集合”,而集合所涉及的操作并、交、差等,即STL提供的如交集set_intersection()、并集set_union()、差集set_difference()和对称差集set_symmetric_difference(),都需要进行大量的比较工作,那么使用底层是有序结构的红黑树就十分恰当了,这也是其相对hash结构的优势所在。

  • multiset容器基本概念

    • multiset特性及用法和set完全相同,唯一的差别在于它允许键值重复。
    • set和multiset的底层实现是红黑树 .
  • hash_set容器基本概念

    • 底层:hashtable
    • set有自动排序功能而hash_set没有。

map/multimap容器

  • Map器实现原理

    • 数据结构:红黑树变体的平衡二叉树数据结构
    • Map的特性是,所有元素都会根据元素的键值自动排序。
    • map中key的值是唯一的。Map所有的元素都是pair,同时拥有实值和键值,pair的第一元素被视为键值,第二元素被视为实值,map不允许两个元素有相同的键值。如果新插入的键值与原有的键值重复则插入无效,可以通过insert的返回的pair中第二个bool型变量来判断是否插入成功来判断是否成功插入.
    • 提供基于key的快速检索能力
    • 元素插入是按照排序规则插入的,不能指定位置插入
    • 对于迭代器来说,可以修改实值,而不能修改key。我们不可以通过map的迭代器改变map的键值, 因为map的键值关系到map元素的排列规则,任意改变map键值将会严重破坏map组织。如果想要修改元素的实值,那么是可以的。
    • 根据key值快速查找,查找的复杂度基本是log2n
    • Map和list拥有相同的某些性质,当对它的容器元素进行新增操作或者删除操作时,操作之前的所有迭代器,在操作完成之后依然有效,当然被删除的那个元素的迭代器必然是个例外。
  • Multimap器实现原理

    • Multimap和map的操作类似,唯一区别multimap键值可重复。
    • Map和multimap都是以红黑树为底层实现机制。
  • hashtable器实现原理

    • 底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
    • 初始size为11,扩容:newsize = olesize*2+1
    • 计算index的方法:index = (hash &0x7FFFFFFF) % tab.length
  • hash_map器实现原理

    • 底层:hashtable
    • map的元素有自动排序功能而hash_map没有
    • 底层数组+链表实现,可以存储null键和null值,线程不安全
    • 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
    • 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
    • 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
    • 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀 计算index方法:index = hash& (tab.length – 1)
    • 数组的特点是:寻址容易,插入和删除困难。
    • 链表的特点是:寻址困难,插入和删除容易。
    • 对于某个元素,我们通过哈希算法,根据元素特征计算元素在数组中的下标,从而将元素分配、插入到不同的链表中去。在查找时,我们同样通过元素特征找到正确的链表,再从链表中找出正确的元素。
  • unordered_map器实现原理

    • 无序
  • map、hash_map、unordered_map比较

    • 运行效率方面:unordered_map最高,hash_map其次,而map效率最低单提供了有序的序列。
    • 占用内存方面:hash_map内存占用最低,unordered_map其次(数量少时优于hash_map),而map占用最高。
    • 需要无序容器时候用unordered_map,有序容器时候用map。
    • 无论从查找、插入上来说,unordered_map的效率都优于hash_map,更优于map;而空间复杂度方面,hash_map最低,unordered_map次之,map最大。

常见面试问题

resize和reserve的区别

必须了解

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

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

reserve:

  • reserve是设置了capacity的值,比如reserve(20),表示该容器最大容量为20,但此时容器内还没有任何对象,也不能通过下标访问。
  • reserve只修改capacity大小,不修改size大小,resize既修改capacity大小,也修改size大小。
  • reserve是容器预留空间,但并不真正创建元素对象,在创建对象之前,不能引用容器内的元素,因此当加入新的元素时,需要用push_back()/insert()函数。

resize:

  • resize既分配了空间,也创建了对象,可以通过下标访问。当resize的大小

  • resize是改变容器的大小,并且创建对象,因此,调用这个函数之后,就可以引用容器内的对象了,因此当加入新的元素时,用operator[]操作符,或者用迭代器来引用元素对象。

  • 再者,两个函数的形式是有区别的,reserve函数之后一个参数,即需要预留的容器的空间;resize函数可以有两个参数,第一个参数是容器新的大小,第二个参数是要加入容器中的新元素,如果这个参数被省略,那么就调用元素对象的默认构造函数。

  • 如果n比当前的vector元素数目要小,vector的容量要缩减到resize的第一个参数大小,既n。并移除那些超出n的元素同时销毁他们。

  • 如果n比当前vector元素数目要大,在vector的末尾扩展需要的元素数目,如果第二个参数val指定了,扩展的新元素初始化为val的副本,否则按类型默认初始化。

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

  • 因此为了避免内存的重新配置,可以用reserve预留出足够的空间或者利用构造函数构造足够的空间。

lower_bound()和upper_bound()简单总结

⭐lower_bound()和upper_bound()都是c++ 标准库中的函数。

  • 二者都利用二分查找的方法查找已排序的数组中的元素。

  • 它们的返回值都是一个地址。

  • lower_bound()用于在已排好序的数组中找出大于等于目标元素的下标最小的元素的地址。

  • upper_bound()用于在已排好序的数组中找出大于目标元素的下标最小的元素的地址。

template <class ForwardIterator, class T, class Compare>
ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last,const T& val, Compare comp);
  • 第一个参数是被查找数组的首地址,第二个参数是末地址,第三个是要目标元素,第四个是被查数组的排序方式。
  • 被查数组排序方式写的时候返回值应为bool类型,设定两个参数(与被查数组内元素类型相同即可)。

vector的扩容过程

⭐Vector在中分配了一段连续的内存空间来存放元素。

  • 包括三个迭代器,first 指向的是vector中对象的起始字节位置;last指向当前最后一个元素的末尾字节;end指向整个vector容器所占用内存空间的末尾字节。
    • last - first表示 vector 容器中目前已被使用的内存空间;
    • end - last 表示 vector 容器目前空闲的内存空间;
    • end - first表示 vector 容器的容量。
      在这里插入图片描述
vector<int>v;

v.push_back(1);

 std::vector<int>::iterator iter1 = v.begin();
 
 v.push_back(2);
 v.push_back(3);
 v.push_back(4);
 v.push_back(5);
  • 若声明时未设定好vector的大小,每来一个元素,就意味着会重新开辟空间,拷贝元素,这样不仅仅时间复杂度上很高。而且在重新开辟空间之后,原来的指向第一个元素的迭代器,也就是那个指针无法使用了。

  • 这时reserve函数的作用就体现出来了,如果一开始预留的空间足够大,也就是capacity足够使用,那么在这个过程中,不用重新开辟空间,这样原来的迭代器还可以使用,这就是reserve的作用。

    • 避免内存重新配置的方法(尽量避免迭代器的失效)
    • 可以使用Reserve()保留适当容量

解决方案:
在创建容器后,第一时间为容器分配足够大的空间,避免重新分配内存。

    std::vector<int> v;//create an empty vector
    v.reserve(80);// reserve memory for 80 elements

这样在push_back的过程中,在不超过capacity之前,都不需要重新开辟空间迭代器都不会失效

同时也可以使用resize的方法为接下来的数据预留空间,只不过使用resize会使得capacity变得比当前的size更大。

下面说一下关于reserve和resize之间的区别:

在前面如果说使用reserve(10)

将容器的大小设置成10

那么之后每次push_back的过程中,在数量小于10的情况下,不会去重新开辟内存空间,然后进行数据的拷贝等等操作。

减少内存拷贝的复杂,和迭代器的失效等问题

如果说设置成resize,此时空间size为10,且capacity为10,并且里面被填充上了0,

即使之后push_back(1),这时因为capacity已经满了,此时还是需要重新开辟内存空间,之前的迭代器失效

那么此时设置resize的目的在哪呢?因为此时再次开辟空间的时候,因为有一个基数10在那,所以在进行空间开辟的时候,之间去另一个内存空间处,开辟出15大小的capacity。这样在接着进行push_back的过程中,就可以减少空间的开辟次数。

在vector中reserve和resize之间的区别:

1.reserve是开辟一段空的空间,push_back的过程中直接往里面插入数据,设置的大小就是capacity的大小

2.resize是开辟一段空间,但是全部填充元素0,push_back是在后面接着添加,但是不可避免的是,第一步就要重新开辟空间,而resize的初始值的意义只是有一个基数在那,使得capacity无论是以1.5还是2倍都会减少内存开辟的次数

class A
{
public:
    A(){ cout << "construct" << endl; }
    A(const A &a){ cout << "copy construct" << endl; }
    ~A(){ cout << "destruction" << endl; }
};
int main(void)
{
    vector<A> p;
    A a;
    A b;
    p.push_back(a);
    p.push_back(b);

}

在这里插入图片描述
分析:
首先构造a,b (construct,construct) push_back(a)会调用对象的复制构造函数构造一个对象p(copy construction),在将这个p的放入vector中(push_back的参数是const引用),当需要放b时,容量不够,扩容(分配新的大小使用allocate类,分配的capacity为原来的1.5(我测试是这样,但是源码中好像是两倍)),将容器里的对象复制构造然后放入到新的容器中(copy construction)),将旧容器里的对象删除(destruction),然后调用b的copy struction,最后程序结束后调用a,b和容器里的两个对象的析构函数(4个destruction)

⭐线性容器vector在拷贝构造数据时导致数据量size值扩大,size值扩大到等于capacity时,容器为了使capacity变大就会扩容。reserve函数数值扩容,避免容器自动扩容带来性能消耗,自动扩容导致拷贝构造函数频繁被调用

问题:
1、线性容器以vector为例,
2、每次扩大capacity时,容器就需要重新在堆中申请一块更大的内存,
3、然后将源容器中的值一个个拷贝构造到新容器中,
4、如果扩大的capacity不够,比如一次push_back了100000个数据到vector容器中,vector容器就会不停的扩容,每次扩容都会消耗更多的内存和时间。
5、这会导致大量的内存消耗,时间消耗,十分影响程序性能。
6、为什么要成倍的扩容而不是一次增加一个固定大小的容量呢?

  • 采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容量只能达到O(n)的时间复杂度。

7、为什么是以两倍的方式扩容而不是三倍四倍,或者其他方式呢?

  • 考虑可能产生的堆空间浪费,所以增长倍数不能太大,一般是1.5或2;GCC是2;VS是1.5k =2 每次扩展的新尺寸必然刚好大于之前分配的总和,之前分配的内存空间不可能被使用,这样对于缓存并不友好,采用1.5倍的增长方式可以更好的实现对内存的重复利用。C++并没有规定扩容因子K,这是由标准库的实现者决定的。

vector的insert何时会引起内存的重新分配

迭代器失效的情况

C++中迭代器失效
迭代器iterator就是类似指针,迭代器是一个变量,相当于容器和操纵容器的算法之间的中介。迭代器可以指向容器中的某个元素,通过迭代器就可以读写它指向的元素。
迭代器失效情况根据数据结构分为三种情况,分别为数组型,链表型,树型数据结构。

⭐针对数组型数据结构

  • 数组型结构有vector、deque等,由于它们的元素是分配在连续的内存中,当进行insert和erase操作,都会使得插入点和删除点之后的元素挪位置,插入点和删除掉之后的迭代器全部失效。
  • 解决方法就是更新迭代器,对于删除,erase()返回的是下一个有效迭代器的值,可以通过iter=vec.erase(iter);来避免迭代器失效。insert同理,insert返回的是插入元素的迭代器的值。
  • 注意:在deque容器首部或者尾部插入元素不会使得任何迭代器失效,在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效,在deque容器的任何其他位置的插入和删除操作将使指向该容器元素的所有迭代器失效

⭐针对链表型数据结构

  • 如list容器,使用了不连续分配的内存,删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器。
  • 解决办法有两种
    • 一种是erase(iter)会返回下一个有效迭代器的值,可以通过iter=vec.erase(iter);来避免迭代器失效
    • 另一种方法是通过erase(iter++);来避免迭代器失效,(顺便说一下,erase(iter++)避免迭代器失效的原理,先把iter传值到erase里面,然后iter自增,在失效前已经自增,然后执行erase将自增前的迭代器删除,自增前的迭代器失效),对于插入不会使迭代器失效。

⭐针对树形数据结构

  • 如map, set,multimap,multiset, 它们是使用红黑树来存储数据,插入不会使得任何迭代器失效,删除会使指向删除位置的迭代器失效,但是不会失效其他迭代器。
  • 解决方法:由于erase()返回值为void,所以要采用erase(iter++);来避免迭代器失效。

map和unordered_map的底层数据结构及为什么这么设计

内部实现机理不同

  • map: map内部实现了一个红黑树(红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉搜索树(又名二叉查找树、二叉排序树,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值)存储的,使用中序遍历可将键值按照从小到大遍历出来。
  • unordered_map: unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的。哈希表详细介绍

优缺点以及适用处
map:

  • 优点:

    • 有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作
    • 红黑树,内部实现一个红黑书使得map的很多操作在lgn的时间复杂度下就可以实现,因此效率非常的高
  • 缺点:

    • 空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间
  • 适用处:对于那些有顺序要求的问题,用map会更高效一些

unordered_map:

  • 优点: 因为内部实现了哈希表,因此其查找速度非常的快
  • 缺点: 哈希表的建立比较耗费时间
  • 适用处:对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_map
    总结:

内存占有率的问题就转化成红黑树 VS hash表 , 还是unorder_map占用的内存要高。
但是unordered_map执行效率要比map高很多
对于unordered_map或unordered_set容器,其遍历顺序与创建该容器时输入的顺序不一定相同,因为遍历是按照哈希表从前往后依次遍历的

map和unordered_map的使用

  • unordered_map的用法和map是一样的,提供了 insert,size,count等操作,并且里面的元素也是以pair类型来存贮的。其底层实现是完全不同的,上方已经解释了,但是就外部使用来说却是一致的。
  • 5
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值