C++(STL)

STL 容器不是一个单独的数据结构,而是一组通用的模板类。算法通常不直接依赖某个具体容器,而是通过迭代器操作容器中的元素。

当前 C++ 容器库主要分为:

  • 顺序容器:array、vector、deque、list、forward_list
  • 有序关联容器:set、multiset、map、multimap
  • 无序关联容器:unordered_set、unordered_multiset、unordered_map、unordered_multimap
  • 容器适配器:stack、queue、priority_queue
  • 较新的平面容器:flat_set、flat_multiset、flat_map、flat_multimap
  • 较新的固定容量或块状容器:inplace_vector、hive

一、理解容器必须先掌握几个概念

1.size和capacity

以vector为例:

std::vector<int>v;

它内部大致有三个信息:

begin 指向第一个元素

end 指向最后一个元素之后

capacity 指向当前已分配空间的末尾

eg:

size=3

capacity=5

10\]\[20\]\[30\]\[空\]\[空

begin capacity

  • size():当前已经构造出来的元素数量
  • capacity():当前已经分配的存储空间可以容纳多少个元素
  • reserve(n):提前申请容量
  • resize(n):真正改变元素数量

std::vector<int>v;

v.reserve(100);//capacity至少为100,但是size仍然是0

v.resize(100);//size变成100,元素真正构造出来

2.迭代器

迭代器可以理解为"广义指针"

cpp 复制代码
std::vector<int> v{1, 2, 3};

for (auto it = v.begin(); it != v.end(); ++it) {
    std::cout << *it << '\n';
}

不同容器的迭代器能力不同:

迭代器类型 能力
InputIterator 只能读取、向前移动
ForwardIterator 可以多次遍历、向前移动
BidirectionalIterator 可以前后移动
RandomAccessIterator 可以 it + n、比较距离
ContiguousIterator 元素连续存储

forward_list 单项迭代器

list 双向迭代器

set/map 双向迭代器

vector/deque 随机访问迭代器

array 连续迭代器

3.迭代器失效

cpp 复制代码
std::vector<int> v{1,2,3};
auto it=v.begin();
v.push_back(4);

原因是vector扩容后,所有元素可能被搬到一块新的内存中,原来的迭代器、指针、引用都不再有效

容器 插入时稳定性
vector 扩容可能使全部迭代器失效
deque 迭代器失效规则较复杂
list 插入通常不影响其他元素迭代器
forward_list 插入通常不影响其他元素迭代器
set/map 插入通常不影响其他迭代器
unordered_map 重新哈希会使迭代器失效
flat_* 类似 vector,移动元素可能导致失效

所以修改容器之后,不要继续使用之前保存的迭代器,除非你非常清楚该容器的失效规则

二、顺序容器

顺序容器中的元素按照线性顺序排列

1.std::array(不支持大小改变,大小写在模版参里了)

头文件:#include<array>

表层概念:

std::array<T,N>是固定长度数组

std::array<int,5>a;

它和原生数组类似,但是提供了STL风格的接口:

  • size()
  • begin()
  • end()
  • front()
  • back()
  • at()
  • fill()
  • data()

底层原理:

std::array<int,5>本质上就是对对象内部直接保存五个int

array 对象

+----+----+----+----+----+

| 10 | 20 | 30 | 40 | 50 |

+----+----+----+----+----+

它不会动态分配内存

cpp 复制代码
std::array<int,5>a;   

a是局部变量,通常在栈上

cpp 复制代码
auto p=new std::array<int,5>;

通过new创建,则整个对象位于堆上

复杂度:

操作 复杂度
随机访问 O(1)
修改元素 O(1)
查找 O(n)
插入/删除 不支持改变长度
复制代码
#include <array>
#include <iostream>

int main() {
    std::array<int, 5> a{10, 20, 30, 40, 50};

    std::cout << a[2] << '\n';
    std::cout << a.at(2) << '\n';

    a.fill(0);

    for (int x : a) {
        std::cout << x << ' ';
    }
}

operator[] 越界时不会检查,at() 越界会抛出异常。

适用场景:

  • 大小编译期固定
  • 追求性能,不想有堆分配

2.std::vector

头文件:

复制代码
#include <vector>

表层概念

vector 是最常用的 STL 容器,可以理解为"自动扩容的动态数组"。

复制代码
std::vector<int> v;

特点:

  • 元素连续存储
  • 支持随机访问
  • 尾部插入很快
  • 中间插入、删除较慢
  • 可以自动扩容

底层原理

vector 通常维护一块连续内存:

复制代码
[10][20][30][空][空][空]

当容量不足时,通常会:

  1. 申请一块更大的连续空间
  2. 将旧元素移动或复制过去
  3. 销毁旧元素
  4. 释放旧内存
  5. 更新内部指针

扩容倍数由实现决定,不能依赖具体是 1.5 倍还是 2 倍。

复制代码
std::vector<int> v;

for (int i = 0; i < 1000; ++i) {
    v.push_back(i);
}

虽然某一次扩容可能需要 O(n),但整体平均下来,尾部插入是摊销 O(1)。

复杂度

操作 复杂度
v[i] O(1)
front/back(可以通过计算得到) O(1)
尾部插入 摊销 O(1)
中间插入 O(n)
尾部删除 O(1)
中间删除 O(n)
查找 O(n)
排序 O(n log n)

示例

复制代码
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v{1, 2, 3};

    v.push_back(4);
    v.emplace_back(5);

    v.insert(v.begin() + 1, 100);

    v.erase(v.begin() + 2);

    for (int x : v) {
        std::cout << x << ' ';
    }
}

