CPP(容器)STL:
1、vector的底层实现?vector的扩容过程?push_back()会有什么操作?
vector的底层实现是一个动态数组。vector是一个连续内存空间的动态数组,能够根据下标随机访问,且可以在数组内进行插入删除操作。vector提供了动态扩容机制,因此称为动态数组。
底层实现是它使用三个指针来维护数组:start指向数组的起始位置,finish指向数组中最后一个有效元素的后一个位置,end指向整个连续内存空间的末尾。vector支持随机访问,其元素在内存中连续存储。当vector的元素数量超过当前容量时,它会重新分配更大的内存空间,通常是当前容量的两倍,并将现有元素复制到新的内存空间中。这种动态内存分配策略允许vector的大小动态变化,同时避免了频繁内存分配和释放的开销。
扩容机制:
- 触发条件:当插入元素(如 push_back)导致当前元素数量(size())等于容器容量(capacity())时,触发扩容 。
- 分配新内存:根据编译器的扩容策略(如 GCC 采用 2 倍扩容,MSVC 采用 1.5 倍扩容),申请一块更大的连续内存空间 。
- 数据迁移:将旧内存中的所有元素逐个拷贝或移动到新内存中(若元素支持移动语义,优先移动以减少开销) 。
- 释放旧内存:析构旧内存中的元素并释放原内存块。
- 更新指针:调整 vector 的内部指针(_start、_finish、_end_of_storage),指向新内存的起始、结束和容量边界。
push_back会有什么操作?
- 空间检查:首先检查
vector.size()和vector.capacity(),判断是否需要扩容; - 扩充内存(如果需要):进行扩容和数据复制;
- 构造新元素:
push_back会通过拷贝构造或移动构造来在vector的末尾构造新元素。 - 增加大小: 更新
vector的大小(size),表示其中实际存储的元素数量。
2、push_back和emplace_back的区别?vector删除元素会不会释放空间?
- 构造方式不同
-
push_back: 接受一个已经构造好的对象的副本或移动对象,会调用拷贝构造函数或移动构造函数,将对象复制或移动到vector中 ;emplace_back: 接受构造对象所需的参数, 然后调用对象的构造函数在容器内直接构造对象。对于复杂对象,emplace_back通常比push_back更高效,因为它避免了额外的拷贝或移动操作。
- 可变参数支持
-
emplace_back: 支持可变参数模板,可以传递任意数量和类型的参数,只要这些参数能匹配对象的构造函数。push_back: 只能接受单个参数,即要添加的对象。
vector的内存是只增不减的,删除元素不会减少vector的占用内存,只有析构掉vector数组才会释放其空间。
3、vector和list的区别?
在 C++ 的标准模板库(STL)中,list 是一种双向链表容器。它属于序列容器的一种,允许在其任意位置高效地插入和删除元素,非连续内存空间,但不支持随机访问(即不能通过索引直接访问元素)。
区别:
- vector是数组,占用连续内存,是顺序访问;list是双向链表,随机访问;
- vector是一次性分配好内存,不够时再扩容;list每插入新节点都会进行内存申请;
- vector随机访问性能好、插入删除性能差;list随机访问性能差,插入删除性能好。
4、什么是deque?底层原理是什么?deque和vector的区别?
deque是一个双向开口的连续线性空间,可以在常数级的时间复杂度下对链表进行操作。
deque是以**分段连续空间组合成的,当需要增加新的空间时,只需要配置一段定量连续空间拼接在头部或者尾部即可。因此,deque需要维护整体的连续性**,这也是deque的迭代器比较复杂的原因。deque内部有一个指针指向map,map是一小块连续空间,其中每个元素称为节点(node),这个node是这个指针,指向一个缓冲区,也就是实际存放数据的地方。
deque是分段连续空间,维持其"整体连续"假象的任务,落在了迭代器的operator++ 和 operator-- 两个运算符重载身上,当一个缓冲区到达边界时,则需要从一个缓冲区跳到下一个缓冲区,deque的插入删除等操作也需要结合缓冲区来实现。
deque除了维护一个指向map的指针外,也维护start、finish两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素(的下一位置)。
区别:
- vector是一整块连续的内存来存储数据;deque是分段连续,在扩容时效率更高,但需要更复杂的迭代器来维护整体连续性;
- vector只能在数组末尾实现常数级别的插入和删除,deque在头尾均可以。
5、set和map的区别?底层是怎么实现的?
- set可以理解为只有key值(或者说key和value相等)的有序集合,它是按照key值排序的,并且不允许有重复的key值,key不允许修改,因此其迭代器是
constance iterators;multiset与set的唯一区别就是允许存在重复的key值。 - map则是存放key-value的有序容器,按照key值排序,不允许有重复的key值,key不允许修改但value可以修改;multimap允许存在重复的key值。
由于需要排序,在大多数情况下能够提供快速的插入、删除和查找操作,C++ STL中set、map、multiset、multimapt的实现是通过【红黑树】来实现的。红黑树是一种自平衡的二叉搜索树,它保证了在最坏的情况下,基本操作(插入、删除和查找)的时间复杂度为O(log n)。
关于不允许重复值,是通过红黑树中的insert_unique()的API实现的。
6、unordered_map和unordered_set的区别?底层是怎么实现的?
https://yuanbao.tencent.com/bot/app/share/chat/UZ6IpNz557HF
unordered_set和unordered_map是两种常见的关联容器,与set和map不同,它们不保证元素的顺序,而是通过哈希表(hash table)来实现的。这使得它们在某些情况下可以提供更快的访问速度。
通过哈希表(hash table,使用链地址法解决冲突)实现的,提供了常数时间复杂度(O(1))的插入、删除和查找操作,这使得unordered_set和unordered_map在大多数情况下比set和map更快,特别是在需要频繁访问或修改元素
7、哈希碰撞是什么?怎么解决的?
https://yuanbao.tencent.com/bot/app/share/chat/XasR2YfnIbT6
就是发生在使用哈希表的时候,哈希表保存键值对的时候,由于哈希表的每个位置只能保存一个键值对,这个位置是通过键来计算的,键是通过哈希函数来计算的,如果说计算出来这个下标位置相同,则说明的是产生了哈希冲突,
解决哈希冲突,我们一般情况下使用链地址法,比如说hashmap就是使用的链地址法来解决的, 哈希表的每个桶(Bucket)维护一个链表。当多个键被哈希到同一位置时,它们会被存储在该桶对应的链表中。
除了链地址法以外还有开放地址法,发生哈希冲突的时候可以使用线性探测,二次探测等方法来找到下一个位置。
还有公共溢出区法 就是将 将冲突元素统一存入独立的溢出区。
8、stl容器迭代器失效的情况、原因、解决方法?
迭代器失效分三种情况考虑,也是分三种数据结构考虑,分别为数组型,链表型,树型数据结构。
- 数组型数据结构
-
- 该数据结构的元素是分配在连续的内存中
- insert和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除掉之后的迭代器全部失效,也就是说insert(*iter)(或erase(*iter)),然后在iter++,是没有意义的。
- 解决方法:erase(*iter)的返回值是下一个有效迭代器的值。 iter = cont.erase(iter);
- 链表型数据结构
-
- 对于list型的数据结构,使用了不连续分配的内存
- 插入不会使得任何迭代器失效
- 删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.
- 解决办法两种:使用erase方法删除元素时,用其返回值更新迭代器。在调用erase之前,先使用迭代器的后置递增(如iter++)获取下一个有效的迭代器。
- 树形数据结构
-
- 使用红黑树来存储数据
- 插入不会使得任何迭代器失效
- 删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.
- erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。
9、sort的底层原理:
C++中的std::sort函数底层实现依赖于三种不同的排序算法的组合:introsort(内省排序),它结合了快速排序、堆排序和插入排序,以实现高效的排序。它能够处理不同规模的数据,并且在最坏情况下避免了快速排序的性能退化问题。结合了三种算法的优点:
- 快速排序(Quicksort):在大多数情况下效率非常高,平均时间复杂度为
O(nlogn)。 - 堆排序(Heapsort):在最坏情况下可以保证
O(nlogn)的时间复杂度,避免快速排序在极端情况下退化为O(n^2)。 - 插入排序(Insertion Sort):在处理小数据集时非常高效,时间复杂度为
O(n^2),但由于常数项很小,适合小规模数据。
