C++ STL容器的底层实现

https://blog.csdn.net/qq_43313035/article/details/89600276
顺序容器:

vector
deque
list

关联容器:

set
multiset
map
multimap

C++ STL容器的底层实现_第1张图片

vector(向量容器)

特点:

  • 内存可2倍增长的动态数组
  • 数据结构:线性连续空间

增加新元素时,如果超出当前容量,则容量会扩充至两倍。如果两倍还不够,则扩张至足够大的空间。
容量的扩张:

  1. 开辟新空间(因为无法保证之后尚有可供分配的空间)
  2. 复制数据于新空间中
  3. 释放旧空间

vector 对象本身在栈上,里面的成员指针指向堆。
vector 和基本数据类型一样使用,定义指针一样。
vector 内的元素存放在堆中,由STL库中的程序负责分配和释放,使用方便,但是效率低下。

原生数组的执行效率是vector的10倍左右,因为他内存的分配和释放完全由系统自动完成。

vector效率低下的原因:vector的动态自增,并不是在原空间之后接续新空间,而是在原空间不够使用时,以原空间大小的两倍另外配置一块大空间,然后将原内容拷贝过来,再在新拷贝的原内容之后构造新元素,并释放原空间。

size指的是当前拥有的元素个数,capacity指的是vector当前分配的总个数,所有capacity>=size。

C++ STL容器的底层实现_第2张图片

deque(双端队列)

特点:

  • 数据结构:一种双向开口的存储空间分段连续的数据结构,每段数据空间内部是连续的,而每段数据空间之间则不一定连续。
  • deque没有所谓的容量概念,因为它是以分段连续空间组合而成,随时可以增加一段新的空间并连接起来
    C++ STL容器的底层实现_第3张图片
    图deque有四部分数据空间,这些空间都是程序运行过程中在堆上动态分配的。
    中控器(map)保存着一组指针,每个指针指向一段数据空间的起始位置,通过中控器可以找到所有的数据空间。如果中控器的数据空间满了,会重新申请一块更大的空间,并将中控器的所有指针拷贝到新空间中。

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

list

  • 有效利用空间
  • 数据结构:环状双向链表
  • 插入(insert)和接合(splice)操作都不会造成原来list的迭代器失效
  • 删除(erase)操作仅仅使“指向被删除元素”的迭代器失效,其它迭代器不受影响
  • 随机访问比较慢
  • 每次插入或删除一个元素,就配置或释放一个元素空间,元素也是在堆中。因此,list 对于空间的运用有绝对的精准,一点也不浪费。

为什么list容器可以很方便的随机插入:

list是一个线性双向链表结构,它的数据由若干个节点构成,每一个节点都包括一个信息块(即实际存储的数据)、一个前驱指针和一个后驱指针。它无需分配指定的内存大小且可以任意伸缩,这是因为它存储在非连续的内存空间中,并且由指针将有序的元素链接起来。由于其结构的原因,list 随机检索的性能非常的不好,因为它不像vector 那样直接找到元素的地址,而是要从头一个一个的顺序查找,这样目标元素越靠后,它的检索时间就越长。检索时间与目标元素的位置成正比。虽然随机检索的速度不够快,但是它可以迅速地在任何节点进行插入和删除操作。因为list 的每个节点保存着它在链表中的位置,插入或删除一个元素仅对最多三个元素有所影响,不像vector 会对操作点之后的所有元素的存储地址都有所影响,这一点是vector 不可比拟的。

关联容器

set(集合)

特点:

  • 数据结构:底层使用平衡的搜索树——红黑树实现
  • 插入删除操作时仅仅需要指针操作节点即可完成,不涉及到内存移动和拷贝
  • set中元素都是唯一的,而且默认情况下会对元素自动进行升序排列
  • set内部元素也是以键值对的方式存储的,只不过它的键值与实值相同
  • set中不允许存放两个实值相同的元素
  • 迭代器是被定义成const iterator的,说明set的键值是不允许更改的,并且不允许通过迭代器进行修改set里面的值

multiset

