12.STL容器基础

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释放多余容量,减少内存使用。

    cpp 复制代码
    std::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'; // 可能变为 0

1.3 Vector如何避免push_back的扩容开销?

  • 通过reserve方法预先分配足够大小的容量,避免使用push_back时多次扩容的开销。

    cpp 复制代码
    std::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的末尾。

    cpp 复制代码
    struct 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/copy

1.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:直接构造元素,避免不必要的拷贝和移动操作。

    cpp 复制代码
    std::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.迭代器与指针的区别?

  • 迭代器:提供统一的接口访问不同类型的容器。抽象到容器元素的访问器。
  • 指针:指向一个变量的内存地址。适用于数组等简单结构。

注意:指针是迭代器,迭代器不一定是指针。

cpp 复制代码
int arr[] = {3,1,2};
std::sort(arr, arr + 3); // 指针当作迭代器

17.deque的实现?

  • 内部结构:由多个固定大小的数组和一个中控结构组成,中控结构含有指向这些数组的指针。**支持随机访问,**通过中控数组 + 缓冲区内偏移,可在 O(1) 时间计算任意位置元素。
  • 应用场景:适用于需要两端快速操作的系统,如消息队列。
  • 滑动窗口:适用于需要频繁添加、删除的算法。
相关推荐
龚礼鹏1 天前
Android应用程序 c/c++ 崩溃排查流程二——AddressSanitizer工具使用
android·c语言·c++
qq_401700411 天前
QT C++ 好看的连击动画组件
开发语言·c++·qt
额呃呃1 天前
STL内存分配器
开发语言·c++
七点半7701 天前
c++基本内容
开发语言·c++·算法
嵌入式进阶行者1 天前
【算法】基于滑动窗口的区间问题求解算法与实例:华为OD机考双机位A卷 - 最长的顺子
开发语言·c++·算法
嵌入式进阶行者1 天前
【算法】用三种解法解决字符串替换问题的实例:华为OD机考双机位A卷 - 密码解密
c++·算法·华为od
啊董dong1 天前
noi-2026年1月07号作业
数据结构·c++·算法·noi
m0_635647481 天前
Qt使用第三方组件库新手教程(一)
开发语言·c++·qt
星火开发设计1 天前
二叉树详解及C++实现
java·数据结构·c++·学习·二叉树·知识·期末考试