reserve 的作用

如果提前知道元素数量:

复制代码
std::vector<int> v;
v.reserve(100000);

for (int i = 0; i < 100000; ++i) {
    v.push_back(i);
}

这样可以减少扩容次数。

注意:

复制代码
v.reserve(100); // 只改变 capacity
v.resize(100);  // 改变 size,并创建 100 个元素

迭代器失效

发生扩容时:

  • 所有迭代器失效
  • 所有指针失效
  • 所有引用失效

即使没有扩容,在中间插入或删除时,插入/删除位置之后的迭代器通常也会失效。

适用场景

默认优先考虑 vector:

  • 需要动态数组
  • 需要随机访问
  • 主要在尾部添加元素
  • 希望良好的缓存局部性
  • 需要把数据传给 C 接口

3.std::vector<bool>

这是 vector 的特殊版本。

复制代码
std::vector<bool> flags;

底层原理

普通的:

复制代码
std::vector<int>

通常一个元素占 4 字节。

但 vector<bool> 通常会把一个 bool 压缩成一个 bit:

复制代码
101100101010...

因此它节省空间。

但是它并不真正保存一组普通的 bool 对象。访问元素时:

复制代码
flags[0]

返回的通常不是 bool&,而是一个代理对象。

示例

复制代码
#include <vector>

int main() {
    std::vector<bool> flags(10);

    flags[3] = true;
    flags[5] = true;

    if (flags[3]) {
        // ...
    }
}

优缺点

优点:

  • 节省空间
  • 适合大量布尔标志

缺点:

  • 不是普通 vector<T> 的行为
  • 不能正常取得 bool&
  • 与模板代码组合时容易产生意外

如果你需要普通的布尔数组,可以考虑:

复制代码
std::vector<char>

4.std::deque

头文件:

复制代码
#include <deque>

deque 读作 double-ended queue,双端队列。

表层概念

deque 支持在两端高效插入和删除:

复制代码
std::deque<int> d;

d.push_front(1);
d.push_back(2);

它也支持随机访问:

复制代码
d[3];

底层原理

deque 通常不是一整块连续内存,而是:

复制代码
中央索引数组
    |
    +----> 数据块 1:[1][2][3][4]
    |
    +----> 数据块 2:[5][6][7][8]
    |
    +----> 数据块 3:[9][10][11][12]

中央索引数组保存多个数据块的地址。

因此:

  • 头部扩展不需要整体搬迁
  • 尾部扩展不需要整体搬迁
  • 随机访问时,需要先定位数据块,再定位块内偏移
  • 元素通常不连续

复杂度

操作 复杂度
随机访问 O(1)
头部插入 O(1)
尾部插入 O(1)
头部删除 O(1)
尾部删除 O(1)
中间插入 O(n)
中间删除 O(n)

示例

复制代码
#include <deque>
#include <iostream>

int main() {
    std::deque<int> d;

    d.push_back(2);
    d.push_front(1);
    d.push_back(3);

    std::cout << d[1] << '\n';

    d.pop_front();
    d.pop_back();

    for (int x : d) {
        std::cout << x << ' ';
    }
}

与 vector 的区别

特性 vector deque
内存连续 是 通常不是
尾部插入 快 快
头部插入 慢 快
随机访问 O(1) O(1)
缓存友好性 更好 略差
支持 data() 支持 不保证连续,不适合直接作为数组

适用场景

  • 需要两端插入和删除
  • 需要随机访问
  • 不要求连续内存
  • 实现任务队列、滑动窗口

5.std::list

头文件:

复制代码
#include <list>

表层概念

list 是双向链表。

每个节点保存:

  • 数据

  • 指向前一个节点的指针

  • 指向后一个节点的指针

    nullptr <- [prev | data | next] <-> [prev | data | next] -> nullptr

底层原理

典型节点结构:

复制代码
struct Node {
    Node* prev;
    Node* next;
    T value;
};

插入一个节点时,只需要修改相邻节点的指针:

复制代码
原来:

A <-> B

插入 X:

A <-> X <-> B

不需要搬动其他元素。

复杂度

操作 复杂度
访问第 n 个元素 O(n)
头部插入 O(1)
尾部插入 O(1)
已知位置插入 O(1)
已知位置删除 O(1)
查找 O(n)

示例

复制代码
#include <list>
#include <iostream>

int main() {
    std::list<int> values{1, 2, 4};

    auto it = values.begin();
    ++it;
    ++it;

    values.insert(it, 3);

    for (int x : values) {
        std::cout << x << ' ';
    }
}

list::splice

list 有一个很有特色的操作:splice。

它可以把一个链表中的节点直接转移到另一个链表中,通常不需要复制元素。

cpp 复制代码
// 1. 转移单个节点:把 it 指向的节点放到 pos 之前
void splice(const_iterator pos, list& other, const_iterator it);

// 2. 转移一段区间:把 [first, last) 区间内的节点放到 pos 之前
//    注意:last 指向的元素不包含在内
void splice(const_iterator pos, list& other,
            const_iterator first, const_iterator last);

// 3. 转移整条链表:把 other 的所有节点放到 pos 之前
void splice(const_iterator pos, list& other);

三种转移元素的方式

复制代码
#include <list>
#include <iostream>

int main() {
    std::list<int> a{1, 2, 3};
    std::list<int> b{10, 20};

    auto pos = a.begin();
    ++pos;

    a.splice(pos, b);

    for (int x : a) {
        std::cout << x << ' ';
    }
}

缺点

每个元素至少额外需要两个指针:

复制代码
数据 + prev 指针 + next 指针

而且节点分散在堆内存中,缓存局部性较差。

因此,很多情况下虽然理论上链表插入是 O(1),实际性能却可能不如 vector。

适用场景

  • 已经拥有某个位置的迭代器
  • 需要频繁在中间插入、删除
  • 需要节点稳定地址
  • 需要 splice

不要因为"插入 O(1)"就默认使用 list。如果你每次都要先遍历到插入位置,整体仍然是 O(n)。

