C++ STL 容器与算法深度解析

✍️ Fer·📅 2026年8月17日·👁 1 次阅读
c++开发
📚 系列:现代 C++ 学习之路

一、STL 整体架构

⭐ STL 六大组件

⭐ 容器分类总览


二、顺序容器

vector 深度解析

底层结构

vector 内部结构(三个指针):

┌──────────────────────────────────────────────────────┐
│  vector<int>                                          │
│  ┌─────────┐                                         │
│  │ _start   │──→┌───┬───┬───┬───┬───┬───┬───┬───┐   │
│  ├─────────┤    │ 1 │ 2 │ 3 │ 4 │ 5 │   │   │   │   │
│  │ _finish  │──→└───┴───┴───┴───┴───┴───┴───┴───┘   │
│  ├─────────┤                        ↑               │
│  │ _end_cap │────────────────────────┘               │
│  └─────────┘                                         │
│                                                      │
│  size() = _finish - _start = 5                       │
│  capacity() = _end_cap - _start = 8                  │
│  空闲空间 = _end_cap - _finish = 3                    │
└──────────────────────────────────────────────────────┘

⭐ 扩容机制

⚠️ 扩容会导致所有迭代器、指针、引用失效!

cpp
std::vector<int> v;
v.reserve(1000);          // ✅ 预分配,避免多次扩容

// 扩容的均摊复杂度分析:
// 每次扩容为 2 倍,n 次 push_back 总共拷贝:
// n/2 + n/4 + n/8 + ... ≈ n
// 均摊每次 push_back 为 O(1)

vector<bool> 的特殊性

cpp
// ⚠️ vector<bool> 不是真正的容器!
std::vector<bool> vb = {true, false, true};

// 内部每个 bool 用 1 bit 存储(而非 1 byte)
// operator[] 返回代理对象,不是 bool&

auto ref = vb[0];         // 类型是 vector<bool>::reference,不是 bool&
bool* ptr = &vb[0];       // ❌ 编译错误!不能取地址

// ✅ 替代方案
std::vector<char> vc;     // 用 char 代替
std::deque<bool> db;      // deque<bool> 是正常的
std::bitset<100> bs;      // 固定大小的位集合

deque 的底层结构

deque 的分段连续存储:

┌──────────┐
│  map 数组  │ (指针数组,指向各个缓冲区)
│  ┌──────┐ │
│  │ ptr0 │─┼──→ ┌───┬───┬───┬───┐  缓冲区 0
│  ├──────┤ │    └───┴───┴───┴───┘
│  │ ptr1 │─┼──→ ┌───┬───┬───┬───┐  缓冲区 1
│  ├──────┤ │    └───┴───┴───┴───┘
│  │ ptr2 │─┼──→ ┌───┬───┬───┬───┐  缓冲区 2
│  ├──────┤ │    └───┴───┴───┴───┘
│  │ ptr3 │─┼──→ ┌───┬───┬───┬───┐  缓冲区 3
│  └──────┘ │    └───┴───┴───┴───┘
└──────────┘

• 每个缓冲区是一段连续内存
• map 是指针数组,管理所有缓冲区
• 头部/尾部插入时,在对应缓冲区操作
• 看起来连续,实际分段 → 支持 O(1) 头尾操作

list vs vector vs deque 对比

特性vectordequelist
底层连续数组分段数组双向链表
随机访问O(1) ✅O(1)O(n) ❌
头部插入O(n) ❌O(1)O(1) ✅
尾部插入均摊 O(1)O(1)O(1)
中间插入O(n)O(n)O(1)(已知位置)
内存布局连续(缓存友好)近似连续不连续(缓存不友好)
迭代器失效插入/扩容时全部头尾插入时全部仅删除节点
额外开销尾部预留空间map + 多个缓冲区每节点两个指针
推荐场景默认首选需要双端操作频繁中间增删

三、关联容器

map / set 底层:红黑树

红黑树示例(map<int, string>):

            ┌───[7,B]───┐
            │            │
        ┌─[3,R]─┐    ┌─[18,R]─┐
        │       │    │         │
      [1,B]  [5,B] [10,B]   [22,B]
                      │
                   [15,R]

B = 黑色, R = 红色
按 key 有序:1, 3, 5, 7, 10, 15, 18, 22

map 的常见操作与陷阱

