容器的主要用途就是对其中存储的数据进行“增删改查”。那么不同的数据结构的设计,增删改查的效率是不一样的。

顺序容器

元素按顺序存储,支持动态调整大小和顺序访问

【arry-静态数组】

存储结构---所有的元素会严格按照内存地址线性排列,使用连续的内存空间存储数据元素。

创建了arry,编译的时候就固定了大小,不支持动态扩展。比较适合大小固定并且已知大小的数据集合。

⚠️注意:对两个array执行swap会交换两个数组范围内的元素,但是不会改变内存地址,也就是不会改变两个容器各自的迭代器的依附属性。

array 和 C++语法中的[]符号无限接近,访问速度与普通数组几乎无差,只是多了 STL 接口。支持begin(), end(), front(), back(), at(), empty(), data(), fill(), swap(), ... 等等标准接口

方法 含义
operator[] / at() 随机访问元素;at 有越界检查
size() 返回固定大小(编译期常量)
front() / back() 第一个和最后一个元素
fill(value) 所有元素填充为 value
begin()/end() 返回起始/结束迭代器
cbegin()/cend() 常量迭代器(只读)
data() 返回指向原始数组的指针
swap(array) 与另一个 std::array 交换内容
empty() 如果 size 为 0,返回 true

举例:

//声明方式
std::array<int, 4> arr1 = {1, 3, 4, 9};   // 创建了一个元素为int类型,一共四个元素的确定数组
std::array<int, 4> arr2 = {};  // 创建了一个元素为int类型,一共四个元素,但是元素未知的数组
std::array<int, 4> arr3 = {5};  // 创建了一个元素为int类型,一共四个元素,但是只有第一个元素已知,其余未知的数组

//基本操作
arr1.at(2);  // 访问位置2处的元素
arr1[3];  // 访问位置3处的元素
arr1.size();  // 获取数组大小
arr1.front();  //获取第一个元素
arr1.back();  // 获取最后一个元素
arr2.fill(1);  // 全部填充1

【vector-动态数组】

存储结构---所有的元素会严格按照内存地址线性排列,使用连续的内存空间存储数据元素,这样就表明可以使用普通的++或者--指针来访问里面的元素(与arry相同)
与arry不同的地方就是它的存储空间可以自动调整,也就是vector就是能够动态调整大小的arry。

template<typename T>
class Vector {
    T* _start;     // 指向数据块起始地址
    T* _finish;    // 当前元素末尾(下一可插入位置)
    T* _end_of_storage; // 总容量末尾
};

扩容机制:vector使用动态分配的array,当现有的空间无法满足增长需求的时候,会重新分配一个更大的array(C++里面一般设置为2倍),然后把元素移动过去。也就是vector的容量一般会比实际的size大。
重新分配内存的操作步骤:
(1)分配新内存
(2)拷贝旧数据到新内存
(3)释放旧内存

优缺点---使用vector访问随即元素和尾部添加删除元素效率高,但是在尾部以外的地方增删元素效率很低,并且迭代器的一致性比较差。

方法 含义
vec.begin() 返回指向首元素的迭代器
vec.end() 返回指向末尾后一个位置的迭代器
vec.front() 返回首元素(引用)
vec.back() 返回末尾元素(引用)
vec.push_back(x) 在末尾插入一个元素
vec.pop_back() 删除末尾一个元素
vec.size() 当前元素个数
vec.empty() 返回是否为空
vec.clear() 清空所有元素

举例:

//声明方式
//底层代码提供了template<type T> ==>可以支持创建不同数据类型的动态数组
vector<int> vec;
vector<char> vec;
vector<pair<int,int> > vec;
struct node{...};
vector<node> vec;

vector<int> v1;                   // 空向量
vector<int> v2(5);                // 5 个元素,值为 0
vector<int> v3(5, 42);            // 5 个元素,每个为 42
vector<int> v4 = {1, 2, 3, 4};    // 列表初始化

【list-双向链表】

存储结构---list的底层结构是一个双向链表,每个元素存储在独立的节点中,节点之间通过前向指针和后向指针连接;

//底层结构
template<typename T>
struct Node {
    T val;
    Node* prev;
    Node* next;
};

