C++面经(三):stl,vector,list,deque,map,multimap,set,multiset,红黑树,unordered_map,unordeded_set

1. STL的介绍:

STL = 标准模板库,提高程序的 开发效率 和 复用性。

STL包含的 6大组件:
容器、迭代器、算法、仿函数、适配器、空间配置器。

各组件的作用:
容器: 用于容纳一组相同类型的元素
迭代器:
算法:
仿函数:
适配器:
空间配置器: 负责空间配置和管理

2. 空间配置器:

负责 对象构造前的空间配置 和 对象析构后的空间释放。

空间配置器的一个重要作用是解决内存的申请和释放时引入的内存碎片问题,SGI使用的方法是 “双层级配置器”:
(1)第一级直接调用 malloc()、deallocate()、free()等系统调用分配内存;
(2)第二级根据具体情况使用不同的策略:当配置区块大于128bytes时,调用第一级配置器,使用malloc等方式直接分配;
当配置区块小于128bytes时,采用内存池的整理方式:配置维护16个自由链表,负责16种小型区块的配置能力。

3. 各种容器的特点和适用情况:

vector: 底层存储是一个数组,可变大小的数组,支持随机访问(O(1)),在尾部之外的位置插入和删除操作时间复杂度是 O(N);

deque: 双端队列,支持随机访问(O(1)),在头部和尾部之外的位置插入和删除的时间复杂度是O(N);

list: 双向链表,不支持随机访问,只支持顺序访问,在任意位置的插入和删除速度很快;

forward_list: 单向链表;

array: 固定大小的数组(vector是可变大小的数组);

string: 与vector类似,可以理解为特殊的vector,专门用来存储字符,支持随机访问,在尾部之外的位置插入的时间复杂度是O(N);

4. 各种容器的底层机制和常见面试题:

4.1 vector:

4.1.1 vector的底层原理:

vector的底层实现是三个指针:

struct _Vector_impl : public _Tp_alloc_type {
pointer _M_start;
pointer _M_finish;
pointer _M_end_of_storage;
};

start 和 finish 之间是已经被使用的空间范围(即 vector.size() 的大小),
start 和 end_of_stroage 之间是vector底层数组的整个空间(即 vector.capacity() 的大小)。

所以 vector 这个类本身并不存储数据的内容,通过指针 start 指向真正的存储元素的数组的地址。

vector的 内存增长机制:
当现有的内存空间不够装下数据时(push_back(val)触发),vector会自动申请另一块更大的内存(1.5倍 或者 2倍),把原有的数据 拷贝 到新的内存空间,然后释放旧有内存空间。

当 clear清空 vector中的元素时(vector.clear()),其存储空间不释放,仅仅是情况内存上的数据。

例如:

vector v;
for(int i = 0; i < 1000; i++) {
v.push_back(i);

}
cout << v.size() << " " << v.capacity() << endl;

v.clear();
cout << v.size() << " " << v.capacity() << endl;


1000 1024
0 1024

循环插入1000个元素到vector中,vector的size为1000,capacity为1024;
调用clear清空vector中的元素后,vector的size变为0,capacity仍为1024。

4.1.2 vector的reserve和resize的区别:

reserve 的作用是在vector初始化时,根据预估的所需要的内存大小 提前申请内存,目的是避免程序运行中重复的 申请、释放 内存空间,以提高程序的效率;
reserver修改的是vector的capacity的值,只表示一种尝试,当vector的现有实际size大于reserve(n)的n值时,vector不做任何修改。

resize 改变vector的实际大小,可能导致vector中的多余元素被删除,接受两个参数的resize不仅改变vector大小,也改变元素的默认值。

4.1.3 vector的size和capacity的区别:

size = finish - start; 表示的是vector中实际元素的 个数;
capacity = end_of_storage - start; 表示的是vector当前分配的内存可以容纳的最多元素的 个数。

注意 size 和 capacity 表示的都是元素个数,不是vector内存的字节数。