特点:

  • 数据结构:底层实现与set一样,也采用了红黑树
  • 允许插入重复的键值,使用insert_equal机制
  • 插入、删除操作的时间复杂度为O(log2n)

map

  • map中key的值是唯一的
  • 数据结构:红黑树变体的平衡二叉树数据结构
  • 提供基于key的快速检索能力
  • 元素插入是按照排序规则插入的,不能指定位置插入
  • 对于迭代器来说,可以修改实值,而不能修改key。
  • 根据key值快速查找,查找的复杂度基本是logn
  • map在进行插入的时候是不允许有重复的键值的,如果新插入的键值与原有的键值重复则插入无效

multimap

特点:

  • 与 map 不同,multimap 可以包含重复键

unordered_map

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

特点:

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

Hash_Map

hash_map基于hash table(哈希表)。

哈希表最大的优点:

就是把数据的存储和查找消耗的时间大大降低,几乎可以看成是常数时间;而代价仅仅是消耗比较多的内存。然而在当前可利用内存越来越多的情况下,用空间换时间的做法是值得的。另外,编码比较容易也是它的特点之一。

其基本原理是:

使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数,也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标,hash值)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素“分类”,然后将这个元素存储在相应“类”所对应的地方,称为桶。

但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了“冲突”,换句话说,就是把不同的元素分在了相同的“类”之中。

总的来说,“直接定址”与“解决冲突”是哈希表的两大特点。

hash_map,首先分配一大片内存,形成许多桶。是利用hash函数,对key进行映射到不同区域(桶)进行保存。其插入过程是:

  1. 得到key
  2. 通过hash函数得到hash值
  3. 得到桶号(一般都为hash值对桶数求模)
  4. 存放key和value在桶内。

其取值过程是:

  1. 得到key
  2. 通过hash函数得到hash值
  3. 得到桶号(一般都为hash值对桶数求模)
  4. 比较桶的内部元素是否与key相等,若都不相等,则没有找到。
  5. 取出相等的记录的value。

hash_map和map的区别在哪里?

  • 构造函数。hash_map需要hash函数,等于函数;map只需要比较函数(小于函数).
  • 存储结构。hash_map采用hash表存储,map一般采用红黑树(RB Tree)实现。因此其memory数据结构是不一样的。

什么时候需要用hash_map,什么时候需要用map?

总体来说,hash_map 查找速度会比map快,而且查找速度基本和数据数据量大小,属于常数级别;而map的查找速度是log(n)级别。并不一定常数就比log(n)小,hash还有hash函数的耗时,明白了吧,如果你考虑效率,特别是在元素达到一定数量级时,考虑考虑hash_map。但若你对内存使用特别严格,希望程序尽可能少消耗内存,那么一定要小心,hash_map可能会让你陷入尴尬,特别是当你的hash_map对象特别多时,你就更无法控制了,而且hash_map的构造速度较慢。

关于Rehash

当键值对的数量>=设定的阀值(capacity * load factor(0.75))时,为保证HashMap的性能,会进行重散列(rehash)。

HashMap中,重散列主要有两步:1、扩充table长度。2、转移table中的entry,从旧table转移到新的table。

table长度以2倍的方式扩充,一直到最大长度2^30。

entry转移的过程是真正意义上的重散列,在此过程中,对原来的每个entry的key重新计算新的散列地址,旧table中相同位置的entry极有可能会被散列到新table中不同的位置,这主要是因为table的length变化的原因。

priority_queue(优先队列)

优先队列不是按照普通对象先进先出原FIFO则进行数据操作,其中的元素有优先级属性,优先级高的元素先出队。
优先队列的底层实现是堆,默认的优先队列是从大到小排序,底层也就是大顶堆。

优先队列的插入删除和大顶堆基本相同,每次插入和删除都需要进行对应的上浮和下沉操作,所有优先队列的入队和出队时间复杂度是logn。

PriorityQueue队列不适合经常出队入队的频繁操作,但是他的优先级特性非常适合一些对顺序有要求的数据处理场合。

你可能感兴趣的:(C++ STL容器的底层实现)