优缺点---支持在任意位置快速的插入和删除元素,并且支持双向遍历。但是list不支持随机访问,我们要访问一个元素的话,需要从一个已知元素(begin/end)朝一个方向遍历,直到遍历到要访问的元素,此外,list会需要更多的内存空间,用来保存各个元素的关联信息。

方法 功能说明
push_back(val) 在尾部插入元素
push_front(val) 在头部插入元素
pop_back() 删除尾部元素
pop_front() 删除头部元素
insert(pos, val) 在指定位置插入元素(迭代器)
erase(pos) 删除指定位置的元素(迭代器)
remove(val) 删除所有值为 val 的元素
clear() 清空容器
size() 返回元素个数
empty() 检查是否为空
begin()/end() 获取首/尾迭代器
rbegin()/rend() 反向迭代器
sort() 排序(必须自己实现比较器)
unique() 去除连续重复元素
merge(other) 合并两个已排序的 list
reverse() 反转 list 顺序

举例:

//声明方式
list<int> l = {1, 2, 3};
std::list<int> l1; // 空链表
std::list<int> l2(5); // 5个默认值为0的int元素
std::list<int> l3(5, 10); // 5个值为10的元素
std::list<int> l5(l); // 拷贝构造

l.push_back(4);
l.push_front(0);
for (int x : l) cout << x << " ";  // 输出 0 1 2 3 4

【forward_list-单向链表】

存储结构---forward_list与list的存储结构类似,相比于 list 的核心区别是它是一个单链表,因此每个节点只存储一个后向指针。每个元素只会与相邻的下一个元素关联!
由于关联信息少了一半,因此 forward_list 占用的内存空间更小,且插入和删除的效率稍稍高于 list。作为代价,forward_list 只能单向遍历。

//底层结构
template<typename T>
struct Node {
    T value;
    Node* next;
};

?设计 forward_list 的目的是为了达到不输于任何一个C风格手写链表的极值效率!为此,forward_list 是一个最小链表设计,没有size()接口,因为内部维护一个size变量会降低增删元素的效率。所以要想知道大小就需要遍历。

总结就是:list 兼顾了接口丰富性牺牲了效率,而 forward_list 舍弃了不必要的接口只为追求极致效率。

方法 功能说明
push_front(val) 插入头部元素
pop_front() 删除头部元素
insert_after(pos, val) 在指定位置插入
erase_after(pos) 删除指定位置后的元素
remove(val) 删除所有值为 val 的元素
unique() 删除连续重复元素
sort() 排序
reverse() 反转顺序
merge(list2) 合并两个升序的 forward_list
before_begin() 获取头结点前的一个迭代器(用于插入)
begin()/end() 获取首/尾迭代器
empty() 判断是否为空
clear() 清空链表

举例:

//声明方式
forward_list<int> fl = {1, 2, 3};
std::forward_list<int> fl1; // 空单向链表
std::forward_list<int> fl2 = {0, 0, 0, 0, 0}; // 用初始化列表填充,不能写 forward_list<int> fl(5);
std::forward_list<int> fl3;
fl3.assign(5, 100); // 5个值为100的元素
std::forward_list<int> fl4(fl);  //拷贝构造

fl.push_front(0);  // 插入头部
auto it = fl.before_begin(); // 必须使用前驱节点进行插入
fl.insert_after(it, -1);
for (int x : fl) cout << x << " ";  // 输出 -1 0 1 2 3

【deque-双端队列】

存储结构---不同的库对deque的实现可能不同,但大体上都是使用分段连续内存块 + 中控表(map)的结构管理数据,内存块称为 “缓冲区”(buffer):每个 buffer 大小固定,通常 512 字节或一个指针大小的整数倍,中控结构是一个指针数组,称为 map(控制中心):它记录了所有 buffer 的地址。通过一个中控结构(map-like)连接这些块,支持前后动态扩容。vector使用的是单一的array,deque则会使用很多个离散的array来组织数据,因此vector是连续的,deque 则是分段连续,也就是deque 会维护不同 array 之间的关联信息。由于deque不保证存储区域一定是连续的,因此不能用指向元素的普通指针做++和--操作。