4.1.4 vector的元素类型不能是引用:

vector的底层存储要求是连续的对象排列,引用不是对象,没有实际地址,因此vector的元素类型不能是引用。

4.1.5 释放vector的内存的方式:

clear、swap、shrink_to_fit。

clear:
v.clear(); //清空vector的元素,占用的内存不会释放,即size变0,capacity不变

swap:
vector v2; //先定义个空的vector
v2.swap(v);

swap 函数的效果是将 v1和v2 “整体交换”, 不仅交换两个vector的元素内容,并且还会交换vector的size、capacity,相当于是两个vector中的是 三个指针 都进行调换。

shrink_to_fit:
//原型:
v.shrink_to_fit(); //shrink_to_fit 将vector的capacity修改为与size一致。作用是释放多余的内存空间
//注意 shrink_to_fit() 函数不接受参数

//使用shrink_to_fit 释放vector内存的写法:
v.clear();
v.shrink_to_fit(); //先调用clear将vector的size变为0,再使用shrink_to_fit将capacity也变为0

4.2 list:

4.2.1 list的底层原理:

list的底层是一个 “双向链表”。
list不支持随机访问;插入或删除一个元素时,就对应配置或释放一个结点的空间。

4.3 deque:

4.3.1 deque的底层原理:

deque的底层是一个 “双端队列”。
deque在头尾两端插入和删除的时间复杂度都是 O(1)。

4.3.2 什么情况使用vector,什么情况使用list,什么情况使用deque:

随机访问频繁,元素数量变化不大(不会频繁的插入、删除)的场景,优先选用vector;
相反的场景,使用list,即元素频繁的插入删除,但不会经常进行随机反问。

除非必要,应尽可能的选择使用vector而非deque,因为 deque的迭代器比vector的迭代器要复杂的多。

除非真的需要频繁的在容器的首尾两端进行频繁的插入和删除元素的操作。

4.4 priority_queue:

4.4.1 priority_queue的底层原理:

priority_queue(优先队列)的底层使用 堆 来实现的。
在优先队列中,队首元素 一定是当前队列中优先级最高的那一个。

4.5 map、set、multimap、multiset:

map、set、multimap、multiset 的底层实现都是 红黑树。
(epoll模型的底层数据结构也是红黑树,Linux系统中CFS进程调度算法也是红黑树)

请添加图片描述

红黑树的特性:
(1)每个节点是 红色 或者 黑色;
(2)根节点 是 黑色;
(3)所有叶子节点 都是 黑色;
(4)如果有一个节点是红色,则它的两个子结点都是 黑色;(因此红色节点不能相邻,黑色的节点可以相邻)
(5)对每个节点,从该节点到其子孙节点(即叶子节点)的所有途径上,包含相同数据的黑色节点。(所以红黑树平衡的是黑色节点的高度,红色节点主要是用来做区分的)

红黑树平衡的是 黑色节点的高度,红色节点主要是用来做区分的。

以红色树实现的map的 查找、插入、删除 的时间复杂度都是 O(logN)。(平均、最坏、最好都是)

在map中判断一个key是否存在,可以使用find或count:

map.find(key) != map.end();
map.count(key) != 0;
1
2
map和set会对插入的元素自动排序,set中元素不允许重复,multiset中的元素可以重复。

4.6 unordered_map、unordered_set:

4.6.1 unordered_map、unordered_set的底层原理:

unordered_map 的底层数据结构是一个 哈希表。
哈希表的特点是 查找、插入、删除 的时间复杂度是 O(1),缺点是耗费一定的内存(哈希表数组需要预留空间)。

哈希表的实现方法是 使用一个下标范围较大的数组来存储元素,使用 哈希函数(或称“散列函数”)对每个元素的key进行计算,将经过哈希计算得到的值作为数组下标。
当不同的key经过哈希函数计算后得出的结果发生冲突时,使用链表解决。

请添加图片描述

map的底层数据结构是 红黑树,unordered_map的底层数据结构是 哈希表(数组+链表)。

