C++ STL详解及面试相关重点问题

目录

一、STL概述

(一)序列式容器

1. vector(基于动态数组)

2. list(基于双向链表)

3. deque(基于分段的动态数组)

4. string(基于动态数组)

(二)关联式容器

1. map(基于红黑树)

2. unordered_map(基于哈希表)

3. set(基于红黑树)

4. unordered_set(基于哈希表)

5. bitset(位图)

(三)容器适配器

1. stack(栈)

2. queue(队列)

3. priority_queue(优先队列)

(四)常见的算法

1. 非修改序列算法

2. 修改序列算法

3. 非修改关联算法

4. 修改关联算法

5. 排序算法

6. 数值算法

7. 其他算法

二、STL 的六大核心组件

(一)容器

(二)算法

(三)迭代器

(四)适配器

(五)仿函数

(六)空间配置器

三、面试常见问题

1. vector和list的区别?

2. vector是如何插入数据的?

3. vector和list都有缺点,有什么其他的设计方案, 能解决问题?

4. map和set底层是什么?

5. map的operator[ ]实现是什么?

6. 一个类型做map和set的key有什么要求?

7. map和set和multi_xxx的区别?

8. unordered_map和unordered_set是如何实现的?

9. unordered_map和map的区别是什么?

10. 一个类型做unordered_map和unordered_set有什 么要求?

11. C++的优缺点

12. 容器是否是线程安全的?

13. 迭代器失效是什么?

14. STL的优缺点是什么?


一、STL概述

(一)序列式容器

1. vector(基于动态数组)

std::vector 是一个动态数组,支持随机访问,尾部插入和删除操作效率高。

示例:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    vec.push_back(6); // 尾部插入
    vec.insert(vec.begin() + 2, 10); // 在指定位置插入
    vec.erase(vec.begin() + 2); // 删除指定位置的元素

    for (int x : vec) {
        std::cout << x << " ";
    }
    return 0;
}

底层实现是动态数组:内存连续,支持随机访问。

扩容机制

vector 的容量不足以容纳新元素时,会分配一块更大的内存(通常是当前容量的两倍)。

将现有元素拷贝到新内存中,然后释放旧内存。

优点

随机访问效率高(O(1))。

尾部插入和删除效率高(平均 O(1))。

内存连续,缓存友好。

缺点

插入和删除中间元素效率低(O(n))。

扩容时需要重新分配内存并拷贝数据。

2. list(基于双向链表)

std::list 是一个双向链表,支持在任意位置高效地插入和删除元素。

示例:

#include <iostream>
#include <list>

int main() {
    std::list<int> lst = {1, 2, 3, 4, 5};
    lst.push_back(6); // 尾部插入
    lst.push_front(0); // 头部插入
    lst.insert(std::next(lst.begin(), 2), 10); // 在指定位置插入
    lst.erase(std::next(lst.begin(), 2)); // 删除指定位置的元素

    for (int x : lst) {
        std::cout << x << " ";
    }
    return 0;
}

底层实现是双向链表:每个节点包含数据和前后指针。

内存分配:每个节点单独分配内存,内存不连续。

优点

插入和删除任意位置的元素效率高(O(1))。

双向遍历。

缺点

随机访问效率低(O(n))。

内存占用较大(每个节点包含指针)。

3. deque(基于分段的动态数组)

std::deque 是一个双端队列,支持在两端高效地插入和删除元素。

示例:

#include <iostream>
#include <deque>

int main() {
    std::deque<int> dq = {1, 2, 3, 4, 5};
    dq.push_back(6); // 尾部插入
    dq.push_front(0); // 头部插入
    dq.insert(dq.begin() + 2, 10); // 在指定位置插入
    dq.erase(dq.begin() + 2); // 删除指定位置的元素

    for (int x : dq) {
        std::cout << x << " ";
    }
    return 0;
}

底层实现是分段的动态数组:内存分段管理,每段是一个固定大小的数组。

内存分配:动态分配内存,支持两端扩展。

优点

两端操作效率高(O(1))。