//底层结构
//控制结构
T** map;          // map 是指针数组,指向多个 buffer
size_t map_size;
_Deque_iterator start, finish;
template<typename T>
struct _Deque_iterator {
    T* cur;       // 当前元素指针
    T* first;     // 当前 buffer 的起始地址
    T* last;      // 当前 buffer 的末尾地址
    T** node;     // 当前 buffer 在 map 中的指针位置
};

扩容机制
(1)当我们执行 push_back() 或 push_front() 等操作时检查空间,当前 buffer 满了,就需要:申请一个新的 buffer 内存块,allocate_node(),将其指针挂到 map 的合适位置上,更新迭代器指针,如 start 或 finish。
(2)当 map 本身不够大时,分配更大的 map、拷贝旧指针、调整 start/finish 的 node 指针、删除旧 map。

支持头尾两端高效插入/删除(都是 O(1) 时间复杂度,摊还)、支持随机访问(O(1))、自动扩展容量(不需要手动扩容),但是deque 并不适合遍历。因为每次访问元素时,deque 底层都要检查是否触达了内存片段的边界,造成了额外的开销

分类 方法 描述
构造函数 deque<T> d 默认构造空队列
deque<T> d(n) n 个默认值元素
deque<T> d(n, val) n 个 val 元素
deque<T> d{...} 列表初始化
元素访问 d[i]d.at(i) 访问第 i 个元素
d.front() / d.back() 访问头部/尾部元素
修改 d.push_back(val) 末尾插入元素
d.push_front(val) 头部插入元素
d.pop_back() / pop_front() 删除尾部 / 头部元素
d.insert(pos, val) 插入元素到指定位置(迭代器)
d.erase(pos) 删除指定位置元素
d.clear() 清空容器
大小 d.size() / d.empty() 返回元素个数 / 是否为空
其他 d.swap(other) 与另一个 deque 交换
迭代器 begin(), end(), rbegin(), rend() 支持正向、反向迭代

举例:

//声明方式
deque<int> dq = {1, 2, 3};
deque<string> names;
deque<pair<int, int>> pointPairs;
deque<int> d1;              // 空
deque<int> d2(3);           // [0, 0, 0]
deque<int> d3(3, 5);        // [5, 5, 5]
deque<int> d4{1, 2, 3, 4};  // [1, 2, 3, 4]

dq.push_front(0);  // 头部插入
dq.push_back(4);   // 尾部插入
for (int x : dq) cout << x << " ";  // 输出 0 1 2 3 4

dq.pop_front();  // 删除 0
dq.pop_back();   // 删除 4
cout << "\nsize: " << dq.size();    // 输出 3

常见使用deque的题目:
滑动窗口最大值(如单调队列)
LRU 缓存淘汰策略
BFS(广度优先搜索)队列
双向模拟队列系统

【性能对比】

操作 deque vector list
随机访问 O(1) O(1) ❌ 不支持
尾部插入/删除 O(1) O(1) O(1)
头部插入/删除 O(1) O(n) O(1)
中间插入/删除 O(n) O(n) O(1)(已知位置)
内存结构 分段数组 连续内存 双向链表

关联容器

元素以键值对方式存储,对所有元素的检索都是通过元素的 key 进行的(而非元素的内存地址),实现高效查找;数据存储有序,底层实现是平衡二叉树(红黑树)

【map-键值对容器】

存储结构---map通过底层的红黑树数据结构来将所有的元素按照 key 的相对大小进行排序,所实现的排序效果也是严格弱序特性,为此,开发者需要重载 key 的<运算符或者模板参数中的 class Compare。【解释:插入、查找、删除等操作都依赖于 key 之间的大小比较。比较规则默认使用operator<,因为如果你的 key 是用户自定义类型(struct/class),STL 默认不知道怎么排序。也可以通过模板参数传入自定义的比较器。比较规则定义了 map 的排序标准,必须满足“严格弱序”的条件,才能构建稳定可靠的有序结构。】

🚫严格弱序 ≠ 全序,不要求所有元素都可以比较大小