五、迭代器的底层级制和失效的问题:

5.1 迭代器底层原理:

STL的 算法 和 容器 是分离的,两者通过迭代器连接。
算法的实现并不知道自己被传进来的参数是什么类型的容器。

萃取器(Traits) 相当于在接口和实现之间加一层封装,来隐藏一些细节并协助调用合适的方法,这需要一些技巧(例如,偏特化)。

例如STL中的 distance() 函数可用于计算容器中两个迭代器之间的距离,对于vector容器来说,由于内存是连续分配的,因此指针直接相减即可获得二者举例,而如果传入 distance函数的容器类型是list,则distance需要遍历list链表中的元素,才能求得两个迭代器之间的距离。

5.2 迭代器的种类:(5种)

输入迭代器: 只读迭代器,在每个被遍历的位置上只能读取一次,例如find函数的参数就是输入迭代器(类似于 cbegin()、cend() 只读迭代器);
输出迭代器: 只写迭代器,在每个被遍历的位置上只能被写一次;
前向迭代器: 具输入和输出迭代器的能力,但是它可以对同一个位置重复进行读和写。但它 **不支持operator–-*,所以只能向前移动;
双向迭代器: 可以前、后移动;
随机访问迭代器: 有双向迭代器的所有功能,并且提供 “迭代器算数”,支持 iter+n、iter-n、iter1-iter2 等操作。

5.3 迭代器失效的问题:

所谓的“迭代器失效”,指的是 在对容器进行某些操作,导致容器上的旧有迭代器在这些操作后失去了本来的效果。

这些操作一般包括 对容器进行 insert插入、erase删除 等操作,
迭代器失效的本质与其所作用的容器的 底层内存实现方式 有关。

5.3.1 对于vector、string、deque这些 连续存储 的容器:

  1. 插入操作:

当对容器进行 insert插入 操作时,可能 会发生现有内存空间不足,需要重新分配内存的情况,此时会导致原容器上的所有迭代器全部失效;

如果在insert插入后,内存未重新分配,则 插入点之前的 迭代器仍然有效,插入点之后的迭代器失效;

对于deque,如果插入点在front和back,则所有迭代器失效,指针和引用仍有效;
如果插入点在非front和back的其他位置,则所有迭代器、指针、引用 都失效。

  1. 删除操作:

对于vector和string,删除点之前的引用、指针、迭代器仍有效,删除点之后的失效,尾迭代器(off-the-end)总是失效的;

5.3.2 对于list、forward_list、map这些 非连续存储 的容器:

  1. 插入操作:

由于容器中的元素的结点都是离散存储的,每次insert插入新节点时都是要构造一个元素并挂到链表中,所以 不存在内存空间不足需要重新分配内存的情况,因此 插入操作不会引起任何迭代器的失效。

  1. 删除操作:

与插入操作的原理相同,map、list这类容器的各个节点都是离散存储不连续,所以删除操作除了会导致 被删除的元素上的迭代器失效 外,其余迭代器都仍然有效。

5.3.3 迭代器失效的处理方式:

map m; //

void proxy_timer_callback() {

for(map::iterator it = m.begin(); m != end(); ) {
	map::iterator old_it = it;
	it++;
	
	if(old_it->second > cur_time) {
		m.erase(old_it);
	}
}

}

因为erase()会将迭代器删除,所以如果将 it++ 的操作放在for循环条件里面的话,由于it迭代器已经被删除了,再对其进行 ++ 操作会导致程序 Segment Fault 段错误。
正确的处理方式是在for循环体内保存it当前指向,然后先对it++,再删除 old_it指向的元素节点。

六、STL容器的线程安全性:

同一容器的多线程写操作是 线程不安全的,需要进行加锁。

参考内容:

https://www.cnblogs.com/mangoyuan/p/6446046.html
https://blog.csdn.net/daaikuaichuan/article/details/80717222

你可能感兴趣的:(C++,c++,链表,数据结构)