支持随机访问(O(1))。

缺点

内存分配较为复杂,可能涉及多次内存分配。

随机访问效率低于 vector

4. string(基于动态数组)

std::string 是一个动态数组,用于存储字符序列,支持随机访问和动态扩展。

示例:

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello";
    str += " World"; // 尾部插入
    str.insert(5, ", "); // 在指定位置插入
    str.erase(5, 2); // 删除指定范围的字符

    std::cout << str << std::endl;
    return 0;
}

底层实现是动态数组:内存连续,支持随机访问。

扩容机制

string 的容量不足以容纳新字符时,会分配一块更大的内存(通常是当前容量的两倍)。

将现有字符拷贝到新内存中,然后释放旧内存。

优点

随机访问效率高(O(1))。

尾部插入和删除效率高(平均 O(1))。

内存连续,缓存友好。

缺点

插入和删除中间字符效率低(O(n))。

扩容时需要重新分配内存并拷贝数据

(二)关联式容器

1. map(基于红黑树)

std::map 是一个基于红黑树的有序映射,键值对按键的顺序存储。

示例:

#include <iostream>
#include <map>

int main() {
    std::map<int, std::string> m = {{1, "one"}, {2, "two"}, {3, "three"}};
    m[4] = "four"; // 插入键值对
    m.insert({5, "five"}); // 插入键值对

    for (const auto& pair : m) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }

    return 0;
}

底层实现是红黑树:一种自平衡二叉搜索树,满足以下性质:

①每个节点是红色或黑色。

②根节点是黑色。

③所有叶子节点(空节点)是黑色。

④如果一个节点是红色,则它的两个子节点都是黑色。

⑤从任意节点到其每个叶子的所有路径都包含相同数量的黑色节点。

优点

插入、删除和查找操作的平均时间复杂度为 O(log n)。

键值对按顺序存储,支持范围查询。

缺点

内存占用较大(每个节点包含额外的指针)。

插入和删除操作涉及树的重新平衡,复杂度较高。

2. unordered_map(基于哈希表)

std::unordered_map 是一个基于哈希表的无序映射,键值对按哈希值存储。

示例:

#include <iostream>
#include <unordered_map>

int main() {
    std::unordered_map<int, std::string> um = {{1, "one"}, {2, "two"}, {3, "three"}};
    um[4] = "four"; // 插入键值对
    um.insert({5, "five"}); // 插入键值对

    for (const auto& pair : um) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }

    return 0;
}

底层实现是哈希表:通过哈希函数将键映射到桶中,支持链表或开放寻址解决冲突。哈希函数将键映射到桶索引。如果发生冲突,使用链表或开放寻址解决冲突。

优点

插入、删除和查找操作的平均时间复杂度为 O(1)。

内存占用相对较小。

缺点

最坏情况下(大量冲突)时间复杂度为 O(n)。

不保证键值对的顺序。

3. set(基于红黑树)

std::set 是一个基于红黑树的有序集合,存储唯一键值。

示例:

#include <iostream>
#include <set>

int main() {
    std::set<int> s = {1, 2, 3, 4, 5};
    s.insert(6); // 插入元素
    s.erase(3); // 删除元素

    for (int x : s) {
        std::cout << x << " ";
    }

    return 0;
}

底层实现是红黑树:一种自平衡二叉搜索树,满足以下性质:

①每个节点是红色或黑色。

②根节点是黑色。

③所有叶子节点(空节点)是黑色。

④如果一个节点是红色,则它的两个子节点都是黑色。

⑤从任意节点到其每个叶子的所有路径都包含相同数量的黑色节点。

优点

插入、删除和查找操作的平均时间复杂度为 O(log n)。

键值按顺序存储,支持范围查询。

缺点

内存占用较大(每个节点包含额外的指针)。

插入和删除操作涉及树的重新平衡,复杂度较高。

4. unordered_set(基于哈希表)

std::unordered_set 是一个基于哈希表的无序集合,存储唯一键值。

示例:

#include <iostream>
#include <unordered_set>

int main() {
    std::unordered_set<int> us = {1, 2, 3, 4, 5};
    us.insert(6); // 插入元素
    us.erase(3); // 删除元素

    for (int x : us) {
        std::cout << x << " ";
    }

    return 0;
}

底层实现是哈希表:通过哈希函数将键映射到桶中,支持链表或开放寻址解决冲突。哈希函数将键映射到桶索引。如果发生冲突,使用链表或开放寻址解决冲突。

优点

插入、删除和查找操作的平均时间复杂度为 O(1)。

内存占用相对较小。

缺点

最坏情况下(大量冲突)时间复杂度为 O(n)。

不保证键值的顺序。

5. bitset(位图)

std::bitset 是一个固定大小的位集合,支持位操作。

示例:

#include <iostream>
#include <bitset>

int main() {
    std::bitset<8> bs(0b10101010); // 初始化为 8 位,值为 10101010
    bs.set(1); // 设置第 1 位为 1
    bs.reset(2); // 将第 2 位设置为 0
    bs.flip(3); // 翻转第 3 位

    std::cout << bs << std::endl; // 输出位集合

    return 0;
}

底层实现是固定大小的位集合:内存连续,支持位操作。

操作

set(pos):将第 pos 位设置为 1。

reset(pos):将第 pos 位设置为 0。

flip(pos):翻转第 pos 位。

test(pos):检查第 pos 位是否为 1。

优点

位操作效率高(O(1))。

内存占用小,适合存储大量布尔值。

缺点

大小固定,无法动态扩展。

不支持范围操作。

(三)容器适配器

1. stack(栈)

std::stack 是一个后进先出(LIFO)的栈,支持在栈顶插入和删除元素。

示例:

#include <iostream>
#include <stack>

int main() {
    std::stack<int> stk;
    stk.push(1); // 入栈
    stk.push(2);
    stk.push(3);

    while (!stk.empty()) {
        std::cout << stk.top() << " "; // 访问栈顶元素
        stk.pop(); // 出栈
    }

    return 0;
}

默认底层容器std::deque

常见操作

push(value):在栈顶插入元素。

pop():删除栈顶元素。

top():访问栈顶元素。

empty():检查栈是否为空。

size():获取栈的大小。

优点

简单易用,适合需要后进先出操作的场景。

支持高效的插入和删除操作(O(1))。

缺点

只能访问栈顶元素,不支持随机访问。

无法直接访问中间元素。

2. queue(队列)

std::queue 是一个先进先出(FIFO)的队列,支持在队尾插入元素,在队首删除元素。

示例:

#include <iostream>
#include <queue>

int main() {
    std::queue<int> q;
    q.push(1); // 入队
    q.push(2);
    q.push(3);

    while (!q.empty()) {
        std::cout << q.front() << " "; // 访问队首元素
        q.pop(); // 出队
    }

    return 0;
}

默认底层容器std::deque

常见操作

push(value):在队尾插入元素。

pop():删除队首元素。

front():访问队首元素。

back():访问队尾元素。

empty():检查队列是否为空。

size():获取队列的大小。

优点

简单易用,适合需要先进先出操作的场景。

支持高效的插入和删除操作(O(1))。

缺点

只能访问队首和队尾元素,不支持随机访问。

无法直接访问中间元素。

3. priority_queue(优先队列)

std::priority_queue 是一个优先队列,支持高效的插入和删除操作,元素按优先级顺序排列。

示例:

#include <iostream>
#include <queue>

int main() {
    std::priority_queue<int> pq;
    pq.push(1); // 插入元素
    pq.push(3);
    pq.push(2);

    while (!pq.empty()) {
        std::cout << pq.top() << " "; // 访问优先级最高的元素
        pq.pop(); // 删除优先级最高的元素
    }

    return 0;
}

默认底层容器std::vector

操作

push(value):插入元素,自动按优先级排序。

pop():删除优先级最高的元素。

top():访问优先级最高的元素。

empty():检查优先队列是否为空。