条件 描述
非自反性 !(x < x) 必须为 true(即,元素不能小于自己)
反对称性 如果 x < y 为 true,则 y < x 必须为 false
传递性 如果 x < yy < z,则 x < z
不可比性 如果 !(x < y)!(y < x),那么 x 和 y 被认为是等价的,不是相等,而是“无法区分大小”

当 key 为自定义类型(如 struct),必须提供比较方式(operator< 或自定义比较器)。

struct Point {
    int x, y;
    bool operator<(const Point& other) const {
        return x < other.x || (x == other.x && y < other.y);
    }
};
std::map<Point, string> pointMap;  // key的类型是Point,里面自定义了比较器

优缺点---大多数情况下,map的查找、插入、删除操作都在 对数时间 O(logN) 内完成,由于底层是红黑树结构不会退化为链表形式。支持自动排序,范围查询。但是查询效率比其他的容器效率低一些,并且占用的内存比较大(使用红黑树结构+指针进行管理),删除和插入效率也不是最好的。就是更加稳定!

函数原型 说明 返回值 / 备注
size() 返回元素个数 size_t 类型
empty() 判断是否为空 truefalse
clear() 清空所有元素
insert(pair<key, value>) 插入键值对 pair<iterator, bool>
erase(key) 删除指定 key 的元素 删除数量(0 或 1)
erase(iterator) 删除指定位置元素 返回下一个有效迭代器
find(key) 查找 key 对应元素 找到返回迭代器,找不到返回 end()
count(key) 判断 key 是否存在 0 或 1
operator[] 访问或插入 key 对应的 value 如果不存在 key,则默认插入
at(key) 安全访问(若 key 不存在抛异常) 返回引用,异常:out_of_range
begin() / end() 返回迭代器(起始 / 末尾) 双向迭代器
rbegin() / rend() 反向迭代器(逆序遍历) 反向双向迭代器
lower_bound(key) 返回 >= key 的首个元素迭代器 支持区间查找
upper_bound(key) 返回 > key 的首个元素迭代器
equal_range(key) 返回 pair<lower, upper> 用于范围操作
swap(map2) 与另一个 map 交换内容
emplace(key, value) 原地构造元素(C++11起) 返回 pair<iterator, bool>
emplace_hint(pos, key, value) 提供插入位置提示的原地构造 返回 iterator

举例:

std::map<std::string, int> ageMap;
ageMap["Tom"] = 20;

【set-集合容器】

存储结构---它的底层结构是红黑树,和 map 不一样的是,set 是直接保存 value 的,或者说,set 中的 value 就是 key。set 中的元素必须是唯一的,不允许出现重复的元素,且元素不可更改,但可以自由插入或者删除。并且元素按照一定规则自动排序(默认用 < 运算符,从小到大)

set和map类似,具有稳定的插入、删除、查找操作复杂度,都是 O(log n)。 插入效率低于哈希表,无法直接访问下标,对比map,由于map需要维护pair<key, value> 结构,比单一 key 的 set稍复杂,占用空间更多。set只关心元素是否存在,map可以记录额外的关联信息。

函数原型 功能说明 返回值 / 说明
insert(val) 插入元素(自动排序+去重) pair<iterator, bool>
erase(val) 删除指定值的元素 删除个数(0或1)
erase(iterator) 删除指定位置元素 返回下一个有效迭代器
find(val) 查找元素 找到返回迭代器,未找到返回 end()
count(val) 判断某个值是否存在 返回 0 或 1
size() 返回元素个数 size_t 类型
empty() 判断容器是否为空 true / false
clear() 清空所有元素
begin() / end() 返回迭代器(起始 / 尾后) 支持前向遍历
rbegin() / rend() 反向迭代器 逆序遍历
lower_bound(val) 返回 >= val 的第一个元素 iterator
upper_bound(val) 返回 > val 的第一个元素 iterator
equal_range(val) 返回 pair<lower, upper> 可用于区间遍历
emplace(val) 原地构造插入(C++11) pair<iterator, bool>
swap(set2) 与另一个 set 交换数据

举例:

std::set<int> s1; // 创建一个空的整数 set
std::set<std::string> s2; // 创建一个空的字符串 set
std::set<int> s = {3, 1, 4, 1, 5, 9}; // 自动去重、排序 ==> s 中为:{1, 3, 4, 5, 9}

//自定义规则
struct cmp {
    bool operator()(const int &a, const int &b) const {
        return a > b; // 降序排序
    }
};
std::set<int, cmp> s = {5, 2, 8, 1}; // 输出顺序:8 5 2 1

//利用数组初始化
int arr[] = {4, 2, 5, 2, 3};
std::set<int> s1(arr, arr + 5);

std::vector<int> v = {10, 20, 10, 30};
std::set<int> s2(v.begin(), v.end());

无序容器

基于哈希表实现,以哈希表形式存储,查找性能更好,但数据无序

【unordered_map-基于哈希实现】

存储结构---map以红黑树作为底层结构组织数据,而 unordered_map 以哈希表(通常使用开链法 + vector)(hash table)作为底层数据结构来组织数据,其中哈希冲突的解决办法:默认使用开链法:一个桶存多个元素链表
优缺点

  1. unordered_map 不支持排序,在使用迭代器做范围访问时(迭代器自加自减)效率更低;
  2. 但 unordered_map 直接访问元素的速度更快(尤其在规模很大时),因为它通过直接计算 key 的哈希值来访问元素,是O(1)复杂度
    map 增删元素的效率更高,unordered_map 访问元素的效率更高,另外,unordered_map 内存占用更高,因为底层的哈希表需要预分配足量的空间。
函数原型 功能说明
insert({key, value}) 插入一个键值对(不覆盖旧值)
operator[] / at(key) 插入或访问 key 对应的 value
erase(key) / erase(it) 删除指定键或迭代器位置元素
find(key) 查找 key,返回迭代器
count(key) 判断 key 是否存在(返回 0 或 1)
size() / empty() / clear() 容器状态操作
begin() / end() 迭代器遍历
emplace(key, value) 原地构造,性能优于 insert
bucket_count() 哈希桶的数量
load_factor() 当前负载因子(元素 / 桶数)
rehash(n) 重新分配至少 n 个桶
reserve(n) 预分配空间避免频繁扩容

举例:

std::unordered_map<std::string, int> umap1;
std::unordered_map<std::string, int> umap2 = {
    {"one", 1},
    {"two", 2},
    {"three", 3}
};

使用场景:
频繁插入/查找 key-value 映射,如计数器(词频、字符统计等)
不关心元素顺序的哈希查找场景
替代 map 获取更高性能

【unordered_set-基于哈希实现】

存储结构---以哈希表(开链法)作为底层结构,每个元素在容器中唯一,不允许重复。
元素顺序不固定,由哈希函数决定。查找效率极高,平均复杂度为 O(1)。

函数原型 作用描述
insert(val) 插入元素(自动去重)
emplace(val) 原地构造,性能优
erase(val) 删除指定元素
find(val) 查找元素,返回迭代器
count(val) 返回是否存在(0或1)
size() / empty() 获取大小 / 是否为空
clear() 清空容器
begin() / end() 迭代器遍历
load_factor() 查看当前负载因子
rehash(n) / reserve(n) 手动控制桶数和容量

举例:

std::unordered_set<int> us1;
std::unordered_set<int> us2 = {4, 2, 7, 2};  // 自动去重,us = {2, 4, 7}
//自定义哈希函数初始化
struct Point {
    int x, y;
    bool operator==(const Point &p) const {
        return x == p.x && y == p.y;
    }
};
struct HashFunc {
    size_t operator()(const Point &p) const {
        return hash<int>()(p.x) ^ hash<int>()(p.y << 1);
    }
};
std::unordered_set<Point, HashFunc> points;

容器适配器

🧩作为适配器,需要指定底层容器才能实例化。

template<
    class T,                                // 数据类型
    class Container = std::deque<T>         // 底层容器类型,默认为 deque
> class stack/queue;
template<
    class T,                             // 元素类型
    class Container = vector<T>,         // 底层容器,默认是 vector
    class Compare = less<typename Container::value_type> // 比较器
> class priority_queue;

【stack-栈】

