字节跳动 广告基础架构 实习一面

1. 自我介绍

2. 介绍一下你常用的 STL 容器

答案:我常用的 STL 容器主要有 vectorlistdequemapunordered_mapsetunordered_setqueuestackpriority_queue。如果从使用频率看,vectorunordered_map 是最常用的。vector 适合连续存储、随机访问和频繁遍历;unordered_map 适合根据 key 做快速查找;map 适合需要有序遍历或者范围查询的场景。

工程里我不会只看理论复杂度,还会看缓存局部性、扩容成本、迭代器失效、内存占用和数据规模。比如元素数量不大时,vector 线性查找有时反而比链表或者树结构更快。

3. 对比一下 vectorlist

答案:vector 底层是连续内存,支持随机访问,遍历性能好,缓存命中率高。缺点是中间插入删除需要移动元素,扩容时可能导致整体搬迁。list 底层是双向链表,每个节点单独分配,插入删除某个已知节点很快,但不支持随机访问,而且缓存局部性差,遍历时会频繁跳地址。

所以不是简单说 list 插入删除一定快。真实场景下,如果数据量不大或者主要是遍历,vector 往往更快;只有在频繁在中间插入删除,并且已经拿到节点位置时,list 才比较有优势。

4. 如果想让 list 像数组一样连续存储,应该怎么做

答案:严格来说,list 的设计目标就是节点式存储,不可能直接让标准库 list 变成连续内存。如果希望有链表的插入删除语义,同时减少内存碎片和缓存 miss,可以考虑自己实现基于内存池的链表,把节点从一大块连续内存里分配出来。这样节点逻辑上还是链表,但物理分配更集中。

另一种思路是不用 list,而是用 vector 存节点,通过下标模拟链表的 next/prev,这样可以获得更好的连续内存访问特性。

代码:

#include <vector>
using namespace std;

struct Node {
    int val;
    int prev;
    int next;
};

class IndexList {
private:
    vector<Node> nodes;
    int head = -1;
    int tail = -1;

public:
    int push_back(int x) {
        int id = nodes.size();
        nodes.push_back({x, tail, -1});
        if (tail != -1) nodes[tail].next = id;
        tail = id;
        if (head == -1) head = id;
        return id;
    }
};

5. 为什么有时候遍历 vector 比遍历 list 更快

答案:主要原因是缓存局部性。vector 的元素连续存储,CPU 读取一个元素时,通常会把相邻的一段内存一起加载到 cache 里,后续遍历可以直接命中缓存。list 每个节点可能分散在堆上的不同位置,遍历时要不断通过指针跳转,容易 cache miss,而且每个节点还要额外存前后指针,内存占用也更高。

所以即使 list 某些操作理论复杂度更低,在现代 CPU 上也不一定更快。很多性能问题不能只看大 O,还要看内存访问模式。

6. vectorlist 删除元素时迭代器会失效吗

答案:会,但规则不一样。vector 删除元素后,被删除位置以及其后的迭代器、引用、指针一般都会失效,因为后面的元素会向前移动。如果触发扩容,那原来的所有迭代器都会失效。list 删除某个节点时,只有指向被删除节点的迭代器失效,其他节点的迭代器通常仍然有效,因为节点之间不需要整体搬移。

遍历删除时要特别注意写法,不能删除后继续使用旧迭代器。

代码:

#include <vector>
using namespace std;

int main() {
    vector<int> v = {1, 2, 3, 4, 5};

    for (auto it = v.begin(); it != v.end(); ) {
        if (*it % 2 == 0) {
            it = v.erase(it);
        } else {
            ++it;
        }
    }
}

7. mapunordered_map 的区别

答案:map 底层通常是红黑树,元素按 key 有序存储,查找、插入、删除复杂度一般是 O(logN)unordered_map 底层是哈希表,平均查找、插入、删除复杂度是 O(1),但不保证顺序,极端哈希冲突时可能退化。

如果需要有序遍历、范围查询、找上下界,用 map 更合适;如果只是根据 key 快速查找,通常用 unordered_map。工程里还要考虑 key 的哈希质量、内存占用、扩容成本和迭代器稳定性。

8. unordered_mapmap 的使用场景有什么差异

答案:如果业务逻辑关心 key 的顺序,比如按文件路径排序输出、查找某个范围内的数据、做前缀范围扫描,那么 map 更合适。如果只是做 ID 到对象、字符串到配置、用户到状态的快速映射,unordered_map 更常见。