size():获取优先队列的大小。

优点

支持高效的插入和删除操作(O(log n))。

元素自动按优先级排序,适合需要按优先级处理元素的场景。

缺点

只能访问优先级最高的元素,不支持随机访问。

无法直接访问中间元素。

(四)常见的算法

1. 非修改序列算法

这些算法用于检查或操作序列中的元素,但不会修改元素本身。

  • std::find:查找指定值的元素,返回找到的第一个元素的迭代器。

  • std::count:统计指定值的元素数量。

  • std::equal:检查两个范围是否相等。

  • std::all_of:检查范围内所有元素是否满足指定条件。

  • std::any_of:检查范围内是否有任意元素满足指定条件。

  • std::none_of:检查范围内是否有任意元素不满足指定条件。

  • std::for_each:对范围内的每个元素执行指定操作。

2. 修改序列算法

这些算法用于修改序列中的元素。

  • std::copy:将一个范围的元素拷贝到另一个范围。

  • std::move:将一个范围的元素移动到另一个范围。

  • std::fill:将指定值填充到一个范围。

  • std::transform:对范围内的每个元素应用函数,并将结果存储到另一个范围。

  • std::reverse:反转范围内的元素顺序。

  • std::sort:对范围内的元素进行排序。

  • std::unique:移除范围内的重复元素。

3. 非修改关联算法

这些算法用于处理关联容器(如 std::mapstd::set)。

  • std::find_if:查找满足指定条件的第一个元素。

  • std::find_if_not:查找不满足指定条件的第一个元素。

  • std::count_if:统计满足指定条件的元素数量。

  • std::mismatch:查找两个范围中第一个不匹配的元素对。

  • std::equal:检查两个范围是否相等(支持自定义比较)。

4. 修改关联算法

这些算法用于修改关联容器中的元素。

  • std::remove:移除范围内的指定值的元素。

  • std::remove_if:移除满足指定条件的元素。

  • std::replace:将范围内的指定值的元素替换为另一个值。

  • std::replace_if:将满足指定条件的元素替换为另一个值。

  • std::erase:移除范围内的元素(需要容器支持)。

5. 排序算法

这些算法用于对序列进行排序和查找操作。

  • std::sort:对范围内的元素进行排序。

  • std::stable_sort:对范围内的元素进行稳定排序。

  • std::partial_sort:对范围内的部分元素进行排序。

  • std::nth_element:将范围内的第 n 个元素放到正确的位置。

  • std::lower_bound:查找范围内的第一个不小于指定值的元素。

  • std::upper_bound:查找范围内的第一个大于指定值的元素。

  • std::equal_range:查找范围内的指定值的上下界。

6. 数值算法

这些算法用于对序列进行数值计算。

  • std::accumulate:计算范围内的元素的累加和。

  • std::inner_product:计算两个范围内的元素的内积。

  • std::partial_sum:计算范围内的部分和。

  • std::adjacent_difference:计算范围内的相邻元素的差。

7. 其他算法

这些算法用于其他用途。

  • std::for_each:对范围内的每个元素执行指定操作。

  • std::generate:用生成器函数填充范围内的元素。

  • std::shuffle:随机打乱范围内的元素顺序。

  • std::next_permutation:生成范围内的下一个排列。

  • std::prev_permutation:生成范围内的上一个排列。

二、STL 的六大核心组件

(一)容器

容器是用于存储和管理数据的对象。STL 提供了多种类型的容器,每种容器都有其特定的用途和性能特点。

序列容器

std::vector:动态数组,支持随机访问。

std::list:双向链表,支持任意位置的高效插入和删除。

std::deque:双端队列,支持两端的高效插入和删除。

std::array(C++11):固定大小的数组,支持随机访问。

std::forward_list(C++11):单向链表,内存占用小。

关联容器

std::setstd::multiset:基于红黑树的有序集合。

std::mapstd::multimap:基于红黑树的有序映射。

std::unordered_setstd::unordered_multiset:基于哈希表的无序集合。