6.std::forward_list

头文件:#include<forward_list>

表层概念

forward_list 是单向链表。

每个节点只有一个 next 指针:

复制代码
[A] -> [B] -> [C] -> nullptr

底层原理

复制代码
struct Node {
    Node* next;
    T value;
};

相比 list,它少了一个 prev 指针,因此占用空间更少。

但是它只能向前遍历,不能后退。

特殊接口

因为它是单向链表,所以主要接口是:

复制代码
before_begin()
insert_after()
erase_after()

而不是普通 list 的:

复制代码
insert()
erase()

示例

复制代码
#include <forward_list>
#include <iostream>

int main() {
    std::forward_list<int> values{1, 3, 4};

    auto it = values.before_begin();
    ++it;

    values.insert_after(it, 2);

    values.remove(4);

    for (int x : values) {
        std::cout << x << ' ';
    }
}

为什么没有 size()

forward_list 通常不提供 size() 成员函数。

因为单向链表为了节省空间,可以不额外维护元素数量。如果调用:

复制代码
std::distance(fl.begin(), fl.end());

就必须遍历整个链表,复杂度是 O(n)。

适用场景

  • 只需要单向遍历
  • 希望比 list 更节省空间
  • 频繁在已知位置后面插入或删除
  • 实现简单链表、邻接表

7.std::inplace_vector

这是较新的容器,实际使用时要看编译器和标准库实现支持情况。

标准草案中,inplace_vector 是一种连续存储、容量固定且元素直接存放在容器对象内部的容器。

表层概念

它类似:

复制代码
std::vector<T>

但最大容量在编译期确定:

复制代码
std::inplace_vector<int, 100> v;

最多存储 100 个元素。

底层原理

复制代码
inplace_vector 对象内部:

[元素存储区,最多容纳 100 个 int]

不会进行普通 vector 那样的堆扩容。

适用场景

  • 实时系统
  • 嵌入式系统
  • 不希望动态分配内存
  • 最大元素数量已知
  • 仍然需要动态改变当前 size

示意代码:

复制代码
#include <inplace_vector>

int main() {
    std::inplace_vector<int, 100> values;

    values.push_back(10);
    values.push_back(20);
}

8. std::hive

hive 是较新的块状序列容器。标准草案描述它使用多个元素块管理存储,并且删除后的空间可以被后续插入重新利用。

表层概念

它适合:

  • 元素频繁插入和删除
  • 希望元素地址尽可能稳定
  • 不要求元素按照插入顺序排列
  • 不需要随机访问

底层原理

内部大致是多个块:

复制代码
块 1:[A][B][空][D]
块 2:[E][F][G]
块 3:[H][空][J]

删除元素后,位置可能变成空洞;后续插入可以重新利用这些空位。

示例

复制代码
#include <hive>

int main() {
    std::hive<int> values;

    values.push_back(1);
    values.push_back(2);
    values.push_back(3);

    values.erase(values.begin());

    values.insert(4);
}

它适合特殊场景,不是日常代码中的默认容器。

三、有序关联容器

有序关联容器通常基于平衡搜索树实现

它们的共同特点:

  • 元素按照比较器排序
  • 查找、插入、删除通常为 O(log n)
  • 迭代器遍历结果有序
  • 插入通常不会让已有迭代器失效

标准只规定行为和复杂度,不强制要求具体使用红黑树,但绝大多数实现采用红黑树或类似平衡树。

1.std::set

头文件:

#include<set>

表层概念

set 保存唯一的键,并自动排序。

复制代码
std::set<int> s{5, 1, 3, 3};

结果是:

复制代码
1 3 5

重复的 3 只保存一次。

底层原理

典型实现是平衡二叉搜索树:

复制代码
        3
       / \
      1   5

插入元素时:

  1. 从根节点开始比较
  2. 小于当前节点则向左
  3. 大于当前节点则向右
  4. 插入后通过旋转和重新着色保持平衡

