C/C++面试八股题(十)

目录:#面试#

1.STL中常用的容器有哪些?

2.请你说说STL容器有哪些并且说一下时间复杂度?

3.请说说vector和list有什么区别?

4.vector和deque的区别?

5.请你说明一下STL底层数据结构实现?

6.请你说说map和set区别差异?

7.请你说说STL迭代器怎样删除元素?

内容:

1.STL中常用的容器有哪些?

顺序容器

1. vector

  • 动态数组,支持随机访问。
  • 元素存储在连续的内存空间中,支持高效的索引操作(O(1) 时间复杂度)。
  • 在尾端增删元素。

2. deque

  • 双端队列,支持在两端高效地插入和删除。
  • 支持随机访问。
  • 相较于 vector,不需要频繁搬移整个数组,但随机访问性能稍差。
  • 需要在头尾进行频繁插入或删除操作。

3.list

  • 双向链表,支持高效的插入和删除。
  • 不支持随机访问。
  • 插入和删除操作仅需要调整指针,时间复杂度为 O(1)。
  • 频繁插入和删除操作,但对随机访问要求不高。

4 .forward_list

  • 单向链表,支持单向遍历和高效的插入、删除操作。
  • 相较于 list,更加轻量级。
  • 使用单向链表实现,元素通过单指针链接,内存不连续。
  • 需要简单链表操作,内存开销较小的场景。

5. string

  • 专门用于操作字符串的容器,功能类似 vector<char>。
  • 基于动态数组实现,支持动态扩容。
  • 提供了大量字符串处理函数,例如拼接、查找、替换等。
  • 字符串操作。

关联容器

1. map

  • 存储键值对,键是唯一的,支持高效的查找、插入、删除操作(O(log n))。
  • 数据按照键值排序,支持有序遍历。
  • 需要按键有序存储键值对,并支持快速查找。

2. multimap

  • 和 map 类似,但允许键重复。
  • 和 map 一样基于红黑树实现,区别在于支持存储重复键。
  • 键值对存储,但允许键重复。

3. set

  • 存储唯一的键,自动按顺序排列。
  • 支持高效的插入、查找、删除操作(O(log n))。
  • 基于红黑树实现。
  • 元素作为键值存储,不能重复。
  • 需要存储唯一值,并支持快速查找和有序遍历。

4. multiset

  • 和 set 类似,但允许重复值。
  • 和 set 一样基于红黑树实现,区别在于允许重复值。
  • 数据存储需支持重复值,但仍需要有序存储。

5. unordered_map

  • 存储键值对,键是唯一的,查找和插入操作的平均时间复杂度为 O(1)。
  • 基于哈希表实现。
  • 数据无序存储。
  • 快速查找键值对,但对顺序无要求。

6. unordered_multimap

  • 和 unordered_map 类似,但允许键重复。
  • 基于哈希表实现,支持重复键。
  • 快速查找键值对,允许键重复。

7. unordered_set

  • 存储唯一值,无序排列。
  • 基于哈希表实现。
  • 无序存储唯一值,并支持快速查找。

8. unordered_multiset

  • 和 unordered_set 类似,但允许重复值。
  • 基于哈希表实现。
  • 无序存储值,并允许重复。

总结

vector

动态数组,随机访问高效

动态数组

数据量变化大,需要高效随机访问

deque

双端队列,支持头尾插入删除

分段数组

频繁的头尾操作

list

双向链表,插入删除高效

双向链表

频繁插入删除,随机访问不重要

array

静态数组,大小固定

静态数组

固定大小数组

map

有序键值对存储,键唯一

红黑树

需要有序键值对存储

unordered_map

无序键值对存储,键唯一

哈希表

快速键值对查找,但无需顺序

set

存储唯一值,有序排列

红黑树

存储唯一值,并需要排序

unordered_set

存储唯一值,无序排列

哈希表

存储唯一值,但无需排序

2.请你说说STL容器有哪些并且说一下时间复杂度?

顺序容器

顺序容器中的元素按插入顺序存储,主要用于序列存储。

vector

动态数组,支持随机访问,内存连续,支持动态扩容

O(1)

(尾部),O(n)(其他位置)

O(n)

O(1)

deque