存储结构---遵循LIFO设计原则的容器适配器,也即只能从一端插入和删除;vector,list,deque 皆可作为stack的底层容器,底层容器必须要支持在尾部的插入和删除,默认使用deque;可见它并不是一种独立的数据结构,而是对底层容器(如 deque 或 vector)的一种包装,只暴露栈的操作接口。
优缺点---使用简单,操作符合栈模型,插入、删除操作效率高,可以自由选择底层容器,但是只能访问栈顶,不能随机访问,不支持遍历,无法初始化时指定初始数据内容

接口 功能描述
push(val) 压栈(插入元素)
pop() 出栈(移除栈顶元素)
top() 返回栈顶元素(不删除)
empty() 判断栈是否为空
size() 返回栈中元素数量
emplace(args...) 原地构造一个元素压栈(C++11 起)

举例:

stack<int> s1;                         // 空栈
stack<int, vector<int>> s2;           // 指定底层容器为 vector
stack<int, deque<int>> s3({1, 2, 3}); // 用 deque 初始化

常见使用场景
括号匹配、表达式求值
DFS遍历(非递归写法)
浏览器前进/后退功能实现
压栈回溯、状态保存等算法逻辑

【queue-队列】

存储结构---遵循FIFO设计原则的容器适配器,也即只能从一端插入、从另一端删除;vector,list,deque 皆可作为queue的底层容器,默认为 deque
优缺点---符合 FIFO 的使用需求,接口简单,操作直观,可自由选择底层容器;但是不能随机访问内部元素,不能遍历,插入删除只能在队尾/队首

成员函数 说明
push(x) 入队:将元素插入队尾
pop() 出队:删除队首元素
front() 返回队首元素
back() 返回队尾元素
empty() 判断队列是否为空
size() 返回队列中的元素个数
emplace(x) 原地构造一个元素到队尾(C++11)

举例:

//声明
std::queue<int> q1;                                // 默认构造,使用 deque
std::deque<int> d = {1, 2, 3};
std::queue<int> q2(d);                             // 用已有 deque 初始化
std::queue<int, std::list<int>> q3;                // 指定底层容器为 list

使用场景
任务调度:生产者-消费者模型
宽度优先搜索(BFS)
消息队列、事件队列
控制流程排队(如打车、排号系统)

【priority_queue-优先队列(默认大顶堆)】

存储结构---遵循最高优先级先出;priority_queue在内部维护一个基于二叉树的大顶堆数据结构,在这个数据结构中,最大的元素始终位于堆顶部,且只有堆顶部的元素才能被访问和获取,默认使用 vector 作为底层容器,也就是在 vector 上使用堆算法将 vector 中元素构造成大顶堆的结构。核心特点在于其严格弱序特性:即priority_queue保证容器中的第一个元素始终是所有元素中最大的,每当有新元素进入,它都会根据既定的排序规则找到优先级最高的元素,并将其移动到队列的队头。
优缺点---获取最大(或最小)元素效率高,插入/删除元素复杂度是 O(logN),灵活的比较器支持自定义优先级;不支持遍历,不支持随机访问,不能直接修改堆中已有元素的值

①堆中某个节点的值总是大于或等于其父节点的值,称为最小堆/小根堆
②堆中某个节点的值总是小于或等于其父节点的值,称为最大堆/大根堆
③堆总是一棵完全二叉树

成员函数 作用描述
push(x) 插入元素到堆中
pop() 移除堆顶元素
top() 获取堆顶元素(优先级最高)
empty() 判断堆是否为空
size() 返回堆中元素数量
emplace(args...) 原地构造元素(C++11)

举例:

priority_queue<int> pq1;  //默认最大堆
priority_queue<int, vector<int>, greater<int>> minHeap;  //最小堆实现方式

//自定义类型的优先级队列
struct Node {
    int val;
    int priority;
};
struct cmp {
    bool operator()(const Node& a, const Node& b) {
        return a.priority < b.priority; // priority大的优先
    }
};
priority_queue<Node, vector<Node>, cmp> pq2;

使用场景
堆排序
Dijkstra 最短路径算法
A* 搜索算法
任务调度系统
Top-K 问题