std::unordered_mapstd::unordered_multimap:基于哈希表的无序映射。

容器适配器

std::stack:后进先出(LIFO)的栈。

std::queue:先进先出(FIFO)的队列。

std::priority_queue:优先队列,支持高效的插入和删除操作。

(二)算法

算法是用于对容器中的元素进行操作的函数模板。STL 提供了一组通用算法,这些算法通过迭代器与容器进行交互。

非修改序列算法:如 std::findstd::countstd::equal

修改序列算法:如 std::copystd::transformstd::sort

排序算法:如 std::sortstd::stable_sortstd::nth_element

数值算法:如 std::accumulatestd::inner_product

(三)迭代器

迭代器是用于访问容器中元素的对象,类似于指针。迭代器是容器和算法之间的纽带。

Input Iterator:只读迭代器,支持单向遍历。

Output Iterator:只写迭代器,支持单向遍历。

Forward Iterator:支持单向遍历,可以多次遍历。

Bidirectional Iterator:支持双向遍历。

Random Access Iterator:支持随机访问,可以进行任意位置的访问。

Contiguous Iterator(C++20):支持连续存储的迭代器。

(四)适配器

适配器是通过封装和扩展其他组件的功能,将一个容器或迭代器转换为另一种数据结构或接口。

容器适配器

std::stack:后进先出(LIFO)的栈。

std::queue:先进先出(FIFO)的队列。

std::priority_queue:优先队列,支持高效的插入和删除操作。

迭代器适配器

std::reverse_iterator:反向迭代器。

std::insert_iterator:插入迭代器。

std::ostream_iterator:输出流迭代器。

(五)仿函数

仿函数是结合函数和对象的特性,重载 operator(),用于算法的行为定制。

标准仿函数

std::plusstd::minusstd::multipliesstd::divides

std::equal_tostd::not_equal_tostd::lessstd::greater

自定义仿函数

通过重载 operator() 实现自定义行为。

(六)空间配置器

空间配置器为容器提供内存管理支持。容器通过分配器动态分配和释放内存,从而存储和管理数据。

默认分配器std::allocator

自定义分配器:用户可以自定义分配器,以满足特定的内存管理需求。

三、面试常见问题

1. vector和list的区别?

vector 是基于动态数组实现的,支持高效的随机访问和尾部插入删除操作,但插入删除中间元素效率较低,扩容时需要重新分配内存并拷贝数据。
list 是基于双向链表实现的,支持在任意位置高效地插入和删除元素,但不支持随机访问,内存占用较大。
如果需要频繁的随机访问和尾部操作,vector 是更好的选择;如果需要在任意位置频繁插入和删除元素,list 更合适。

2. vector是如何插入数据的?

插入数据:
尾部插入:通过 push_back 方法在 vector 的尾部添加一个元素。如果当前容量足够,直接在尾部添加元素;如果容量不足,会触发扩容操作。
中间插入:通过 insert 方法在指定位置插入一个元素。这需要将插入点之后的所有元素向后移动一个位置,因此时间复杂度为 O(n)。

扩容机制:
容量检查:当 vectorsize() 达到当前 capacity() 时,需要扩容。
分配新内存:通常会分配一块更大的内存,通常是当前容量的两倍(具体倍数由实现决定)。
拷贝数据:将现有元素从旧内存拷贝到新内存。
释放旧内存:释放旧内存,更新指针指向新内存。

3. vector和list都有缺点,有什么其他的设计方案, 能解决问题?

vector 插入和删除操作在非尾部位置时效率较低,而 list 是双向链表随机访问速度慢,需要逐个遍历节点。:中间方案是deque,它结合了vectorlist的优点,支持两端操作且随机访问,但是效率不高。

4. map和set底层是什么?

mapset 的底层实现通常是红黑树。红黑树是一种自平衡的二叉查找树,通过以下规则保持平衡:

①每个节点是红色或黑色。

②根节点是黑色。

③所有叶子节点(空节点)是黑色。

④如果一个节点是红色,则它的两个子节点都是黑色。