双端队列,支持头尾快速插入删除

O(1)

(头/尾)

O(1)

(头/尾)

O(1)

(随机访问)

list

双向链表,插入删除高效,不支持随机访问

O(1)

(任意位置)

O(1)

(任意位置)

O(n)

(线性访问)

forward_list

单向链表,仅支持单向遍历和插入删除

O(1)

O(1)

O(n)

(线性访问)

array

定长数组,大小在编译时确定,支持随机访问

O(1)

O(1)

O(1)

string

动态字符数组,功能类似于 vector<char>,专为字符串操作优化

O(1)

(尾部),O(n)(其他位置)

O(n)

O(1)

关联容器

关联容器通常按键值对存储,底层实现基于红黑树,支持有序存储和快速查找。

map

有序字典,键唯一,支持按键值快速查找

O(log n)

O(log n)

O(log n)

multimap

有序字典,键允许重复

O(log n)

O(log n)

O(log n)

set

有序集合,存储唯一元素

O(log n)

O(log n)

O(log n)

multiset

有序集合,允许重复元素

O(log n)

O(log n)

O(log n)

无序关联容器

无序关联容器底层基于哈希表实现,支持快速的查找、插入和删除操作,但不保证元素的顺序。

unordered_map

无序字典,键唯一

O(1)

(均摊),O(n)(最坏)

O(1)

(均摊),O(n)(最坏)

O(1)

(均摊),O(n)(最坏)

unordered_multimap

无序字典,键允许重复

O(1)

(均摊),O(n)(最坏)

O(1)

(均摊),O(n)(最坏)

O(1)

(均摊),O(n)(最坏)

unordered_set

无序集合,存储唯一元素

O(1)

(均摊),O(n)(最坏)

O(1)

(均摊),O(n)(最坏)

O(1)

(均摊),O(n)(最坏)

unordered_multiset

无序集合,允许重复元素

O(1)

(均摊),O(n)(最坏)

O(1)

(均摊),O(n)(最坏)

O(1)

(均摊),O(n)(最坏)

容器适配器

stack

通常基于 deque

vector

O(1)

O(1)

后进先出(LIFO),只操作栈顶。

queue

通常基于 deque

O(1)

O(1)

先进先出(FIFO),只操作队首和队尾。

priority_queue

通常基于 vector

O(log n)

O(log n)

优先队列,堆实现,最大/最小元素优先出队。

3.请说说vector和list有什么区别?

相同点

  • 两者都属于顺序容器,用于按插入顺序存储元素。
  • 支持动态大小调整(虽然实现方式不同)。
  • 都能通过 STL 提供的标准迭代器(iterator)进行元素的访问和操作。
  • 两者提供类似的容器操作接口,如插入(insert)、删除(erase)、遍历(begin/end)等。
  • 支持 STL 算法(如 std::sort,但需要注意 list 需要先转为其他容器才支持随机访问相关算法)。
  • 容器中的元素可以是基本数据类型,也可以是用户自定义的复杂类型。

不同点

底层实现

动态数组,存储在连续的内存块中。

双向链表,每个节点存储元素和前后指针。

内存分配方式

单块连续内存,扩容时可能导致整块重新分配。

分散的非连续内存,每个节点单独分配内存空间。

随机访问

支持随机访问(O(1)),例如通过索引

v[i]

不支持随机访问,只能通过迭代器线性遍历(O(n))。

插入/删除复杂度

尾部操作为 O(1),中间插入/删除需移动元素,复杂度 O(n)

任意位置操作为 O(1)(只需调整指针)。

遍历性能

由于数据连续,访问时缓存性能更好,速度更快。

数据分散,遍历性能较慢,但在长链表中删除或插入性能更高。

动态扩展

扩容时容量按 倍数增长,可能会导致内存重新分配及数据拷贝。

每次添加一个节点,仅分配所需的节点内存,无需整体重新分配。

排序

使用 STL 中的 std::sort,复杂度为

O(n log n)

使用成员函数 list::sort,复杂度为

O(n log n),不需要额外空间。

内存利用率

连续内存分配,开销小,内存利用率高。

每个节点需额外存储两个指针,内存开销更大。

使用场景

