1 Vector的底层实现?
vector本质上就是一个能够自动扩容的动态数组。他的所有元素存储在一块连续的内存空间中。对于随机访问的效率高,适用于对于vector尾部插入、删除的场景。对于头部和中间操作效率低。
cpp#include <vector> #include <iostream> int main() { std::vector<int> v = {1, 2, 3}; v.push_back(4); // 尾部插入 O(1) 均摊 v.pop_back(); // 尾部删除 O(1) v.insert(v.begin(), 0); // 头部插入 O(n) }1.1 Vector的扩容机制?
vector在容量不足时,继续向其中放入元素会自动进行扩容。会重新创建一个1.5或2倍原数组大小的新数组。并将原数组的所有元素拷贝/转移到新数组中。释放掉原数组的内存。
1.5倍和2倍主要取决于编译器(如:GCC一般是1.5或2倍。MSVC一般是1.5倍),为了考虑到扩容的次数不能过多和不能扩容后浪费过多内存,1.5或2倍为一个均衡的倍数。
cpp#include <vector> #include <iostream> int main() { std::vector<int> v; for (int i = 0; i < 10; ++i) { std::cout << "size=" << v.size() << ", cap=" << v.capacity() << '\n'; v.push_back(i); } } // GCC 输出:cap: 0→1→2→4→8→16...1.2 Vecto如何释放内存?
vector通过clear清空内容,但不释放容量。通过Shrink_to_fit释放多余容量,减少内存使用。
cppstd::vector<int> v(1000); v.clear(); std::cout << "after clear: cap=" << v.capacity() << '\n'; // 仍为 1000 v.shrink_to_fit(); std::cout << "after shrink: cap=" << v.capacity() << '\n'; // 可能变为 01.3 Vector如何避免push_back的扩容开销?
通过reserve方法预先分配足够大小的容量,避免使用push_back时多次扩容的开销。
cppstd::vector<int> v; v.reserve(1000); // 预分配 for (int i = 0; i < 1000; ++i) v.push_back(i); // 无扩容1.4 Vector的resize和reserve区别?
- resize:用于改变vector的大小(size),如果新大小大于原大小,会初始化新元素,改变容量。
- reserve:只改变vector的容量(capacity),不改变大小。
1.5 Vector的emplace_back和push_back的区别?
emplace_back:使用原地构造元素,避免不必要的拷贝和移动操作。
push_back:复制或移动对象到vector的末尾。
cppstruct A { A(int x) { std::cout << "ctor\n"; } A(const A&) { std::cout << "copy\n"; } }; std::vector<A> v; v.emplace_back(1); // 直接构造,无 copy v.push_back(A(2)); // 先构造临时对象,再 move/copy1.6 vector::at()和vector::operator[]的区别是什么?
- at():提供边界检查,如果访问越界,会抛出std::out_of_range异常。
- operator[]:不提供边界检查,如果越界访问,会出现未定义的行为。
2.STL容器是线程安全的吗?怎样在多线程的环境下使用容器?
- STL线程安全问题:大多数STL容器本身不是线程安全的。多个线程同时访问同一个STL容器时,如果没有适当的同步机制,就会出现数据竞争和未定义行为。
- 在多线程环境下:多个线程同时访问同一个STL容器是安全的。但如果多个线程同时对容器进行写入,或有一个进行写入一个进行读取,就会不安全。
3.C++的map和unordered_map的区别?
|-------------|----------|---------------|
| 特性 | map | unordered_map |
| 底层实现 | 红黑树 | 哈希表 |
| 元素有序性 | 有序 | 无顺序 |
| 迭代器 | 插入、删除后有效 | 插入、删除后失效 |
| 插入、查找、删除复杂度 | O(logn) | O(1) |4.map的插入方式?
insert:插入元素返回pair<iterator,bool> ,表示插入结果。
operator[]:使用[]插入、删除元素。
emplace:直接构造元素,避免不必要的拷贝和移动操作。
cppstd::map<std::string, int> m; m["new"] = 10; // 插入 {"new", 10} m.insert({"key", 20}); // 不覆盖已有 m.emplace("key2", 30); // 原地构造 pair
5.红黑树和哈希表的复杂度分析?
- 红黑树:插入、删除、查找都是O(logn)。
- 哈希表:插入、删除、查找都是O(1),最坏O(n)。
(注意:如果哈希冲突严重,出现最坏O(n)的情况)
6.哈希表的底层实现?
- 数据结构:由一个数组和哈希函数组成,数组用于存储数据,哈希函数用于将键值映射到数组的索引位置。
- 哈希函数:一个映射函数,将输入键转换为数组的索引。
- 哈希冲突:当多个键被映射到同一个索引位置,会出现冲突问题。
6.1哈希冲突如何解决?
- 拉链法:每个数组索引位置维护一个链表,将经过哈希函数的相同索引加入链中。缺点是,链表的长度不可控,容易查找时退化成O(n)的复杂度。
- 开放地址法:在数组中找到下一个空闲位置,用来存储哈希冲突的元素。删除元素操作复杂,需要将删除的元素位置标记为已删除,不能简单直接标记为空。因为如果直接标记为空,下次遇到这个空的位置就会认为探测链结束,后面位置的元素无法查询到。
6.2哈希的负载因子和阈值?
- 负载因子:(存储在哈希表中的元素数量)/(哈希表的总容量)
- 阈值:控制负载因子何时扩容的的负载因子最大值。当负载因子大于阈值时需要进行扩容。
- 扩容:创建一个新的哈希表,并且重新计算每个元素的新索引值,从新加入新的哈希表中。
6.3哈希表的应用?
- 快速查找:哈希表提供平均O(1)的查找效率,提高了查找数据速度。
- 数据去重:使用哈希集合(如:unordered_set)来判断元素是否已经存在,从而实现去重。
- 数据索引:哈希表能够快速定位数据,常用于构建数据库和缓存系统的索引。
7.红黑树的好处?
- 自平衡性:红黑树是一种自平衡的二叉搜索树。不会退化成链表,由于红黑树的性质。
- 动态操作效率高:红黑树的搜索、插入和删除的复杂度都为O(logn),复杂度由红黑树的树高度决定。
- 有序性:红黑树能够保证元素的有序性。
7.1红黑树的性质:
- 节点分为红色节点和黑色节点。
- 根节点、叶子节点都是黑色。
- 红色节点的子节点必须是黑色。
- 从任一节点到其所有叶子节点的路径上,黑色节点数目相同。
8.Linux线程调度底层是否与红黑树有关?
红黑树在Linux中主要是完全公平调度器(CFS)中的应用。用于高效查找和管理处于就绪队列的进程。
- 1.虚拟运行时间(vruntime)
每个进程都有一个虚拟运行时间,表示该进程在CPU上的运行时间。
vruntime是根据标准运行时间调整的,目的是使不同优先级的进程得到公平的CPU时间。
- 2.红黑树存储就绪队列
就绪队列的所有进程按照他们的vruntime存储在红黑树中。
- 3.调度器需要通过红黑树快速找到vruntime最小的进程,选择这个进程为下一个要运行的进程。
9.迭代器的++ite、ite++的区别?
- ++ite:前置递增,返回++后的新值,效率高。
- ite++:后置自增,返回++前的旧值,旧值需要临时对象进行保存,效率低。
10.Vector和List的区别?
|---------|-------------|-----------------|
| 特性 | vector | List |
| 数据结构 | 动态数组 | 双向链表 |
| 内存布局 | 分布在一个连续的内存中 | 分布在离散的内存中 |
| 访问效率 | 随机访问效率高 | 随机访问效率低,需遍历 |
| 插入、删除效率 | 对于尾部操作效率高 | 对于中间高效,头部和尾部较高效 |
11.List和deque的区别?
|---------|-----------------|--------------|
| 特性 | List | deque |
| 数据结构 | 双向链表 | 双端队列 |
| 内存布局 | 存储在分散的内存中 | 存储分段连续的内存块 |
| 访问效率 | 随机访问效率低,需遍历 | 随机访问效率高 |
| 插入、删除效率 | 对于中间高效,头部和尾部较高效 | 头部和尾部高效,中间低效 |
12.迭代器失效的情况?
- vector:在插入、删除、扩容后迭代器失效。
- list:删除元素后指向被删除元素的时候失效。
- map:删除元素后指向被删除元素的时候失效。
- deque:插入、删除、扩容后迭代器失效。(注意:任何插入/删除后都可能失效(因中控数组可能重排))
13.priority_queue的底层实现原理及应用场景?
- 底层实现:底层是基于堆算法实现的,使用vector作为底层容器,通过堆算法维护vector的堆结构。
- 使用场景: 适用于快速访问最大或最小元素的场景,如任务调度、事件驱动的模拟等。
14.什么时候使用stack?他的底层容器是什么?
- 用途:stack是后进先出的数据结构,适用于这种访问顺序的场景,如函数调用栈、撤销操作等。
- 底层容器:默认使用deque作为底层容器,也可以使用vector或list。
15.STL中排序算法如何工作?
std::sort:通常使用快速排序、堆排序和插入排序的混合算法,适用于随机访问迭代器,如:vector和deque。
std::stable_sort:使用归并排序,保持相同元素的相对顺序,适用于所有类型迭代器。
16.迭代器与指针的区别?
- 迭代器:提供统一的接口访问不同类型的容器。抽象到容器元素的访问器。
- 指针:指向一个变量的内存地址。适用于数组等简单结构。
注意:指针是迭代器,迭代器不一定是指针。
cppint arr[] = {3,1,2}; std::sort(arr, arr + 3); // 指针当作迭代器
17.deque的实现?
- 内部结构:由多个固定大小的数组和一个中控结构组成,中控结构含有指向这些数组的指针。**支持随机访问,**通过中控数组 + 缓冲区内偏移,可在 O(1) 时间计算任意位置元素。
- 应用场景:适用于需要两端快速操作的系统,如消息队列。
- 滑动窗口:适用于需要频繁添加、删除的算法。