cpp
std::map<std::string, int> m;

// 插入
m["apple"] = 3;                          // operator[]:不存在则创建
m.insert({"banana", 5});                 // insert:已存在则不覆盖
m.insert_or_assign("cherry", 7);         // C++17:覆盖式插入
m.emplace("date", 9);                    // 原地构造
auto [it, ok] = m.try_emplace("fig", 11); // C++17:key 不存在才构造 value

// ⚠️ operator[] 的陷阱
int val = m["unknown"];  // 如果不存在,会插入 {"unknown", 0}!
// ✅ 安全查找
if (auto it = m.find("key"); it != m.end()) {
    int val = it->second;
}
// 或
int val = m.count("key") ? m.at("key") : -1;

// 遍历(C++17 结构化绑定)
for (const auto& [key, value] : m) {
    std::cout << key << ": " << value << "\n";
}

map vs unordered_map 如何选择?

特性mapunordered_map
底层红黑树哈希表(开链法)
有序✅ key 有序❌ 无序
查找O(log n)均摊 O(1)
最坏查找O(log n)O(n)(哈希冲突)
key 要求需要 operator<需要 hash + operator==
内存较小较大(桶 + 链表)
迭代器稳定性删除不影响其他rehash 时全部失效

unordered_map 底层:哈希表

哈希表结构(开链法 / 拉链法):

bucket 数组:
┌───┐
│ 0 │ → nullptr
├───┤
│ 1 │ → [key1,val1] → [key5,val5] → nullptr
├───┤
│ 2 │ → [key2,val2] → nullptr
├───┤
│ 3 │ → nullptr
├───┤
│ 4 │ → [key3,val3] → [key4,val4] → [key7,val7] → nullptr
├───┤
│ 5 │ → nullptr
├───┤
│ 6 │ → [key6,val6] → nullptr
├───┤
│ 7 │ → nullptr
└───┘

查找过程:
  ① hash(key) % bucket_count → 定位到桶
  ② 在桶的链表中线性查找 key
  ③ 找到 → 返回 value;没找到 → 返回 end()

rehash 触发条件:
  load_factor = size / bucket_count > max_load_factor(默认 1.0)
  → 桶数翻倍,所有元素重新散列

自定义类型作为 unordered_map 的 key

cpp
struct Point {
    int x, y;
    bool operator==(const Point& other) const {
        return x == other.x && y == other.y;
    }
};

// 自定义 hash
struct PointHash {
    size_t operator()(const Point& p) const {
        auto h1 = std::hash<int>{}(p.x);
        auto h2 = std::hash<int>{}(p.y);
        return h1 ^ (h2 << 1);  // 组合哈希
    }
};

std::unordered_map<Point, std::string, PointHash> pm;
pm[{1, 2}] = "A";

四、容器适配器

stackqueuepriority_queue

cpp
// 大顶堆(默认)
std::priority_queue<int> maxHeap;
maxHeap.push(3); maxHeap.push(1); maxHeap.push(5);
maxHeap.top();  // 5

// 小顶堆
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
minHeap.push(3); minHeap.push(1); minHeap.push(5);
minHeap.top();  // 1

// 自定义比较
auto cmp = [](const auto& a, const auto& b) { return a.cost > b.cost; };
std::priority_queue<Task, std::vector<Task>, decltype(cmp)> taskQueue(cmp);

五、迭代器

⭐ 迭代器的五种类型

类型能力支持操作对应容器
输入迭代器只读单遍++ * == !=istream_iterator
输出迭代器只写单遍++ *ostream_iterator
前向迭代器读写多遍输入 + 多遍forward_list unordered_*
双向迭代器可后退前向 + --list set map
随机访问迭代器可跳跃双向 + +n -n [] <vector deque array

⭐ 迭代器失效规则详解

安全删除的正确写法

cpp
// ❌ 错误:erase 后迭代器失效
for (auto it = vec.begin(); it != vec.end(); ++it) {
    if (*it == target) vec.erase(it);  // ❌ it 失效
}

// ✅ 方法1:erase 返回下一个有效迭代器
for (auto it = vec.begin(); it != vec.end(); ) {
    if (*it == target) it = vec.erase(it);
    else ++it;
}

// ✅ 方法2:erase-remove idiom
vec.erase(std::remove(vec.begin(), vec.end(), target), vec.end());