vector 的场景

  • 需要随机访问,如果需要频繁按索引访问元素,vector 是更好的选择。
  • 数据量变化不频繁,数据量相对固定或变化不频繁,避免频繁的动态扩容带来的性能开销。
  • 需要排序或二分查找,vector 支持快速排序和二分查找等操作,效率比 list 高。
  • 空间效率高,内存连续分配,不需要额外的指针存储。

list 的场景

  • 需要频繁的插入/删除操作,特别是在容器中间进行大量插入或删除操作时,list 的性能比 vector 好。
  • 数据较大或变化频繁,数据量较大时,可以避免因 vector 扩容带来的大量内存分配和拷贝。
  • 不需要随机访问,如果只需顺序访问,链表的性能足够且更灵活。

例子:

vector

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v = {1, 2, 3};
    v.push_back(4); // 在尾部插入
    v[2] = 5;       // 通过索引修改
    for (int i : v) {
        cout << i << " "; // 输出:1 2 5 4
    }
    return 0;
}

list

#include <iostream>
#include <list>
using namespace std;

int main() {
    list<int> l = {1, 2, 3};
    auto it = l.begin();
    advance(it, 1); // 移动迭代器到第二个元素
    l.insert(it, 5); // 在第二个位置插入 5
    for (int i : l) {
        cout << i << " "; // 输出:1 5 2 3
    }
    return 0;
}

4.vector和deque的区别?

区别

vector

  • 底层使用动态连续数组实现。
  • 所有元素存储在一块连续的内存空间中。
  • 扩容时需要重新分配更大的连续内存空间,并将原数据拷贝到新内存。
  • 所有元素在一块连续的内存中,内存使用效率较高。
  • 扩容时会分配更大的连续内存块,并将数据拷贝到新内存中。

deque

  • 底层使用分段数组实现。
  • 内存不是连续的,由多个固定大小的内存块(缓冲区)组成,通过中间的指针数组(映射表)管理。
  • 不需要重新分配整个内存,添加元素时,只需分配新的内存块。
  • 内存是分段分配的,不需要分配连续内存,适合频繁的插入和删除操作。
  • 因为有额外的指针数组(映射表)管理多个内存块,内存开销略高。
  • 动态扩容时,只需分配新块并更新指针数组,无需整体移动元素。

随机访问

O(1),因为内存连续,支持高效随机访问。

O(1),通过映射表跳转到对应块,再访问具体元素。

尾部插入/删除

O(1)(大部分情况下),但当容量不足时需扩容,导致 O(n)

O(1),直接在尾部块插入元素,无需整体移动。

头部插入/删除

O(n)

,因为所有元素需要整体向后/向前移动。

O(1),直接在头部块插入元素。

中间插入/删除

O(n),中间位置的元素需要整体移动。

O(n),中间位置的元素也需要整体移动,但性能稍逊于 vector

随机访问

支持高效随机访问,推荐在需要按索引访问的场景使用。

支持随机访问,但性能略逊于 vector

动态扩容

每次扩容需要重新分配内存并拷贝所有元素。

动态扩容时,只需分配新的内存块。

插入/删除

适合尾部插入和删除,头部或中间操作开销较大。

适合头部和尾部插入和删除,中间操作性能接近 vector

迭代器有效性

尾部插入/删除时,可能导致迭代器失效(扩容时会失效)。

插入/删除操作后,迭代器通常仍然有效。

空间效率

连续内存,空间利用率更高。

分块存储,内存碎片可能更多,但扩容效率更高。

适用场景

用于需要随机访问或尾部操作的场景。

用于需要频繁头尾操作的场景(如队列实现)。

例子

vector

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v = {1, 2, 3};
    v.push_back(4); // 在尾部插入元素
    v[1] = 10;      // 通过索引访问
    for (int i : v) {
        cout << i << " "; // 输出:1 10 3 4
    }
    return 0;
}

deque

#include <iostream>
#include <deque>
using namespace std;

int main() {
    deque<int> d = {1, 2, 3};
    d.push_front(0); // 在头部插入元素
    d.push_back(4);  // 在尾部插入元素
    for (int i : d) {
        cout << i << " "; // 输出:0 1 2 3 4
    }
    return 0;
}