复杂度

操作 复杂度
插入 O(log n)
删除 O(log n)
查找 O(log n)
lower_bound O(log n)
遍历 O(n)

示例

复制代码
#include <set>
#include <iostream>

int main() {
    std::set<int> s{5, 1, 3, 3};

    s.insert(4);
    s.erase(1);

    if (s.contains(3)) {
        std::cout << "found\n";
    }

    auto it = s.lower_bound(4);

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

遍历结果始终按照比较器排序。

适用场景

  • 需要去重
  • 需要自动有序
  • 需要频繁查找、插入和删除
  • 不需要通过下标访问

2.std::multiset

表层概念

multiset 和 set 类似,但允许重复键。

复制代码
std::multiset<int> ms{1, 2, 2, 3, 3, 3};

内容是:

复制代码
1 2 2 3 3 3

底层原理

同样通常是平衡搜索树。

与 set 的区别只是:遇到等价键时,不拒绝插入,而是把它插入到等价元素范围中。

示例

复制代码
#include <set>
#include <iostream>

int main() {
    std::multiset<int> scores{90, 90, 80, 70};

    scores.insert(90);

    std::cout << scores.count(90) << '\n';

    auto range = scores.equal_range(90);

    for (auto it = range.first; it != range.second; ++it) {
        std::cout << *it << ' ';
    }
}

适用场景

  • 需要排序
  • 允许重复元素
  • 需要统计某个值出现次数
  • 需要查找某个值的全部范围

3.std::map

头文件:

复制代码
#include <map>

表层概念

map 保存键值对:

复制代码
key -> value

例如:

复制代码
std::map<std::string, int> ages;

可以表示:

复制代码
Alice -> 18
Bob   -> 20

每个键唯一。

底层原理

map 通常也是平衡搜索树。

节点类似:

复制代码
struct Node {
    const Key key;
    T value;
    Node* left;
    Node* right;
};

关键点:

复制代码
std::map<Key, T>::value_type

通常是:

复制代码
std::pair<const Key, T>

键是 const,因为如果允许你直接修改键,就可能破坏树的排序结构。

复杂度

操作 复杂度
插入 O(log n)
删除 O(log n)
查找 O(log n)
operator[] O(log n)
遍历 O(n)

示例

复制代码
#include <map>
#include <iostream>
#include <string>

int main() {
    std::map<std::string, int> ages;

    ages["Alice"] = 18;
    ages["Bob"] = 20;

    ages.insert({"Charlie", 25});
    ages.emplace("David", 30);

    std::cout << ages["Alice"] << '\n';

    for (const auto& [name, age] : ages) {
        std::cout << name << ": " << age << '\n';
    }
}

operator[] 的陷阱

复制代码
std::map<std::string, int> scores;

std::cout << scores["Tom"];

如果 "Tom" 不存在,operator[] 会自动插入:

复制代码
"Tom" -> 0

如果你只想查找,不希望插入,使用:

复制代码
auto it = scores.find("Tom");

或者:

复制代码
scores.at("Tom");

at() 找不到时会抛出 std::out_of_range。

try_emplace

复制代码
std::map<std::string, std::string> users;

users.try_emplace("Alice", "admin");

如果 "Alice" 已经存在,就不会构造新的值对象。

适用场景

  • 根据键查找值
  • 需要键自动排序
  • 需要 O(log n) 的稳定查找性能
  • 需要范围查询,例如查找某个区间内的键

4.std::multimap

表层概念

multimap是允许一个键对应多个值的有序映射

复制代码
课程 -> 学生
数学 -> Alice
数学 -> Bob
英语 -> Tom

示例

复制代码
#include <map>
#include <iostream>
#include <string>

int main() {
    std::multimap<std::string, std::string> courses;

    courses.emplace("Math", "Alice");
    courses.emplace("Math", "Bob");
    courses.emplace("English", "Tom");

    auto range = courses.equal_range("Math");

    for (auto it = range.first; it != range.second; ++it) {
        std::cout << it->first << ": " << it->second << '\n';
    }
}

适用场景

  • 一个键对应多个值
  • 需要按照键排序
  • 需要使用 equal_range 获取全部关联值

如果一个键对应多个值,也可以使用:

复制代码
std::map<Key, std::vector<Value>>

这种设计往往更方便管理。

四、无序关联容器

无序关联容器通常基于哈希表实现。

共同特点:

  • 不保证遍历顺序
  • 平均查找、插入、删除为 O(1)
  • 最坏情况可能退化为 O(n)
  • 通过哈希函数定位元素

1.哈希表底层原理

假设有:

复制代码
std::unordered_map<std::string, int> table;

内部可能类似:

复制代码
bucket 0 -> 节点
bucket 1 -> 节点 -> 节点
bucket 2 -> 空
bucket 3 -> 节点
bucket 4 -> 节点 -> 节点

插入键 "Alice" 时:

复制代码
hash("Alice") -> 某个整数
bucket = hash("Alice") % bucket_count

如果多个键落入同一个 bucket,就发生哈希冲突。

常见解决方式是:

  • 链地址法
  • 桶中挂链表
  • 更复杂的开放寻址或混合结构

具体实现由标准库决定。

当负载因子过高时,容器会重新哈希:

复制代码
旧桶数组 -> 新桶数组

这会重新计算所有元素的位置。

2. std::unordered_set

表层概念

unordered_set 保存唯一键,但不排序。

复制代码
std::unordered_set<int> s{5, 1, 3, 3};

其中只保存:

复制代码
1, 3, 5

但遍历顺序不确定。

复杂度

操作 平均复杂度 最坏复杂度
查找 O(1) O(n)
插入 O(1) O(n)
删除 O(1) O(n)

示例

复制代码
#include <unordered_set>
#include <iostream>

int main() {
    std::unordered_set<int> s{1, 2, 3};

    s.insert(4);
    s.erase(2);

    if (s.contains(3)) {
        std::cout << "found\n";
    }

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

适用场景

  • 只关心元素是否存在
  • 不需要排序
  • 需要平均 O(1) 查找
  • 去重

3. std::unordered_multiset

允许重复键,不保证顺序。

复制代码
#include <unordered_set>
#include <iostream>

int main() {
    std::unordered_multiset<int> values{
        1, 2, 2, 3, 3, 3
    };

    std::cout << values.count(3) << '\n';
}

适合:

  • 统计频率
  • 不需要顺序的重复元素集合
  • 只关心平均 O(1) 查找

4.std::unordered_map

相关推荐
波力海苔夹心脆6751 小时前
C# 海康威视摄像头二次开发入门:HCNetSDK 登录、实时预览、云台控制、录像与抓图(WinForms 实战)
开发语言·windows·经验分享·tcp/ip·c#
数据狐(Datafox)1 小时前
淘宝商品详情API实战:多语言代购商城自动同步数据完整方案
开发语言·前端·数据库·爬虫·json
芯次元玩家2 小时前
技术岗转物联网解决方案架构师,行业洞察、商业模式该从哪开始学?
java·开发语言
2601_962218612 小时前
C++ string 类原理、踩坑与对象语义详解
开发语言·c++
隔窗听雨眠2 小时前
FunProxy用Rust构建跨平台全链路测试抓包代理工具
开发语言·后端·rust
y = xⁿ2 小时前
关于Agent智能体开发
开发语言·前端·javascript
Zhou1411362 小时前
MyBatisPlus_02_条件构造器与高级功能
java·开发语言·python
geovindu2 小时前
rust: search
开发语言·后端·rust
爱写代码的小朋友2 小时前
Windows 下 MSYS2 + MinGW64 安装与使用 OpenCV 全指南(含踩坑记录)
c++·人工智能·windows·opencv