一、容器的本质与学习价值(什么是容器?为什么要学它?)
在编程时,我们常会遇到需要处理批量数据的场景——可能是一组数字、一串字符串,也可能是多个自定义对象。这时如果直接用零散的变量来存储,不仅会让代码充斥着重复的声明与赋值,还得手动编写循环、判断等逻辑来处理数据的增删、查找,既繁琐又容易出错。
C++ 的 “容器” 正是为解决这类问题设计的:它是标准库(STL)提供的数据结构模板,通过封装数据的存储逻辑和操作方法,让开发者得以从 “如何存储数据” 的细节中解放出来,专注于 “如何使用数据”。无需手动实现链表、哈希表等复杂底层结构,只需调用容器提供的现成接口,就能轻松完成数据的增删查改。
简单说:容器 = 数据的 “存储空间” + 预设的 “管理工具”(自动处理增删查改)。容器是 “数据结构的工业化实现”,比如用vector
存班级成绩,不用手动分配数组大小;用map
存通讯录,不用写循环查找联系人 —— 这些细节容器都帮我们做好了。
学习容器的核心价值:
- 提高开发效率:用成熟的容器替代重复开发,减少代码量和出错概率;不用重复造轮子,不用自己手写链表、哈希表这些复杂数据结构;
- 保证程序性能:标准库容器经过严格优化、千锤百炼,执行效率远高于手写数据结构,比自己写的代码运行更快;
- 适配实战需求和各种场景:所有 C++ 项目都会用到容器,是进阶编程的必备基础;同时不管是存数据、查数据还是按规则处理数据,都有对应的容器可用。
容器本质上是 C++ 标准库中定义的模板类(template class)。具体来说:它们是用类模板实现的,因此能存储各种类型的元素(比如
vector<int>
存储整数,list<string>
存储字符串,甚至map<int, MyClass>
存储自定义对象)。作为类,它们封装了数据(元素的存储)和操作数据的方法(如push_back
、find
、size
等成员函数),符合面向对象的设计思想。开发者通过创建容器类的实例(如vector<int> nums;
)来使用它们,就像使用自定义类的对象一样。简单说,容器是 “能存储批量数据的通用类”,模板特性让它们能适配各种数据类型,而类的封装性让它们能提供统一、易用的操作接口。容器的设计意义在于为开发者提供高效、通用、可靠的数据管理方案,解决批量数据存储和操作的核心问题。具体来说,它的价值体现在三个维度:
1. 屏蔽底层复杂性,降低开发成本
计算机处理批量数据时,需要底层数据结构(如数组、链表、哈希表、树等)的支撑,但这些结构的实现细节复杂且容易出错(比如手动管理链表指针、处理哈希冲突、实现红黑树平衡等)。
容器通过封装底层细节,将复杂的数据结构转化为简单的接口(如push_back
添加元素、find
查找元素),让开发者无需关心 “数据如何存储”,只需专注 “如何使用数据”。例如:
用vector
时,不用手动实现动态扩容的数组; 用map
时,不用自己编写红黑树来保证有序性; 用unordered_map
时,不用处理哈希函数设计和冲突解决。
这相当于 “把复杂的轮子做好,让开发者直接用轮子造车”,大幅减少重复劳动和出错概率。2. 统一操作接口,提升代码一致性
不同数据结构的原生操作方式差异很大(比如数组用下标访问,链表用指针遍历,树用递归查找),但容器通过标准化接口(如begin()
/end()
遍历、size()
获取长度、insert()
插入元素),让所有容器的使用逻辑保持一致。
这种一致性带来两个好处:降低学习成本:学会一套通用规则,就能操作所有容器(比如用范围 for 循环遍历vector
、list
、set
的语法完全相同); 提高代码可读性:无论使用哪种容器,其他开发者都能通过熟悉的接口快速理解代码逻辑。
每种容器都在 “易用性” 和 “性能” 之间找到平衡 —— 既保证接口简单,又通过底层优化(如vector
的连续内存、unordered_map
的哈希表)确保高效运行。3. 适配场景需求,平衡性能与易用性
不同业务场景对数据操作的需求不同:有的需要快速按序号访问(如学生成绩表);有的需要频繁在中间插入删除(如日程安排);有的需要通过关键词快速查找(如通讯录);有的需要按优先级处理(如任务调度)。
容器通过多样化的设计,为每种场景提供最优解:用vector
满足 “快速下标访问 + 尾部操作”;用list
满足 “频繁中间增删”;用unordered_map
满足 “快速键值查找”;用priority_queue
满足 “优先级排序”。
每种容器都在 “易用性” 和 “性能” 之间找到平衡 —— 既保证接口简单,又通过底层优化(如vector
的连续内存、unordered_map
的哈希表)确保高效运行。一句话总结,容器是 “数据结构的工业化实现”:它把程序员从重复开发底层结构的工作中解放出来,用统一、可靠的方式管理数据,让开发者能更专注于业务逻辑,同时保证程序的性能和可维护性。这也是为什么容器成为 C++ 标准库的核心 —— 它是 “高效编程” 的基础设施。
二、容器的分类体系(容器家族图谱,一张图看懂所有成员)
C++ 容器按功能可分为三大类,就像三个工具套装,各有各的专长。每类都有明确的适用场景:
容器
├─ 顺序容器(按插入顺序存储,元素有位置关系)
│ ├─ vector:动态数组(最常用,采用下标访问高效,访问快)
│ ├─ deque:双端队列(两端插入/删除效率高,操作快)
│ ├─ list:双向链表(任意位置插入/删除高效)
│ └─ forward_list:单向链表(轻量版,省内存,仅支持单向遍历)
│
├─ 关联容器(按“键”存储元素,支持快速查找)
│ ├─ 有序关联容器(基于红黑树,自动排序)
│ │ ├─ set:元素唯一,按值排序(集合)
│ │ ├─ map:键值对,键唯一,按键排序(映射)
│ │ ├─ multiset:元素可重复,按值排序
│ │ └─ multimap:键可重复,按键排序
│ └─ 无序关联容器(基于哈希表,不排序)
│ ├─ unordered_set:元素唯一,哈希存储(查找更快)
│ └─ unordered_map:键值对,键唯一,哈希存储(查找更快)
│
└─ 容器适配器(简化版容器,适配特定场景:简化接口的专用容器,基于其他容器实现)
├─ stack:栈(先进后出,LIFO)
├─ queue:队列(先进先出,FIFO)
└─ priority_queue:优先队列(按优先级出队)
三、容器通用语法基础(所有容器都认的规则)
所有容器都遵循以下基本规则,是使用容器的 “通用语言”:
1. 头文件与命名空间
- 必须包含对应容器的头文件(如
#include <vector>
、#include <map>
); - 容器属于
std
命名空间,使用时需加std::
前缀(或通过using namespace std;
简化)。
#include <vector> // 包含vector头文件
using namespace std; // 后续使用容器可省略std::
2. 容器的定义与初始化
基本定义格式
容器类型<元素类型> 容器名;
// 示例:vector<int> scores; // 定义存储int类型的vector,名为scores
常用初始化方式(以 vector 为例)
初始化方式 | 示例代码 | 说明 |
---|---|---|
默认初始化 | vector<int> v; | 创建空容器,后续可动态添加元素 |
列表初始化 | vector<int> v = {1, 2, 3}; | 用初始化列表赋值(C++11 及以上支持) |
数量 + 初始值 | vector<int> v(5, 0); | 创建含 5 个元素的容器,每个元素初始化为 0 |
复制初始化 | vector<int> v2(v1); | 复制 v1 的所有元素到 v2 |
范围初始化 | vector<int> v(v1.begin(), v1.end()); | 复制 v1 中从 begin 到 end 的元素 |
3. 迭代器:容器的 “遍历工具”
迭代器是访问容器元素的 “通用指针”,所有容器都支持通过迭代器遍历元素。
迭代器定义
// 可修改元素的迭代器
容器类型<元素类型>::iterator 迭代器名;
// 只读迭代器(不能修改元素)
容器类型<元素类型>::const_iterator 迭代器名;
迭代器核心操作
begin()
:返回指向容器第一个元素的迭代器;end()
:返回指向 “最后一个元素后一位” 的迭代器(作为遍历结束标记);*it
:访问迭代器指向的元素;++it
:迭代器向后移动一位(指向 next 元素)。
遍历示例
vector<int> v = {1, 2, 3};
// 方式1:普通迭代器(可修改元素)
for (vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
*it *= 2; // 修改元素为2, 4, 6
}
// 方式2:auto简化迭代器类型(C++11及以上)
for (auto it = v.begin(); it != v.end(); ++it) {
cout << *it << " "; // 输出:2 4 6
}
// 方式3:范围for循环(最简洁,C++11及以上)
for (int num : v) {
cout << num << " "; // 输出:2 4 6
}
4. 容器通用成员函数
所有容器都支持的基础操作:
函数名 | 功能描述 | 示例(vector<int> v) |
---|---|---|
size() | 返回容器中元素的个数 | int n = v.size(); |
empty() | 判断容器是否为空(空则返回 true) | if (v.empty()) { /* 容器为空时的操作 */ } |
clear() | 清空容器中所有元素(size 变为 0) | v.clear(); |
begin() /end() | 返回指向首元素 / 尾后位置的迭代器 | auto it = v.begin(); |
四、顺序容器详解(像 “排队的盒子”,数据按插入顺序排列)
顺序容器的元素按插入顺序排列,有明确的 “位置” 概念,适合按顺序处理数据。
特点:数据有明确的先后顺序,就像排队一样,每个元素有自己的位置(下标)。
1. vector(动态数组)
相当于 “可伸缩的抽屉”。一开始可以指定大小,不够了会自动扩容。
✅ 优点:通过下标(比如vec[0]
)访问元素特别快,适合频繁读数据、少改动的场景。
❌ 缺点:如果在中间插入 / 删除元素,后面的元素都要挪动位置,效率低。
比如:存班级同学的成绩,查分数时直接按学号(下标)找,很方便。
核心特点
- 内存连续(类似可自动扩容的数组);
- 支持下标访问(
v[i]
),效率极高(O (1)); - 尾部插入 / 删除(
push_back
/pop_back
)高效(O (1)); - 中间 / 头部插入 / 删除低效(需挪动元素,O (n))。
专属语法与操作
操作 | 函数 / 语法 | 示例代码 | 说明 |
---|---|---|---|
元素访问 | 下标运算符 /at() | v[0] = 10; v.at(1) = 20; | at() 会检查下标是否越界,越界时抛出异常 |
访问首尾元素 | front() /back() | int first = v.front(); | 快速获取第一个 / 最后一个元素 |
尾部添加元素 | push_back(值) | v.push_back(30); | 在容器末尾新增一个元素 |
尾部删除元素 | pop_back() | v.pop_back(); | 删除容器最后一个元素(无返回值) |
任意位置插入 | insert(迭代器, 值) | v.insert(v.begin() + 1, 15); | 在迭代器指向的位置插入元素 |
任意位置删除 | erase(迭代器) | v.erase(v.begin()); | 删除迭代器指向的元素 |
调整容器大小 | resize(新大小) | v.resize(5); | 若新大小大于当前 size,新增元素用默认值填充 |
容量管理 | capacity() /reserve() | v.reserve(100); | capacity() 返回当前容量,reserve(n) 预留至少 n 个元素的容量(避免频繁扩容) |
与数组的转换
vector 内存连续,可直接转换为 C 风格数组:
vector<int> v = {1, 2, 3};
int* arr = v.data(); // 获取首元素地址(等价于&v[0])
// 此时arr可作为普通数组使用(如arr[0] = 10 → v[0]也会变为10)
完整示例
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<int> v = {1, 2, 3}; // 初始化:[1, 2, 3]
v.push_back(4); // 尾部添加:[1, 2, 3, 4]
v.insert(v.begin() + 2, 9); // 在位置2插入9:[1, 2, 9, 3, 4]
v.erase(v.begin() + 1); // 删除位置1的元素:[1, 9, 3, 4]
cout << "第一个元素:" << v.front() << endl; // 输出:1
cout << "元素个数:" << v.size() << endl; // 输出:4
// 转换为数组
int* arr = v.data();
cout << "数组第2个元素:" << arr[1] << endl; // 输出:9
// 遍历
for (int num : v) {
cout << num << " "; // 输出:1 9 3 4
}
return 0;
}
适用场景:需要频繁按下标访问、尾部增删的场景(如成绩表、动态数组)。
2. deque(双端队列)
像 “两头都能进出的队伍”。可以在最前面或最后面快速加 / 删元素。
✅ 优点:两端操作效率高,比 vector 更适合频繁在开头加数据的场景。
比如:实现一个队列,需要频繁从队头取元素、队尾加元素。
核心特点
- 内存分段连续(由多个连续内存块组成);
- 支持下标访问(效率略低于 vector);
- 头部和尾部插入 / 删除(
push_front
/pop_front
)均高效(O (1)); - 中间插入 / 删除低效(O (n))。
专属语法(与 vector 的差异)
#include <deque>
#include <iostream>
using namespace std;
int main() {
deque<string> dq;
// 头部操作(vector不支持)
dq.push_front("A"); // 头部添加:["A"]
dq.push_front("B"); // 头部添加:["B", "A"]
dq.pop_front(); // 头部删除:["A"]
// 其他操作与vector类似
dq.push_back("C"); // 尾部添加:["A", "C"]
dq[1] = "D"; // 修改下标1的元素:["A", "D"]
// 遍历
for (const string& s : dq) {
cout << s << " "; // 输出:A D
}
// 转换为数组(因内存分段,需先复制到vector)
vector<string> temp(dq.begin(), dq.end());
string* arr = temp.data(); // 数组:["A", "D"]
return 0;
}
适用场景:需要在两端频繁插入 / 删除的场景(如双端队列、缓冲区)。
3. list(双向链表)
相当于 “一串用绳子连起来的珠子”。每个元素都带着前后元素的 “地址”,元素本身不连续存放。
✅ 优点:在中间任意位置插入 / 删除元素特别快(只需解开绳子重新连),不用挪动其他元素。
❌ 缺点:不能通过下标访问,必须从头一个个找,读数据效率低。
比如:存一个频繁需要插入、删除中间元素的列表(如日程表中频繁调整事项)。
核心特点
- 内存不连续,元素通过前后指针连接;
- 不支持下标访问;
- 任意位置插入 / 删除(包括中间、头部、尾部)均高效(O (1));
- 遍历效率低于 vector(因内存不连续,缓存命中率低)。
专属语法与操作
操作 | 函数 / 语法 | 示例代码 | 说明 |
---|---|---|---|
头部添加 / 删除 | push_front() /pop_front() | l.push_front(0); | 在头部插入 / 删除元素 |
尾部添加 / 删除 | push_back() /pop_back() | l.pop_back(); | 在尾部插入 / 删除元素 |
任意位置插入 | insert(迭代器, 值) | l.insert(it, 2); | 在迭代器指向的位置插入元素 |
任意位置删除 | erase(迭代器) | l.erase(it); | 删除迭代器指向的元素 |
反转链表 | reverse() | l.reverse(); | 反转整个链表的元素顺序 |
排序 | sort() | l.sort(); | 对链表元素按升序排序(默认) |
完整示例
#include <list>
#include <vector> // 用于转换为数组
#include <iostream>
using namespace std;
int main() {
list<int> l = {1, 3, 5}; // 初始化:1 → 3 → 5
// 中间插入(比vector高效)
auto it = l.begin();
++it; // 指向3
l.insert(it, 2); // 插入2:1 → 2 → 3 → 5
// 头部添加
l.push_front(0); // 0 → 1 → 2 → 3 → 5
// 反转与排序
l.reverse(); // 5 → 3 → 2 → 1 → 0
l.sort(); // 0 → 1 → 2 → 3 → 5
// 遍历
cout << "链表元素:";
for (int num : l) {
cout << num << " "; // 输出:0 1 2 3 5
}
// 转换为数组(需复制到vector)
vector<int> temp(l.begin(), l.end());
int* arr = temp.data(); // 数组:[0, 1, 2, 3, 5]
return 0;
}
适用场景:需要频繁在中间插入 / 删除元素的场景(如日程表、动态列表)。
4. forward_list(单向链表)
比 list 更简单,像 “只能往前串的珠子”,每个元素只知道下一个是谁,不知道上一个。
✅ 优点:更省内存,适合只需要从前往后遍历、且频繁改中间元素的场景。
核心特点
- 内存不连续,元素仅通过 “下一个指针” 连接,只能单向遍历;
- 比 list 更节省内存(无需存储 “前向指针”);
- 功能有限(无
reverse
、size()
等函数); - 头部插入 / 删除高效,中间插入 / 删除需通过
insert_after
/erase_after
操作。
专属语法与操作
操作 | 函数 / 语法 | 示例代码 | 说明 |
---|---|---|---|
头部添加 / 删除 | push_front() /pop_front() | fl.push_front(1); | 在头部插入 / 删除元素 |
指定位置后插入 | insert_after(迭代器, 值) | fl.insert_after(it, 2); | 在迭代器指向元素的后面插入值 |
指定位置后删除 | erase_after(迭代器) | fl.erase_after(it); | 删除迭代器指向元素后面的元素 |
排序 | sort() | fl.sort(); | 按升序排序(默认) |
完整示例
#include <forward_list>
#include <vector>
#include <iostream>
using namespace std;
int main() {
forward_list<int> fl = {3, 1, 5}; // 单向链表:3 → 1 → 5
// 头部添加
fl.push_front(0); // 0 → 3 → 1 → 5
// 在指定元素后插入(需先定位到目标元素)
auto it = fl.begin(); // 指向0
++it; // 指向3
fl.insert_after(it, 2); // 在3后面插入2:0 → 3 → 2 → 1 → 5
// 删除元素(删除2后面的1)
it = fl.begin(); // 重新定位:指向0
++it; // 指向3
++it; // 指向2
fl.erase_after(it); // 删除2后面的元素(1):0 → 3 → 2 → 5
// 排序
fl.sort(); // 0 → 2 → 3 → 5
// 遍历(只能单向)
cout << "单向链表元素:";
for (int num : fl) {
cout << num << " "; // 输出:0 2 3 5
}
// 转换为数组(复制到vector)
vector<int> temp;
for (int num : fl) {
temp.push_back(num);
}
int* arr = temp.data(); // 数组:[0, 2, 3, 5]
return 0;
}
适用场景:内存受限,且只需单向遍历的场景(如简单的流程步骤记录、轻量级数据链)。
顺序容器对比表
容器类型 | 下标访问 | 尾部增删效率 | 头部增删效率 | 中间增删效率 | 内存连续性 | 转数组方式 |
---|---|---|---|---|---|---|
vector | 支持(快) | O(1) | O(n) | O(n) | 连续 | 直接用data() 或&v[0] 获取数组首地址 |
deque | 支持(较快) | O(1) | O(1) | O(n) | 分段连续 | 复制到 vector 后,用data() 转数组 |
list | 不支持 | O(1) | O(1) | O(1) | 不连续 | 复制到 vector 后,用data() 转数组 |
forward_list | 不支持 | O(1) | O(1) | O(1) | 不连续 | 复制到 vector 后,用data() 转数组 |
五、关联容器详解(像 “带标签的盒子”,数据按 “关键词” 排序或查找)
关联容器通过 “键” 组织元素,支持快速查找,适合 “通过关键词定位数据” 的场景。
特点:元素不是按插入顺序排,而是按某个 “关键词”(比如数值大小、字符串内容)自动排序或组织,方便快速查找。
1. 有序关联容器(set/map 系列)
基于红黑树实现,元素自动按 “键” 排序(默认升序),查找、插入、删除效率均为 O (log n)。
(1)set(集合)
相当于 “自动去重且排好序的抽屉”。里面的元素不重复,且会自动按从小到大(默认)排好。
✅ 优点:查找某个元素是否存在特别快,还能自动去重。
比如:存一个班级的 unique 学号(不重复),想快速查某个学号是否存在。
- 存储唯一元素(自动去重),元素本身即 “键”,按值升序排列。
#include <set>
#include <vector>
#include <iostream>
using namespace std;
int main() {
set<int> s = {3, 1, 2, 2}; // 自动去重+排序:{1, 2, 3}
s.insert(4); // 插入元素4:{1, 2, 3, 4}
auto it = s.find(2); // 查找元素2(返回迭代器)
if (it != s.end()) {
cout << "找到元素:" << *it << endl; // 输出:找到元素:2
}
s.erase(3); // 删除元素3:{1, 2, 4}
// 遍历(按升序)
cout << "set元素:";
for (int num : s) {
cout << num << " "; // 输出:1 2 4
}
// 转换为数组(复制到vector)
vector<int> temp(s.begin(), s.end());
int* arr = temp.data(); // 数组:[1, 2, 4]
return 0;
}
(2)map(映射)
像 “带目录的笔记本”,每个元素是 “键 - 值对”(比如 “名字 - 成绩”),按键自动排序,通过 “键” 能快速找到 “值”。
✅ 优点:通过关键词(键)查数据特别快,比如用名字查成绩、用 ID 查用户信息。
比如:存学生的 “姓名 - 电话” 对照表,想找 “张三” 的电话,直接用名字查。
- 存储键值对(
key-value
),键唯一且按升序排列,通过键快速访问值。
#include <map>
#include <vector>
#include <iostream>
using namespace std;
int main() {
// 键:姓名(string),值:年龄(int)
map<string, int> person = {{"张三", 18}, {"李四", 19}};
person["张三"] = 19; // 修改键"张三"对应的值为19
person["王五"] = 20; // 新增键值对{"王五", 20}
// 查找键"李四"
auto it = person.find("李四");
if (it != person.end()) {
cout << it->first << "的年龄:" << it->second << endl; // 输出:李四的年龄:19
}
// 遍历(按键升序)
cout << "map元素:";
for (auto& pair : person) {
cout << pair.first << ":" << pair.second << " ";
// 输出:李四:19 王五:20 张三:19(按姓名首字母排序)
}
// 转换为数组(提取键或值到vector)
vector<string> names; // 存储键(姓名)
vector<int> ages; // 存储值(年龄)
for (auto& pair : person) {
names.push_back(pair.first);
ages.push_back(pair.second);
}
string* nameArr = names.data(); // 姓名数组:["李四", "王五", "张三"]
int* ageArr = ages.data(); // 年龄数组:[19, 20, 19]
return 0;
}
(3)multiset/multimap(允许重复)
是 set 和 map 的 “允许重复版”。multiset 可以有重复元素,multimap 可以有相同的键。
比如:multiset 存多个重复的分数(如统计成绩分布),multimap 存 “部门 - 员工”(一个部门可以有多个员工)。
multiset
:允许元素重复(如统计多个相同分数),按值升序排列;multimap
:允许键重复(如 “部门 - 员工” 的一对多关系),按键升序排列。
#include <multimap>
#include <iostream>
using namespace std;
int main() {
// 键:部门,值:员工(允许一个部门有多名员工)
multimap<string, string> dept;
dept.insert({"研发部", "张三"});
dept.insert({"研发部", "李四"});
dept.insert({"市场部", "王五"});
// 查找研发部所有员工
auto range = dept.equal_range("研发部"); // 返回键为"研发部"的元素范围
cout << "研发部员工:";
for (auto it = range.first; it != range.second; ++it) {
cout << it->second << " "; // 输出:张三 李四
}
return 0;
}
2. 无序关联容器(unordered_set/unordered_map)(不排序,基于哈希)
基于哈希表实现,元素不排序,查找、插入、删除效率更高(平均 O (1)),但不支持按顺序遍历。
- 形象理解:像 “哈希表” 形式的收纳盒,数据不排序,但查起来比有序容器更快(平均时间更短)。
- 核心特点:
- 基于哈希表实现,元素无序,查找、增删效率极高(平均 O (1))。
- 缺点是不支持按顺序遍历(因为无序)。
- 适用场景:只需要快速操作,不关心顺序(如存大量数据的快速查询)。
#include <unordered_map>
#include <iostream>
using namespace std;
int main() {
// 键:学号,值:姓名(无序存储)
unordered_map<int, string> idToName;
idToName[1001] = "张三";
idToName[1002] = "李四";
idToName[1003] = "王五";
// 快速查找
cout << "学号1002对应的姓名:" << idToName[1002] << endl; // 输出:李四
// 遍历(顺序不确定)
cout << "无序map元素:";
for (auto& pair : idToName) {
cout << pair.first << ":" << pair.second << " ";
// 输出示例:1003:王五 1001:张三 1002:李四(顺序不固定)
}
return 0;
}
关联容器对比表
容器类型 | 元素类型 | 键是否唯一 | 是否排序 | 查找效率 | 适用场景 |
---|---|---|---|---|---|
set | 单一元素 | 是 | 是 | O(log n) | 去重 + 有序遍历(如唯一标签集合) |
map | 键值对 | 是 | 是 | O(log n) | 键值映射 + 有序遍历(如字典) |
multiset | 单一元素 | 否 | 是 | O(log n) | 允许重复 + 有序遍历(如成绩统计) |
multimap | 键值对 | 否 | 是 | O(log n) | 一对多映射 + 有序遍历(如部门 - 员工) |
unordered_set | 单一元素 | 是 | 否 | O(1) | 去重 + 快速查找(不关心顺序) |
unordered_map | 键值对 | 是 | 否 | O(1) | 键值映射 + 快速查找(不关心顺序) |
六、容器适配器详解(“改造过的盒子”,限制了操作方式)
容器适配器是对现有容器的封装,接口更简洁,专为特定场景设计。
特点:基于上面的顺序容器改造而来,只提供特定的操作方式,更符合某些场景的逻辑。
1. stack(栈)
像 “叠盘子”,只能从最上面放(压栈)和取(弹栈),“先进后出”。
比如:实现网页的 “后退” 功能,每打开新页面就压栈,后退就弹栈
- 逻辑:先进后出(LIFO,Last In First Out),如同叠放的盘子,只能操作顶端元素。
核心操作
操作 | 函数 / 语法 | 说明 |
---|---|---|
入栈 | push(值) | 在栈顶添加元素 |
出栈 | pop() | 删除栈顶元素(无返回值,需先判断非空) |
访问栈顶 | top() | 获取栈顶元素的值(需先判断非空) |
判断空栈 | empty() | 栈为空时返回 true |
元素个数 | size() | 返回栈中元素的数量 |
完整示例
#include <stack>
#include <iostream>
using namespace std;
int main() {
stack<int> s;
// 入栈
s.push(10);
s.push(20);
s.push(30); // 栈:[10, 20, 30](30在顶)
cout << "栈顶元素:" << s.top() << endl; // 输出:30
cout << "栈中元素个数:" << s.size() << endl; // 输出:3
// 出栈
s.pop(); // 移除栈顶元素30 → 栈:[10, 20]
cout << "出栈后栈顶:" << s.top() << endl; // 输出:20
// 转换为数组(需弹出所有元素,顺序与入栈相反)
vector<int> temp;
while (!s.empty()) {
temp.push_back(s.top());
s.pop();
}
int* arr = temp.data(); // 数组:[20, 10]
return 0;
}
适用场景:括号匹配、函数调用栈、深度优先搜索(DFS)。
2. queue(队列)
像 “排队买票”,只能从队尾加人,队头出人,“先进先出”。
比如:处理打印任务,按顺序一个个来。
- 逻辑:先进先出(FIFO,First In First Out),如同排队买票,先到先处理。
核心操作
操作 | 函数 / 语法 | 说明 |
---|---|---|
入队 | push(值) | 在队尾添加元素 |
出队 | pop() | 删除队头元素(无返回值,需先判断非空) |
访问队头 | front() | 获取队头元素的值(需先判断非空) |
访问队尾 | back() | 获取队尾元素的值(需先判断非空) |
判断空队列 | empty() | 队列为空时返回 true |
元素个数 | size() | 返回队列中元素的数量 |
完整示例
#include <queue>
#include <iostream>
using namespace std;
int main() {
queue<string> q;
// 入队
q.push("任务1");
q.push("任务2");
q.push("任务3"); // 队列:[任务1, 任务2, 任务3](任务1在头)
cout << "队头任务:" << q.front() << endl; // 输出:任务1
cout << "队尾任务:" << q.back() << endl; // 输出:任务3
cout << "任务总数:" << q.size() << endl; // 输出:3
// 出队
q.pop(); // 移除队头任务1 → 队列:[任务2, 任务3]
cout << "出队后队头:" << q.front() << endl; // 输出:任务2
// 转换为数组(弹出所有元素,顺序与入队一致)
vector<string> temp;
while (!q.empty()) {
temp.push_back(q.front());
q.pop();
}
string* arr = temp.data(); // 数组:[任务2, 任务3]
return 0;
}
适用场景:任务调度、消息队列、广度优先搜索(BFS)。
3. priority_queue(优先队列)
像 “按优先级排队”,每次取出的都是当前 “最大” 或 “最重要” 的元素(比如数值最大的)。内部是 “堆” 结构,每次top
取的是优先级最高的元素(默认最大)。
比如:医院急诊,病情越重(优先级越高)的病人先处理。
- 逻辑:每次出队的是 “优先级最高” 的元素(默认最大元素优先,可自定义优先级)。
核心操作
操作 | 函数 / 语法 | 说明 |
---|---|---|
入队 | push(值) | 添加元素,自动按优先级排序 |
出队 | pop() | 删除优先级最高的元素(需先判断非空) |
访问队头 | top() | 获取优先级最高的元素(需先判断非空) |
判断空队列 | empty() | 队列为空时返回 true |
元素个数 | size() | 返回队列中元素的数量 |
完整示例(默认最大优先)
#include <queue>
#include <iostream>
using namespace std;
int main() {
priority_queue<int> pq; // 默认:值越大,优先级越高
// 入队
pq.push(3);
pq.push(1);
pq.push(5); // 内部按优先级排序:[5, 3, 1](5在顶)
cout << "最高优先级元素:" << pq.top() << endl; // 输出:5
// 出队
pq.pop(); // 移除5 → 队列:[3, 1]
cout << "出队后最高优先级:" << pq.top() << endl; // 输出:3
// 转换为数组(弹出元素,顺序为优先级从高到低)
vector<int> temp;
while (!pq.empty()) {
temp.push_back(pq.top());
pq.pop();
}
int* arr = temp.data(); // 数组:[3, 1]
return 0;
}
自定义优先级(最小优先)
#include <queue>
#include <vector> // 用于指定底层容器
#include <iostream>
using namespace std;
int main() {
// 定义最小值优先的优先队列(需指定底层容器和比较器)
priority_queue<int, vector<int>, greater<int>> minPq;
minPq.push(3);
minPq.push(1);
minPq.push(5); // 内部排序:[1, 3, 5](1在顶)
cout << "最高优先级元素(最小):" << minPq.top() << endl; // 输出:1
return 0;
}
适用场景:急诊调度(病情重者优先)、任务优先级排序、Top K 问题。
七、容器选择指南(新手必看)
选择容器的核心原则是 “按需选择”,根据数据操作的核心需求匹配容器特性:
核心需求场景 | 推荐容器 | 选择理由 |
---|---|---|
按下标访问、尾部增删频繁 | vector | 下标访问效率最高,尾部操作 O (1) |
两端增删频繁 | deque | 头部和尾部操作均为 O (1) |
中间增删频繁 | list | 无需挪动元素,增删效率 O (1) |
内存受限 + 单向遍历 | forward_list | 比 list 更省内存,适合单向访问 |
去重 + 需要有序遍历 | set | 自动去重且按值排序 |
键值映射 + 需要有序遍历 | map | 按键排序,支持顺序遍历 |
去重 + 快速查找(不关心顺序) | unordered_set | 哈希存储,查找效率 O (1) |
键值映射 + 快速查找(无序) | unordered_map | 哈希存储,查找效率高于 map |
先进后出(如撤销操作) | stack | 严格遵循 LIFO 逻辑 |
先进先出(如排队场景) | queue | 严格遵循 FIFO 逻辑 |
按优先级处理元素 | priority_queue | 自动按优先级出队 |
小结:怎么选容器?
- 想按顺序存,频繁用下标访问?选 vector。
- 频繁在中间插删?选 list。
- 想快速查 “有没有某个元素” 或去重?选 set。
- 想通过关键词(如名字)查对应的值?选 map。
- 想模拟 “叠盘子”“排队”?选 stack 或 queue。
这些容器都是 C++ 标准库自带的,不用自己写复杂的逻辑,直接拿来用就行,省事儿又高效。
八、总结
C++ 容器是管理批量数据的 “利器”,不用自己写复杂的存储逻辑,直接调用接口即可。掌握它们的关键在于:
- 理解三大类容器的核心特点:顺序容器强调 “顺序”,关联容器强调 “查找”,适配器强调 “专用场景”;
- 熟悉通用语法:初始化、迭代器、通用成员函数是操作所有容器的基础;
- 掌握专属操作:不同容器有独特的成员函数(如 vector 的
push_back
、map 的find
); - 按需选择容器:根据数据操作特点(如访问方式、增删位置、是否需要排序)选择最合适的容器;
- 容器与数组转换:内存连续的容器(如 vector)可直接转数组,其他容器需通过 vector 中转。
通过多动手练习(如用 vector 存成绩、用 map 做通讯录、用 stack 处理括号匹配),能快速掌握容器的使用技巧,为后续编程打下坚实基础。