⑤从任意节点到其每个叶子的所有路径都包含相同数量的黑色节点。

查找效率:时间复杂度为 O(logn),因为树的高度被限制在 O(logn)。

插入和删除效率:时间复杂度也是 O(logn),因为需要调整树的结构以保持平衡。

大致实现:

插入:先按照二叉查找树的方式插入节点,然后通过旋转和颜色调整来恢复红黑树的性质。

删除:先删除节点,再通过旋转和颜色调整来保持平衡。

查找:从根节点开始,根据键值大小比较,向左或向右遍历,直到找到目标节点或到达叶子节点。

5. map的operator[ ]实现是什么?

mapoperator[] 是一个非常重要的成员函数,用于访问或插入键值对。
访问已存在的键:如果键已经存在于 map 中,operator[] 返回对应键的值的引用。插入新键:如果键不存在于 map 中,operator[] 会自动插入一个键值对,其中键是提供的键,值是值类型的默认值(例如,对于 int 类型,默认值是 0),并返回这个新值的引用。

operator[] 的实现可以分为以下几个步骤:查找键是否存在:通过红黑树的查找操作,确定键是否已经存在于 map 中。------->  键存在时:直接返回对应键的值的引用。------->  键不存在时:插入一个新键值对,值为默认值,并返回这个新值的引用。

6. 一个类型做map和set的key有什么要求?

能支持比较大小或者提供仿函数比较大小

7. map和set和multi_xxx的区别?

map和set的元素是唯一的。multimap和multiset的元素可以重复

mapset默认使用红黑树(自平衡二叉查找树)实现。元素按键(或值)的顺序存储,提供对元素的有序访问。multimapmultiset同样默认使用红黑树实现。元素按键(或值)的顺序存储。但允许键(或值)重复。

mapset:插入操作会检查键(或值)是否已存在。如果键(或值)已存在,插入操作会失败。查找操作返回指向找到的元素的迭代器。multimapmultiset插入操作不会检查键(或值)是否已存在。允许插入多个相同的键(或值)。查找操作返回一个范围(迭代器对),包含所有匹配的元素。

8. unordered_map和unordered_set是如何实现的?

unordered_mapunordered_set 是基于哈希表实现的关联容器。它们的实现依赖于哈希函数和冲突解决机制。
哈希表:通过哈希函数将键映射到桶,使用链地址法或开放寻址法解决冲突。
效率:平均情况下查找、插入和删除操作的时间复杂度为 O(1),但在最坏情况下可能退化到 O(n)。
实现:通过动态数组存储桶,每个桶是一个链表(或平衡树),并根据负载因子动态调整桶的数量。

9. unordered_map和map的区别是什么?

unordered_map底层是哈希表和map底层是红黑树

哈希表

基于哈希函数将键映射到一个较小范围的整数(通常是数组的索引)。使用链地址法开放寻址法解决冲突。内部通常是一个动态数组,每个桶可以是一个链表或平衡树。

红黑树

是一种自平衡二叉查找树。每个节点存储一个键值对,并通过比较键的大小来维护树的有序性。遵循红黑树的五条规则,通过旋转和颜色调整来保持平衡。

10. 一个类型做unordered_map和unordered_set有什 么要求?

作为 unordered_mapunordered_set 的键,类型必须满足可哈希性(提供哈希函数)和可比较性(支持等价性比较)。

11. C++的优缺点

设计复杂,更新慢,菱形继承

12. 容器是否是线程安全的?

不是,没有加锁

13. 迭代器失效是什么?

指STL中容器的增删操改变了其内部结构,导致 迭代器失效。vector由于经过插入操作以后要进行扩容,将原元素复制到新空间中,原空间的迭代器失效,让迭代器自动更新就行。

14. STL的优缺点是什么?

STL优点:高效、通用、安全且灵活,提供丰富的容器和算法,减少重复代码,提升开发效率。STL缺点:学习曲线陡,模板错误信息复杂,性能和内存占用在某些场景下可能不如手写代码优化,编译时间可能较长。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值