C/C++ STL

常见容器性质总结

1.vector 底层数据结构为数组 ,支持快速随机访问
2.list 底层数据结构为双向链表,支持快速增删
3.deque 底层数据结构为一个中央控制器和多个缓冲区,详细见STL源码剖析P146,支持首尾(中间
不能)快速增删,也支持随机访问
deque是一个双端队列(double-ended queue),也是在堆中保存内容的.它的保存形式如下:
[堆1] --> [堆2] -->[堆3] --> …
每个堆保存好几个元素,然后堆和堆之间有指针指向,看起来像是list和vector的结合品.
4.stack 底层一般用list或deque实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容
耗时
5.queue 底层一般用list或deque实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩
容耗时(stack和queue其实是适配器,而不叫容器,因为是对容器的再封装)
6.priority_queue 的底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现
7.set 底层数据结构为红黑树,有序,不重复
8.multiset 底层数据结构为红黑树,有序,可重复
9.map 底层数据结构为红黑树,有序,不重复
10.multimap 底层数据结构为红黑树,有序,可重复
11.unordered_set 底层数据结构为hash表,无序,不重复
12.unordered_multiset 底层数据结构为hash表,无序,可重复
13.unordered_map 底层数据结构为hash表,无序,不重复
14.unordered_multimap 底层数据结构为hash表,无序,可重复

每种容器对应的迭代器

容器 迭代器
vector、deque 随机访问迭代器
stack、queue、priority_queue
list、(multi)set/map 双向迭代器
unordered_(multi)set/map、forward_list 前向迭代器

vector

vector数据结构

vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。因此能高效的进行随机存取,时间复杂度为o(1);但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。连续存储结构:vector是可以实现动态增长的对象数组,支持对数组高效率的访问和在数组尾端的删除和插入操作,在中间和头部删除和插入相对不易,需要挪动大量的数据。它与数组最大的区别就是vector不需程序员自己去考虑容量问题,库里面本身已经实现了容量的动态增长,而数组需要程序员手动写入扩容函数进形扩容。vector中有三个指针first,last,end, 分别指向了数组的开头, 数组的结尾+1以及数组的最大容量结尾
C/C++ STL_第1张图片

vector扩容

● 可以使用reserve(n)预先分配一块较大的指定大小的内存空间,这样当指定大小的内存空间未使用完时,是不会重新分配内存空间的,这样便提升了效率。只有当n>capacity()时,调用reserve(n)才会改变vector容量。
● resize()成员函数只改变元素的数目,不改变vector的容量。
● vector以连续的数组存放数据,当vector空间已满时会申请新的空间并将原容器中的内容拷贝到新空间中,并销毁原容器
● 存储空间的重新分配会导致迭代器失效
● 因为分配空间后需要进行拷贝,编译器会预分配更多空间以减少发生拷贝影响程序效率
● 扩容的大小叫做扩容因子,扩容因子由编译器决定,VS的扩容因子为1.5,G++中,扩容因子为2

为什么是2倍扩容

理由一:
倍数方式空间拷贝数据次数

假设总共有n个元素,以m倍的形式增长。(比如现在举例n=100, m=2),所以,vector的push_back的操作次数可以是logmN,也就是log2(100),换算下来大概是需要进行7次扩容。这样的话,相当于旧空间数据到原空间数据的拷贝有7次。

个数方式空间拷贝数据次数

和倍数方式假设相同,n=100;这次m代表的是每次新空间的大小位n+m;m为新空间新增大小,比如这次m为10(每次新增10个空间)。所以这次的扩容次数为 100/10 = 10次,也就是说,插入100白个元素,需要扩容10次。

但是,如果n=1000的情况下, 以个数形式进行扩容就不能在为10了,否则拷贝空间次数将会太多
有的小伙伴要问:但是可以取100呀,想想,如果n=10的情况下,取100又不太合适,所以,以个数的形式来进行扩容显然不符合所用n的取值。
所以在STL中vector以倍数的形式进行扩容

理由二:
如果以大于2倍的方式来进行扩容,下一次申请空间会大于之前申请所有空间的总和,这样会导致之前的空间不能再被重复利用,这样是很浪费空间的操作。所以,如果扩容一般基于(1, 2] 之间进行扩容

vector缩容

首先vector删除元素时会不会释放空间,erase()函数,只能删除内容,不能改变容量大小;
由于vector的内存占用空间只增不减,比如你首先分配了10,000个字节,然后erase掉后面9,999个,留
下一个有效元素,但是内存占用仍为10,000个。所有内存空间是在vector析构时候才能被系统回收。
empty()用来检测容器是否为空的,clear()可以清空所有元素。但是即使clear(),vector所占用的内存空
间依然如故,无法保证内存的回收。
如果需要空间动态缩小,可以考虑使用deque。如果vector,可以用swap()来帮助你释放内存

vector(Vec).swap(Vec); 将Vec的内存空洞清除; 
vector().swap(Vec); 清空Vec的内存;

std::vector<T> tmp(v);
tmp.swap(v);

vector<int>(v).swap(v);  //关键的内存收缩语句

vector与list中sort的区别

vector中内存是连续分配的,而list是基于双向链表的。所以sort的时候,vector的sort是基于快排的,而list的sort是基于归并排序的

vector中下标越界访问