// ✅ 方法3:C++20 std::erase / std::erase_if
std::erase(vec, target);
std::erase_if(vec, [](int x) { return x % 2 == 0; });

六、常用算法深度解析

⭐ 排序算法

cpp
std::vector<int> v = {5, 3, 1, 4, 2};

// std::sort —— IntroSort(快排+堆排+插入排序)
std::sort(v.begin(), v.end());                    // 升序
std::sort(v.begin(), v.end(), std::greater<>());  // 降序
// 时间:O(n log n)  空间:O(log n)  不稳定

// std::stable_sort —— 归并排序(内存够时)
std::stable_sort(v.begin(), v.end());
// 时间:O(n log n)  空间:O(n)  稳定

// std::partial_sort —— 堆排序取前 k 个
std::partial_sort(v.begin(), v.begin() + 3, v.end());
// 时间:O(n log k)

// std::nth_element —— 快速选择,第 k 小
std::nth_element(v.begin(), v.begin() + 2, v.end());
// 时间:O(n) 平均
算法底层时间空间稳定场景
sortIntroSortO(n log n)O(log n)默认首选
stable_sort归并排序O(n log n)O(n)需要稳定排序
partial_sort堆排序O(n log k)O(1)只需前 k 个
nth_element快速选择O(n) 平均O(1)找第 k 个元素

⭐ 查找算法

cpp
// 线性查找
auto it = std::find(v.begin(), v.end(), 3);         // O(n)
auto it2 = std::find_if(v.begin(), v.end(),
    [](int x) { return x > 3; });                    // O(n)

// 二分查找(需已排序)
bool found = std::binary_search(v.begin(), v.end(), 3);  // O(log n)
auto lb = std::lower_bound(v.begin(), v.end(), 3);       // ≥ 3 的第一个
auto ub = std::upper_bound(v.begin(), v.end(), 3);       // > 3 的第一个
auto [lo, hi] = std::equal_range(v.begin(), v.end(), 3); // 等于 3 的范围

⭐ 变换与生成

cpp
// transform:对每个元素变换
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst(src.size());
std::transform(src.begin(), src.end(), dst.begin(),
    [](int x) { return x * x; });
// dst = {1, 4, 9, 16, 25}

// accumulate:累加/累积
int sum = std::accumulate(src.begin(), src.end(), 0);         // 15
int product = std::accumulate(src.begin(), src.end(), 1,
    std::multiplies<>());                                      // 120

// C++17 reduce(可并行)
int sum2 = std::reduce(src.begin(), src.end(), 0);

// iota:生成递增序列
std::vector<int> seq(10);
std::iota(seq.begin(), seq.end(), 1);  // {1,2,3,...,10}

// generate:自定义生成
int n = 0;
std::generate(seq.begin(), seq.end(), [&n]() { return n++; });

七、C++20 Ranges

⭐ Ranges 管道式编程

cpp
#include <ranges>
#include <vector>
#include <iostream>

std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// 传统写法
std::vector<int> result;
for (auto x : v) {
    if (x % 2 == 0) {
        result.push_back(x * x);
    }
}

// Ranges 管道写法(C++20)
auto result2 = v
    | std::views::filter([](int x) { return x % 2 == 0; })
    | std::views::transform([](int x) { return x * x; })
    | std::views::take(3);

for (int x : result2) {
    std::cout << x << " ";  // 4 16 36
}
Ranges 视图作用示例
views::filter过滤views::filter(is_even)
views::transform变换views::transform(square)
views::take取前 n 个views::take(5)
views::drop跳过前 n 个views::drop(3)
views::reverse反转views::reverse
views::zip (C++23)合并views::zip(v1, v2)
views::enumerate (C++23)带索引views::enumerate

八、容器选择决策

⭐ 如何选择合适的容器?

⭐ 各容器时间复杂度速查表

操作vectordequelistset/mapunordered_*
随机访问O(1)O(1)O(n)O(log n)O(1) 均摊
头部插入O(n)O(1)O(1)
尾部插入O(1) 均摊O(1)O(1)
有序插入O(log n)O(1) 均摊
查找O(n)O(n)O(n)O(log n)O(1) 均摊
删除(已知位置)O(n)O(n)O(1)O(log n)O(1) 均摊

九、总结知识图谱

💬 评论

加载评论中...
📑 文章目录