不过 unordered_map 不是永远更好。它扩容时会 rehash,可能造成延迟抖动;哈希质量不好时冲突会变多;内存占用也往往比 map 更高。所以高性能场景里,可能还会提前 reserve,控制 max_load_factor,避免运行中频繁扩容。

代码:

#include <unordered_map>
#include <string>
using namespace std;

int main() {
    unordered_map<string, int> cnt;
    cnt.reserve(10000);
    cnt.max_load_factor(0.7);

    cnt["read"]++;
    cnt["write"]++;
}

9. unordered_map 的底层原理是什么

答案:unordered_map 底层一般是哈希表。插入元素时,先对 key 计算 hash 值,然后根据桶数量取模或者做映射,找到对应 bucket。不同 key 可能落到同一个 bucket,这就是哈希冲突。标准库常见实现里,冲突通常用链地址法处理,也就是每个 bucket 后面挂一个链表或节点结构。

当元素数量增长到一定程度,负载因子超过阈值时,哈希表会扩容并 rehash。这个过程需要重新分布已有元素,成本比较高,所以在性能敏感场景下要尽量提前预估容量。

10. 哈希冲突的链地址法怎么理解

答案:链地址法就是每个哈希桶后面挂一个链表或类似节点结构。如果多个 key 经过哈希后落到同一个桶,就把它们都放在这个桶对应的链上。查找时先定位 bucket,再在冲突链里比较 key。

它的优点是实现简单,删除也比较方便;缺点是如果冲突链太长,查询性能会下降,而且链表节点分散,缓存局部性不太好。所以哈希函数质量、桶数量、负载因子控制都很重要。

11. 链地址法如何优化,什么时候需要扩容

答案:链地址法优化主要从几个方向做。第一是选择更好的哈希函数,让 key 尽量均匀分布;第二是控制负载因子,桶里元素太多时及时扩容;第三是减少节点分配开销,比如使用内存池;第四是冲突严重时可以把长链转换成更高效的结构,不过标准库不一定这样做。

一般来说,当元素个数和 bucket 数量的比例超过阈值,就需要扩容。这个比例就是负载因子。扩容后会重新计算每个元素所在桶位置,也就是 rehash。

12. 除了链地址法,哈希冲突还有哪些解决方式

答案:还可以用开放寻址法,比如线性探测、二次探测、双重哈希。线性探测是在冲突后继续找下一个空位,实现简单,缓存局部性好,但容易形成聚集。二次探测能缓解一部分聚集问题,但探测序列设计更复杂。双重哈希则使用第二个哈希函数决定探测步长,分布更好一些。

开放寻址法删除元素时比较麻烦,不能简单把位置清空,否则会影响后续查找,通常要用删除标记。它前期性能可能很好,但负载因子太高后性能会明显抖动,所以通常要控制装载率。

13. 你用过其他库里的哈希表吗,和标准库有什么区别

答案:用过或者了解过一些高性能哈希表,比如 robin hood hashing、flat hash map 这类思想。它们通常更关注缓存局部性和探测距离控制。标准库 unordered_map 接口稳定、通用性好,但节点式结构比较多,内存局部性和常数性能不一定最优。

一些高性能哈希表会把元素放得更紧凑,减少指针跳转,查找时更容易命中 cache。代价是迭代器稳定性、删除实现、扩容策略可能和标准库不同。工程里如果性能瓶颈明确在哈希表,并且能接受额外依赖,才会考虑替换。

14. 智能指针有哪些,分别解决什么问题

答案:常见智能指针有 unique_ptrshared_ptrweak_ptrunique_ptr 表示独占所有权,不能拷贝,只能移动,适合资源唯一归属的场景。shared_ptr 表示共享所有权,通过引用计数管理对象生命周期。weak_ptr 不增加引用计数,主要用于观察 shared_ptr 管理的对象,解决循环引用和异步回调里的生命周期判断问题。

使用智能指针的核心不是“把裸指针都换掉”,而是先想清楚对象的所有权。如果所有权不清楚,智能指针也会被用乱。

15. shared_ptr 为什么能实现共享计数

答案:shared_ptr 内部除了保存对象指针,还会关联一个控制块。控制块里通常包含强引用计数、弱引用计数、删除器、分配器等信息。多个 shared_ptr共享同一个控制块,每拷贝一次强引用计数加一,每析构一个强引用计数减一。当强引用计数变成 0 时,管理的对象会被销毁;当强引用

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++ 常考面试题总结 文章被收录于专栏

本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务