通过下标访问vector中的元素时不会做边界检查,即便下标越界。
也就是说,下标与first迭代器相加的结果超过了finish迭代器的位置,程序也不会报错,而是返回这个地
址中存储的值。
如果想在访问vector中的元素时首先进行边界检查,可以使用vector中的at函数。通过使用at函数不但
可以通过下标访问vector中的元素,而且在at函数内部会对下标进行边界检查。

push_back和emplace_back的区别

使用push_back()函数需要调用拷贝构造函数和转移构造
函数,而使用emplace_back()插入的元素原地构造,不需要触发拷贝构造和转移构造

list

  1. list不再能够像vector一样以普通指针作为迭代器,因为其节点不保证在存储空间中连续存在;
  2. list插入操作和结合才做都不会造成原有的list迭代器失效;
  3. list不仅是一个双向链表,而且还是一个环状双向链表,所以它只需要一个指针;
  4. list不像vector那样有可能在空间不足时做重新配置、数据移动的操作,所以插入前的所有迭代器在插
    入操作之后都仍然有效;
  5. deque是一种双向开口的连续线性空间,所谓双向开口,意思是可以在头尾两端分别做元素的插入和
    删除操作;可以在头尾两端分别做元素的插入和删除操作;
  6. deque和vector最大的差异,一在于deque允许常数时间内对起头端进行元素的插入或移除操作,二
    在于deque没有所谓容量概念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间
    并链接起来,deque没有所谓的空间保留功能

map,set

他们的底层都是以红黑树的结构实现,因此插入删除等操作都在O(logn)时间内完成,因此可以完成高
效的插入删除;

红黑树是怎么能够同时实现这两种容器

在这里我们定义了一个模版参数,如果它是key那么它就是set,如果它是map,那么它就是map;底
层是红黑树,实现map的红黑树的节点数据类型是key+value,而实现set的节点数据类型是value

为什么使用红黑树

因为map和set要求是自动排序的,红黑树能够实现这一功能,而且时间复杂度比较低

unordered_map(hash_map)和map的区别

  1. unordered_map和map类似,都是存储的key-value的值,可以通过key快速索引到value。不同的是
    unordered_map不会根据key的大小进行排序,
  2. 存储时是根据key的hash值判断元素是否相同,即unordered_map内部元素是无序的,而map中的
    元素是按照二叉搜索树存储,进行中序遍历会得到有序遍历。
  3. 所以使用时map的key需要定义operator<。而unordered_map需要定义hash_value函数并且重载
    operator==。但是很多系统内置的数据类型都自带这些,
  4. 那么如果是自定义类型,那么就需要自己重载operator<或者hash_value()了。
  5. 如果需要内部元素自动排序,使用map,不需要排序使用unordered_map
  6. unordered_map的底层实现是hash_table;
  7. hash_map底层使用的是hash_table,而hash_table使用的开链法进行冲突避免,所有hash_map采
    用开链法进行冲突解决。
  8. 什么时候扩容:当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值—即当前
    数组的长度乘以加载因子的值的时候,就要自动扩容啦。
  9. 扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组
    无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。

unordered_map内部是一个hash_table,是由一个大vector,vector的每个元素节点挂一个链表来实现的(就是开链法实现的哈希桶)

C/C++ STL_第2张图片
hashtable中的bucket所维护的list既不是list也不是slist,而是其自己定义的由hashtable_node数据结
构组成的linked-list,而bucket聚合体本身使用vector进行存储。hashtable的迭代器只提供前进操作,
不提供后退操作
在hashtable设计bucket的数量上,其内置了28个质数[53, 97, 193,…,429496729],在创建hashtable
时,会根据存入的元素个数选择大于等于元素个数的质数作为hashtable的容量(vector的长度),其中
每个bucket所维护的linked-list长度也等于hashtable的容量。如果插入hashtable的元素个数超过了
bucket的容量,就要进行重建table操作,即找出下一个质数,创建新的buckets vector,重新计算元素
在新hashtable的位置。

map越界访问

map的下标运算符[]的作用是:将key作为下标去执行查找,并返回相应的值;如果不存在这个key,
就将一个具有该key和value的某人值插入这个map。

map中[]与find的区别?

  1. map的下标运算符[]的作用是:将关键码作为下标去执行查找,并返回对应的值;如果不存在这个关
    键码,就将一个具有该关键码和值类型的默认值的项插入这个map。
  2. map的find函数:用关键码执行查找,找到了返回该位置的迭代器;如果不存在这个关键码,就返回
    尾迭代器。

哈希冲突解决方案

记住前三个:

线性探测
使用hash函数计算出的位置如果已经有元素占用了,则向后依次寻找,找到表尾则回到表头,直到找到
一个空位

开链
每个表格维护一个list,如果hash函数计算出的格子相同,则按顺序存在这个list中

再散列
发生冲突时使用另一种hash函数再计算一个地址,直到不冲突

二次探测
使用hash函数计算出的位置如果已经有元素占用了,按照 1 2 1^2 12 2 2 2^2 22 3 2 3^2 32…的步长依次寻找,如
果步长是随机数序列,则称之为伪随机探测

公共溢出区
一旦hash函数计算的结果相同,就放入公共溢出区

你可能感兴趣的:(C/C++,c语言,c++,数据结构)