5.请你说明一下STL底层数据结构实现?

List(双向链表)

底层数据结构为链表,支持快速插入和删除操作。

Deque(双端队列)

底层数据结构通常由中央控制器和多个缓冲区组成,支持快速的首尾插入和删除操作

Stack(栈)

底层一般使用list或deque实现,封装头部操作以实现栈的功能

Queue(队列)

底层一般使用list或deque实现,封装头部操作以实现队列的功能。

Priority_queue(优先队列)

底层数据结构一般为vector,通过堆(heap)来管理底层容器以实现有序的优先级操作。

Set(集合)

底层数据结构为红黑树,有序且不重复。

Map(映射)

底层数据结构为红黑树,有序的键值对集合,键不重复。

Hash_set(哈希集合)

底层数据结构为哈希表,无序且元素不重复。

6.请你说说map和set区别差异?

差异

1. map

  • 存储键值对的数据。
  • 键(key)是唯一的,每个键对应一个值(value)。
  • 可以通过键快速查找对应的值。
  • 通过键直接访问值:O(log n)
  • 插入时,需要同时插入键和值。

常用操作:

  • 插入:map[key] = valueemplace(key, value)
  • 查找:map.find(key)
  • 访问:通过 map[key] 获取对应的值。

2. set

  • 存储唯一的键,不存储值。
  • 每个元素是唯一的,且只包含键(key)。
  • 只存储键,操作相对更简单,内存占用也更少。

常用操作:

  • 插入:set.insert(key)
  • 查找:set.find(key)
  • 删除:set.erase(key)

底层结构:

  • 二者底层均基于红黑树实现。
  • 红黑树是一种平衡二叉搜索树,保证插入、删除、查找操作的时间复杂度为 O(log n)。
  • 数据在红黑树中是按键值大小自动排序的(中序遍历为升序)。

时间复杂度:两者的插入、删除、查找操作的时间复杂度都是 O(log n)

使用场景

1. 使用 map 的场景

  • 需要通过键快速找到对应值的场景,例如: 记录学生学号与成绩的对应关系。存储配置项(键值对)。
  • 例子:
std::map<std::string, int> studentScores;
studentScores["Alice"] = 90; // 插入
studentScores["Bob"] = 85;  // 插入
std::cout << studentScores["Alice"]; // 输出 90

2. 使用 set 的场景

  • 需要快速查找某个值是否存在,或者对一组数据去重的场景,例如: 检查一组单词是否唯一。判断一个元素是否在集合中。
  • 例子:
std::set<int> uniqueNumbers;
uniqueNumbers.insert(10); // 插入
uniqueNumbers.insert(20); // 插入
if (uniqueNumbers.find(10) != uniqueNumbers.end()) {
    std::cout << "10 exists in the set." << std::endl;
}

mapset 的比较

数据结构

存储键值对(key-value

存储键(key

是否有序

是(按键值排序)

是(按键值排序)

底层实现

红黑树

红黑树

插入时间复杂度

O(log n)

O(log n)

查找时间复杂度

O(log n)

O(log n)

删除时间复杂度

O(log n)

O(log n)

内存占用

较大(存储键和值)

较小(只存储键)

访问方式

通过键访问对应的值(map[key]

查找键是否存在

典型应用

存储键值对关系,例如字典、配置项等

去重、快速查找某元素是否存在

map 例子

#include <iostream>
#include <map>

int main() {
    std::map<std::string, int> scores;
    scores["Alice"] = 90;
    scores["Bob"] = 85;

    // 查找键值对
    if (scores.find("Alice") != scores.end()) {
        std::cout << "Alice's score: " << scores["Alice"] << std::endl;
    }

    // 遍历
    for (const auto& pair : scores) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }

    return 0;
}

set 例子

#include <iostream>
#include <set>

int main() {
    std::set<int> uniqueNumbers;
    uniqueNumbers.insert(10);
    uniqueNumbers.insert(20);
    uniqueNumbers.insert(10); // 重复插入,不会生效

    // 查找
    if (uniqueNumbers.find(10) != uniqueNumbers.end()) {
        std::cout << "10 exists in the set." << std::endl;
    }

    // 遍历
    for (int num : uniqueNumbers) {
        std::cout << num << " ";
    }

    return 0;
}

7.请你说说STL迭代器怎样删除元素?

基本规则

删除容器中的元素时,删除操作会使迭代器失效,但容器通常会返回指向下一个有效元素的迭代器。常见的操作方式如下:

it = container.erase(it);

这里,erase() 函数会删除迭代器 it 指向的元素,并返回一个新的迭代器,指向被删除元素的下一个位置。

各容器中使用迭代器删除元素的方法

(1)std::vectorstd::deque

  • 删除单个元素或范围元素时,需要移动后续元素,效率为 O(n)。
  • 使用 erase() 函数可以避免迭代器失效。

例子:为 3 的元素

#include <vector>
#include <iostream>

std::vector<int> vec = {1, 3, 2, 3, 4};
for (auto it = vec.begin(); it != vec.end(); ) {
    if (*it == 3) {
        it = vec.erase(it); // 删除当前元素,并更新迭代器指向下一个元素
    } else {
        ++it; // 否则移动到下一个元素
    }
}

// 输出结果:1 2 4
for (int v : vec) {
    std::cout << v << " ";
}

注意:直接 ++it 后调用 erase(it) 会导致迭代器失效。

(2)std::list

  • std::list 是双向链表,删除元素时不需要移动后续元素,因此删除效率较高,复杂度为 O(1)。
  • erase() 函数可以删除单个元素或范围。

例子:删除所有偶数

#include <list>
#include <iostream>

std::list<int> lst = {1, 2, 3, 4, 5};
for (auto it = lst.begin(); it != lst.end(); ) {
    if (*it % 2 == 0) {
        it = lst.erase(it); // 删除当前元素,并更新迭代器指向下一个元素
    } else {
        ++it; // 否则移动到下一个元素
    }
}

// 输出结果:1 3 5
for (int v : lst) {
    std::cout << v << " ";
}

(3)std::setstd::map

  • std::setstd::map 基于红黑树,删除元素的复杂度为 O(log n)。
  • 迭代器删除时需要使用 erase() 方法。

例子:从 std::set 中删除所有偶数

#include <set>
#include <iostream>

std::set<int> s = {1, 2, 3, 4, 5};
for (auto it = s.begin(); it != s.end(); ) {
    if (*it % 2 == 0) {
        it = s.erase(it); // 删除当前元素,更新迭代器
    } else {
        ++it; // 否则移动到下一个元素
    }
}

// 输出结果:1 3 5
for (int v : s) {
    std::cout << v << " ";
}

注意:对于 std::map,删除时基于键值对,迭代器指向 std::pair

(4)std::unordered_setstd::unordered_map

  • 这类容器基于哈希表,删除操作的复杂度为 O(1)。
  • 使用迭代器删除与 std::setstd::map 类似。

例子:删除所有偶数

#include <unordered_set>
#include <iostream>

std::unordered_set<int> us = {1, 2, 3, 4, 5};
for (auto it = us.begin(); it != us.end(); ) {
    if (*it % 2 == 0) {
        it = us.erase(it); // 删除当前元素,更新迭代器
    } else {
        ++it;
    }
}

// 输出结果:1 3 5
for (int v : us) {
    std::cout << v << " ";
}

#秋招##春招##嵌入式##牛客激励计划##面经#

嵌入式/C++八股 文章被收录于专栏

本人双飞本,校招上岸广和通。此专栏覆盖嵌入式常见面试题,有C/C++相关的知识,数据结构和算法也有嵌入式相关的知识,如操作系统、网络协议、硬件知识。本人也是校招过来的,大家底子甚至项目,可能都不错,但是在面试准备中常见八股可能准备不全。此专栏很适合新手学习基础也适合大佬备战复习,比较全面。最终希望各位友友们早日拿到心仪offer。也希望大家点点赞,收藏,送送小花。这是对我的肯定和鼓励。 持续更新中

全部评论

相关推荐

2024-12-25 16:42
已编辑
上海理工大学 产品经理
产品小白成长中:怎么回事吞我评论啊啊啊啊查看图片
点赞 评论 收藏
分享
点赞 评论 收藏
分享
评论
7
16
分享

创作者周榜

更多
牛客网
牛客企业服务