标准模板库STL
容器是用于存储数据的 STL 类,STL 提供了两种类型的容器类:
- 顺序容器;
- 关联容器。
另外,STL 还提供了被称为容器适配器(Container Adapter)的类,它们是顺序容器和关联容器的变种,包含的功能有限,用于满足特殊的需求。
顺序容器
顾名思义,顺序容器按顺序存储数据,如数组和列表。顺序容器具有插入速度快但查找操作相对较慢的特征。
STL 顺序容器如下所示。
std::vector:操作与动态数组一样,在最后插入数据;可将 vector 视为书架,您可在一端添加和拿走图书。std::deque:双端队列,与 std::vector 类似,但允许在开头插入或删除元素。std::list:操作与双向链表一样。可将它视为链条,对象被连接在一起,您可在任何位置添加或删除对象。std::forward_list:类似于 std::list,但是单向链表,只能沿一个方向遍历。
STL vector 类与数组类似,允许随机访问元素,即可使用下标运算符(\[\])指定元素在 vector 中的位置(索引),从而直接访问或操作元素。另外,STL vector 是动态数组,因此能够根据应用程序在运行阶段的需求自动调整长度。为保留数组能够根据位置随机访问元素的特征,大多数 STL vector 实现都将所有元素存储在连续的存储单元中,因此需要调整长度的 vector 通常会降低应用程序的性能,这取决于它包含的对象类型。
可将 STL list 类视为普通链表的 STL 实现。虽然 list 中的元素不能像 STL vector 中的元素那样随机访问,但 list 可使用不连续的内存块组织元素,因此它不像 std::vector 那样需要给内部数组重新分配内存,进而导致性能问题。
关联容器
关联容器按指定的顺序存储数据,就像词典一样。这将降低插入数据的速度,但在查询方面有很大的优势。
STL 提供的关联容器如下所示。
std::set:存储各不相同的值,在插入时进行排序;容器的复杂度为对数。std::unordered_set:存储各不相同的值,在插入时进行排序;容器的复杂度为常数。这种容器是 C++11 新增的。std::map:存储键-值对,并根据唯一的键排序;容器的复杂度为对数。std::unordered_map:存储键-值对,并根据唯一的键排序;容器的复杂度为对数。这种容器是C++11 新增的。std::multiset:与 set 类似,但允许存储多个值相同的项,即值不需要是唯一的。std::unordered_multiset:与 unordered_set 类似,但允许存储多个值相同的项,即值不需要是唯一的。这种容器是 C++11 新增的。std::multimap:与 map 类似,但不要求键是唯一的。std::unordered_multimap:与 unordered_map 类似,但不要求键是唯一的。这种容器是 C++11新增的。
有些 STL 实现也支持关联容器 hash_set、hash_multiset、hash_map 和 hash_multimap,它们与标准支持的 unordered_*容器类似。在有些情况下,hash_*和 unordered_*容器有更好的元素搜索性能,因为其元素访问时间为常量(不依赖于容器包含的元素数)。通常,这些容器提供了与相应的标准容器相同的公有方法,因此使用起来很容易。
使用遵循标准的容器时,代码将更容易在不同平台和编译器之间移植,因此是更好的选择。另外,虽然遵循标准的容器的性能呈对数降低,但这可能并不会严重影响应用程序。
容器适配器
容器适配器(Container Adapter)是顺序容器和关联容器的变种,其功能有限,用于满足特定的需求。主要的适配器类如下所示。
std::stack:以 LIFO(后进先出)的方式存储元素,让您能够在栈顶插入(压入)和删除(弹出)元素。std::queue:以 FIFO(先进先出)的方式存储元素,让您能够删除最先插入的元素。std::priority_queue:以特定顺序存储元素,因为优先级最高的元素总是位于队列开头。
查找、排序和反转等都是标准的编程需求,不应让程序员重复实现这样的功能。因此 STL 以 STL算法的方式提供这些函数,通过结合使用这些函数和迭代器,程序员可对容器执行一些最常见的操作。
最常用的 STL 算法如下所示。
std::find:在集合中查找值。std::find_if:根据用户指定的谓词在集合中查找值。std::reverse:反转集合中元素的排列顺序。std::remove_if:根据用户定义的谓词将元素从集合中删除。std::transform:使用用户定义的变换函数对容器中的元素进行变换。
这些算法都是 std 命名空间中的模板函数,要使用它们,必须包含标准头文件<algorithm>。
选择正确的容器



STL 字符串类
STL 提供了一个专门为操纵字符串而设计的模板类:std::basic_string<T>,该模板类的两个常用具体化如下所示。
- std::string:基于 char 的 std::basic_string 具体化,用于操纵简单字符串。
- std::wstring:基于 wchar_t 的 std::basic_string 具体化,用于操纵宽字符串,通常用于存储支持各种语言中符号的 Unicode 字符。
一、实例化和复制 STL string
std::string 是C++标准库提供的字符串类,封装字符存储、内存管理与各类字符串操作,使用前需要引入头文件 <string>。
1. 多种实例化方式
cpp
#include <string>
#include <iostream>
using namespace std;
// 1. 空字符串
string s1;
// 2. 字面量初始化
string s2("hello");
string s3 = "world";
// 3. 重复字符构造
string s4(5, 'a'); // "aaaaa"
// 4. 从字符数组构造
char arr[] = "test";
string s5(arr);
// 5. 截取部分字符构造
string s6("abcdef", 3); // "abc"
2. 字符串复制
cpp
string src = "source text";
// 拷贝构造
string dst1(src);
// 赋值运算符拷贝
string dst2;
dst2 = src;
// assign 方法拷贝
string dst3;
dst3.assign(src);
// 截取部分内容复制
dst3.assign(src, 0, 6);//将源对象 src 中从索引 0 开始的 6 个元素赋值给目标对象 dst3
复制时string会自动分配独立堆内存,修改副本不会影响原字符串。
二、访问std::string的字符内容
提供下标访问、at()、迭代器三种读取/修改字符的方式:
- 下标
[]
直接按索引访问,不做越界检查,运行越界会产生未定义行为
cpp
string s = "abc";
char c = s[0];
s[1] = 'x';
at()成员函数
带边界校验,索引非法时抛出out_of_range异常,安全性更高
cpp
char c = s.at(2);
s.at(0) = 'm';
- 首尾字符快速访问
cpp
char first = s.front();
char last = s.back();
- 迭代器遍历
cpp
// 正向遍历
for (auto it = s.begin(); it != s.end(); ++it)
cout << *it;
// 范围for循环
for (char ch : s)
cout << ch;
三、拼接字符串
三种主流拼接方案,支持字符串、单个字符、字符数组混合拼接:
+=复合赋值运算符(最常用)
cpp
string s = "hello";
s += " ";
s += "c++";
s += '!';
append()成员函数,支持灵活截取拼接
cpp
string base = "hello";
base.append(" world");
// 截取目标字符串前3个字符拼接
base.append("testdemo", 3);
// 重复多个字符
base.append(4, '-');
+运算符拼接,生成新字符串
cpp
string res = "a" + string("b") + "c";
四、在string中查找字符或子字符串
string内置find系列函数,全部返回size_t类型;查找失败返回常量string::npos。
核心查找接口
- find():从左向右正向查找
cpp
string s = "abcabc123";
size_t pos1 = s.find('a'); // 查找字符
size_t pos2 = s.find("abc"); // 查找子串
size_t pos3 = s.find("abc", 3); // 从下标3开始查找
if (pos1 != string::npos)
cout << "找到位置:" << pos1;
- rfind():从右向左反向查找
cpp
size_t rpos = s.rfind("abc");
- find_first_of():查找任意匹配字符
cpp
size_t p = s.find_first_of("0123456789"); // 函数会从字符串 s 的起始位置(默认为索引 0)开始,逐个字符检查,一旦遇到 '0'~'9' 中的任意一个,就会立即停止查找并返回该字符的**索引**。
- find_last_of():反向匹配任意字符
- find_first_not_of():查找第一个不匹配的字符
五、截短STL string
通过erase删除指定区间字符、substr截取子串生成新字符串,实现截断效果:
- erase 删除原字符串内容
cpp
string s = "0123456789";
s.erase(5); // 从下标5开始全部删除,剩余"01234"
s.erase(2, 3); // 从下标2删除3个字符
s.erase(s.begin()+1); // 删除单个迭代器字符
- substr 截取子串(不修改原串)
cpp
string full = "program";
string sub1 = full.substr(0, 3); // 从0截取3个字符 "pro"
string sub2 = full.substr(4); // 从下标4截取到末尾 "ram"
六、字符串反转
方案1:标准算法 std::reverse(推荐)
需要头文件 <algorithm>,直接原地反转字符串
cpp
#include <algorithm>
string s = "12345";
reverse(s.begin(), s.end());
// s 变为 "54321"
方案2:手动迭代交换字符
cpp
void reverseStr(string& s) {
int l = 0, r = s.size() - 1;
while(l < r) swap(s[l++], s[r--]);
}
七、字符串的大小写转换
标准库无内置一键转换函数,结合遍历与 <cctype> 头文件tolower/toupper实现:
cpp
#include <cctype>
string str = "HelloC++";
// 转小写
for (char& ch : str)
ch = tolower(ch);
// 转大写
for (char& ch : str)
ch = toupper(ch);
封装通用转换函数:
cpp
string toLower(string s) {
for (char& c : s) c = tolower(c);
return s;
}
string toUpper(string s) {
for (char& c : s) c = toupper(c);
return s;
}
八、基于模板的STL string实现
std::string本质是模板实例化产物,完整定义:
cpp
template<
class CharT,
class Traits = char_traits<CharT>,
class Allocator = allocator<CharT>
> class basic_string;
std::string=basic_string<char>普通ASCII字符串std::wstring=basic_string<wchar_t>宽字符字符串std::u8string/u16string/u32string对应不同编码字符集
模板参数分工:
- CharT:字符类型,char、wchar_t等
- Traits:字符特性,定义字符比较、拷贝、长度计算逻辑
- Allocator:内存分配器,控制字符串堆内存申请释放
所有字符串操作接口均基于模板封装,因此一套逻辑可适配任意字符类型。
STL动态数组类
一、std::vector的特点
std::vector是STL提供的连续内存动态数组容器,头文件为<vector>,核心特性:
- 内存连续:所有元素在堆上占用一段连续内存,支持下标随机访问,访问速度与普通数组一致;
- 自动扩容:无需手动管理内存,元素增多时容器自动重新分配更大内存并拷贝原有数据;
- 尾部操作高效 :
push_back、pop_back仅修改尾部,时间复杂度O(1); - 中间插入/删除代价高:在数组中间增删元素会导致后方所有元素整体移位,时间复杂度O(n);
- 资源自动释放:容器生命周期结束时,自动释放内部存储的所有元素与堆内存,无内存泄漏风险;
- 模板泛型:可存储任意数据类型(内置类型、自定义类、容器嵌套)。
二、典型的vector操作
1. 实例化vector
提供多种构造方式适配不同初始化场景:
cpp
#include <vector>
#include <string>
using namespace std;
// 1. 空容器,无元素
vector<int> vec1;
// 2. 指定初始容量,全部元素默认初始化
vector<int> vec2(10);
// 3. 指定容量+填充初始值
vector<int> vec3(5, 99); // 5个元素,全部为99
// 4. 拷贝另一个vector
vector<int> vec4(vec3);
// 5. 从数组/迭代器区间构造
int arr[] = {1,2,3};
vector<int> vec5(arr, arr + sizeof(arr)/sizeof(int));
2. 使用push_back()在末尾插入元素
push_back向容器尾部追加单个元素,是最常用的添加接口,内存充足时直接写入,容量耗尽则自动扩容:
cpp
vector<string> vec;
vec.push_back("C++");
vec.push_back("STL");
vec.push_back("vector");
C++11及以上可使用emplace_back,直接在容器内存构造对象,省去拷贝,性能更优:
cpp
vec.emplace_back("new text");
3. 列表初始化
花括号一次性写入多个初始元素,简洁直观,C++11标准支持:
cpp
vector<int> vec = {10, 20, 30, 40, 50};
vector<string> str_vec{"apple", "banana", "orange"};
4. 使用insert()在指定位置插入元素
insert依靠迭代器定位插入点,支持插入单个元素、批量元素、区间数据;插入位置在容器中间时,后方元素会向后移位:
cpp
vector<int> vec = {1, 3, 4};
// 在起始位置插入数字0
vec.insert(vec.begin(), 0);
// 在第二个元素位置插入两个2
vec.insert(vec.begin() + 2, 2, 2);
// 插入另一个容器的区间
vector<int> temp = {5,6};
vec.insert(vec.end(), temp.begin(), temp.end());
5. 使用数组语法访问vector中的元素
重载下标运算符[],和原生数组语法完全一致,不做边界越界检查,超出范围会产生未定义行为:
cpp
vector<int> vec = {10,20,30};
int a = vec[0]; // 读取第一个元素
vec[1] = 99; // 修改第二个元素
配套安全访问接口at(),带边界校验,索引非法抛出out_of_range异常:
cpp
int b = vec.at(2);
6. 使用指针语法访问vector中的元素
vector底层内存连续,begin()返回首元素迭代器(等价于元素指针),可通过指针偏移遍历访问:
cpp
vector<int> vec = {1,2,3};
int* p = &vec[0];
cout << *(p + 1); // 输出2
// 迭代器本质封装的指针,支持加减偏移
auto it = vec.begin();
cout << *(it + 2); // 输出3
7. 删除vector中的元素
提供多种删除接口,适配不同删除需求:
pop_back():删除尾部元素,无返回值,O(1)效率
cpp
vec.pop_back();
erase():按迭代器删除单个/区间元素,中间删除会移位
cpp
vector<int> vec = {1,2,3,4,5};
vec.erase(vec.begin() + 1); // 删除元素2
vec.erase(vec.begin(), vec.begin() + 2); // 删除前两个元素
clear():清空容器内所有元素,不释放底层内存容量
cpp
vec.clear();
三、理解大小和容量
vector区分两个核心属性:size与capacity
- size() :当前容器内有效元素个数,随增删元素实时变化;
- capacity() :容器底层堆内存总容纳上限,是预分配的内存空间,仅扩容时增大;
- 扩容规则:当
size == capacity,执行添加操作时容器自动扩容,通常扩容至原容量1.5~2倍; - 配套控制接口:
reserve(n):预分配内存,设置capacity,避免频繁扩容;resize(n):修改有效元素size,新增元素默认初始化,超出原size则截断;shrink_to_fit():释放多余预分配内存,使capacity等于size。
cpp
vector<int> vec;
vec.reserve(100); // 预分配100个元素空间
vec.push_back(1);
cout << vec.size(); // 输出1
cout << vec.capacity(); // 输出100
四、STL deque类
std::deque双端队列容器,头文件<deque>,弥补vector头部操作低效的缺陷:
- 底层内存分段连续,并非一整块堆内存;
- 首尾插入/删除均为O(1)效率,支持
push_front、pop_front; - 同样支持随机下标访问,但寻址速度略慢于vector;
- 适用场景:需要频繁在头尾增删数据(任务队列、缓冲队列);
- 基础操作示例:
cpp
deque<int> dq = {10,20,30};
dq.push_front(5); // 头部插入
dq.push_back(35); // 尾部插入
dq.pop_front(); // 删除头部
dq.pop_back(); // 删除尾部
cout << dq[0];
vector与deque核心对比
| 特性 | vector | deque |
|---|---|---|
| 内存布局 | 整块连续内存 | 分段块内存 |
| 尾部操作 | 极快 | 快 |
| 头部操作 | 极慢(全量移位) | 极快 |
| 随机访问 | 速度最快 | 略慢 |
| 适用场景 | 只读、仅尾部增删 | 头尾频繁插入删除 |
STL list 和 forward_list
一、std::list 的特点
std::list 是STL双向链表容器,头文件为 <list>,底层由双向链表节点构成,每个节点存储数据、前驱指针、后继指针。
- 内存非连续 :元素分散在堆内存,不存在整块连续空间,不支持下标随机访问,只能通过迭代器顺序遍历;
- 任意位置增删高效:头部、尾部、链表中间插入/删除元素仅修改相邻节点指针,时间复杂度 O(1),不会发生元素整体移位;
- 迭代器不会失效:插入、删除其他节点时,未操作元素的迭代器保持有效;仅删除节点自身的迭代器失效;
- 自带专属排序/反转接口 :内置
sort()、reverse()成员函数,适配链表结构,效率高于全局std::sort; - 额外内存开销:每个节点需要存储两个指针,存储相同数据时内存占用高于 vector、deque;
- 遍历效率偏低:随机访问需要顺着链表一步步跳转,遍历速度弱于连续内存容器。
二、基本的 list 操作
1. 实例化 std::list 对象
提供多种构造方式,适配不同初始化场景:
cpp
#include <list>
#include <string>
using namespace std;
// 空链表
list<int> lst1;
// 指定初始元素个数,全部默认值0
list<int> lst2(5);
// 指定数量+填充值
list<int> lst3(4, 88);
// 拷贝已有链表
list<int> lst4(lst3);
// C++11列表初始化
list<string> lst5{"apple", "banana", "pear"};
// 从数组/迭代器区间构造
int arr[] = {1,2,3};
list<int> lst6(arr, arr + sizeof(arr)/sizeof(int));
2. 在 list 开头或末尾插入元素
链表首尾操作专用接口,时间复杂度均为 O(1):
cpp
list<int> lst;
// 尾部插入
lst.push_back(10);
lst.emplace_back(20); // 原地构造,减少拷贝
// 头部插入
lst.push_front(0);
lst.emplace_front(-5);
3. 在 list 中间插入元素
使用 insert() 基于迭代器定位插入点,插入仅修改指针,无元素移位开销:
cpp
list<int> lst = {10, 30, 40};
auto it = lst.begin();
++it; // 指向第二个元素30
// 在迭代器位置前插入单个元素
lst.insert(it, 20);
// 插入多个相同元素
lst.insert(lst.end(), 2, 50);
// 插入其他容器区间数据
list<int> temp{60,70};
lst.insert(lst.end(), temp.begin(), temp.end());
4. 删除 list 中的元素
提供多套删除接口,适配单元素、区间、按值批量删除场景:
cpp
list<int> lst = {1,2,3,4,5,3};
// 删除头部元素
lst.pop_front();
// 删除尾部元素
lst.pop_back();
// erase:根据迭代器删除单个/区间元素
auto it = lst.begin();
++it;
lst.erase(it); // 删除第二个元素
lst.erase(lst.begin(), ++lst.begin()); // 删除区间
// remove:删除链表中所有等于目标值的元素
lst.remove(3);
// clear:清空全部元素
lst.clear();
三、对 list 中的元素进行反转和排序
1. 使用 list::reverse() 反转元素的排列顺序
内置成员函数,直接原地反转链表节点指针,无需拷贝数据,效率极高:
cpp
list<int> lst = {1,2,3,4,5};
lst.reverse();
// 链表变为 {5,4,3,2,1}
2. 对元素进行排序
std::list 不支持全局 std::sort(全局排序依赖随机访问迭代器),必须使用内置成员 sort():
- 无参
sort():默认升序排列,使用<比较元素;
cpp
list<int> lst{5,2,9,1,3};
lst.sort(); // 结果 1,2,3,5,9
- 传入自定义比较函数/仿函数,实现降序、自定义规则排序:
cpp
// 降序排序
lst.sort(greater<int>());
3. 对包含对象的 list 进行排序以及删除其中的元素
自定义结构体存储时,需要重载比较运算符或传入自定义比较规则,remove_if 可按条件批量删除对象:
cpp
#include <string>
struct Student {
string name;
int score;
Student(string n, int s) : name(n), score(s) {}
// 重载<,支持默认sort升序(按分数)
bool operator<(const Student& other) const {
return score < other.score;
}
};
int main() {
list<Student> stList;
stList.emplace_back("Alice", 80);
stList.emplace_back("Bob", 60);
stList.emplace_back("Cathy", 95);
// 按分数升序排序
stList.sort();
// 删除分数低于70的学生
stList.remove_if([](const Student& s){ return s.score < 70; });
return 0;
}
4. C++11引入的 std::forward_list
forward_list 单向链表,头文件 <forward_list>,是 list 的轻量化版本:
- 底层结构:仅存储后继指针,无前驱指针,内存开销比双向 list 更小;
- 功能限制:仅支持头部增删,不支持尾部操作、反向遍历,无
size()接口; - 插入删除:仅能在迭代器后一位 插入元素(
insert_after)、删除后一位元素(erase_after); - 适用场景:仅从前到后单向遍历、追求极低内存占用的场景;
- 基础示例:
cpp
forward_list<int> flst;
flst.push_front(10);
flst.push_front(5);
// 在第一个元素后方插入
auto it = flst.begin();
flst.insert_after(it, 7);
// 删除迭代器后一个元素
flst.erase_after(it);
// 内置反转、排序接口
flst.reverse();
flst.sort();
STL集合类
一、简介
std::set 与 std::multiset 是STL有序关联容器,头文件为 <set>,底层默认采用红黑树实现,核心共性特征:
- 容器内元素自动保持升序有序,插入、查找、删除操作平均时间复杂度 O(logN);
- 不支持下标随机访问,仅能通过双向迭代器遍历;
- 插入、删除操作不会引发内存整体移位,仅调整树节点指针,操作后未操作元素的迭代器保持有效;
- 二者核心区分:
set:元素唯一不重复,相同值无法插入;multiset:允许存储多个重复值,可存放完全相同的元素。
二、STL set 和 multiset 的基本操作
1. 实例化std::set对象
支持多种构造方式,同时可自定义排序规则:
cpp
#include <set>
using namespace std;
// 1. 空集合,默认升序
set<int> s1;
// 2. 列表初始化,自动去重并排序
set<int> s2 = {5, 2, 8, 2, 1};
// 3. 从其他容器迭代器区间构造
int arr[] = {3, 1, 4};
set<int> s3(arr, arr + sizeof(arr)/sizeof(int));
// 4. 自定义降序排序规则
set<int, greater<int>> s4 = {1,3,2};
// multiset 实例化(允许重复元素)
multiset<int> ms = {2, 2, 5, 5, 5};
2. 在set或multiset中插入元素
统一使用 insert() 完成插入,容器自动完成排序、去重处理:
cpp
set<int> s;
// 插入单个元素,set自动过滤重复值
s.insert(10);
s.insert(5);
s.insert(10); // 重复元素,set无变化
// C++11 emplace 原地构造对象,减少拷贝
s.emplace(7);
// 插入一组区间数据
vector<int> temp = {1, 9, 3};
s.insert(temp.begin(), temp.end());
// multiset 插入重复值会全部保留
multiset<int> ms;
ms.insert(4);
ms.insert(4);
插入返回值说明:
set::insert返回pair<iterator, bool>,bool标识元素是否插入成功(重复则false);multiset::insert仅返回迭代器,无布尔标识,重复元素总能插入。
3. 在STL set或multiset中查找元素
提供两类查找接口,依托红黑树快速检索:
count(val):统计容器内等于val的元素个数- set:返回0或1;
- multiset:返回重复元素总数量。
find(val):返回目标元素迭代器,找不到返回end()
cpp
set<int> s = {2,4,6,8};
auto it = s.find(4);
if (it != s.end())
cout << "找到元素:" << *it;
// multiset 查找重复值
multiset<int> ms = {1,2,2,3};
int num = ms.count(2); // num=2
- 边界查找(有序容器专属)
lower_bound(val):第一个≥val的元素迭代器;
upper_bound(val):第一个>val的元素迭代器;
可用于提取multiset中所有重复值区间。
4. 删除STL set或multiset中的元素
三种删除方式,适配不同场景:
cpp
set<int> s = {1,3,5,7,9};
// 1. 按值删除,删除所有匹配元素
s.erase(3);
// 2. 按迭代器删除单个元素
auto it = s.find(5);
if(it != s.end()) s.erase(it);
// 3. 删除区间内所有元素
auto l = s.find(7), r = s.end();
s.erase(l, r);
// 4. 清空全部元素
s.clear();
multiset执行 erase(val) 会一次性删除所有等于val的重复元素;若仅需删除单个重复元素,必须传入迭代器。
三、使用STL set和multiset的优缺点
优点
- 自动排序:插入后始终有序,无需手动调用排序算法;
- 高效查找:红黑树O(logN)查询效率,远优于链表、动态数组的O(N)遍历;
- 元素唯一性控制:set天然去重,无需额外判断;
- 迭代器稳定:增删元素不影响无关节点的迭代器,遍历过程安全;
- multiset原生支持重复有序数据存储。
缺点
- 无随机访问:不支持下标
[],无法快速获取第N个元素; - 内存开销大:红黑树每个节点存储左右/父指针、颜色标记,内存占用高于vector;
- 插入删除存在常数开销:需要维护树平衡,大批量连续数据读写性能弱于连续内存容器;
- 自定义类型存储要求严格:必须重载
<比较运算符或传入自定义比较器,否则无法编译。
STL映射类
一、STL映射类简介
映射类是存储键值对(key-value) 的关联容器,分为两大类:
- 有序映射 :
std::map/std::multimap,底层红黑树,容器内按键自动升序排列,增删查时间复杂度 O(logN)O(\log N)O(logN); - 无序哈希映射 :
std::unordered_map/std::unordered_multimap,底层哈希表,无固定顺序,平均增删查时间复杂度 O(1)O(1)O(1)。
核心区分:
map:键key唯一,一个键仅对应一个值;multimap:允许同一个key对应多个value,键可重复;unordered_map:哈希版map,键唯一,无排序;unordered_multimap:哈希版multimap,键可重复,无排序。
头文件:<map>(有序映射)、<unordered_map>(哈希映射)。
二、STL map和multimap的基本操作
1. 实例化std::map和std::multimap
cpp
#include <map>
#include <string>
using namespace std;
// 1. 空map,默认按键升序
map<int, string> mp1;
// 2. 列表初始化,自动按key排序、去重
map<int, string> mp2 = {{2, "B"}, {1, "A"}, {3, "C"}};
// 3. 自定义降序排序规则
map<int, string, greater<int>> mp3;
// multimap 实例化,允许重复key
multimap<int, string> mmp = {{1, "a"}, {1, "b"}, {2, "c"}};
map存储的元素类型为pair<const Key, T>,键key为const不可修改,仅value可修改。
2. 在STL map或multimap中插入元素
四种插入方式,适配不同场景:
cpp
map<int, string> mp;
// 方式1:[]下标插入,key不存在则新建,存在则覆盖原值
mp[1] = "Apple";
mp[2] = "Banana";
// 方式2:insert插入pair键值对
mp.insert(make_pair(3, "Orange"));
mp.insert({4, "Grape"});
// 方式3:emplace原地构造,省去pair拷贝
mp.emplace(5, "Peach");
// multimap插入,重复key可正常存入
multimap<int, string> mmp;
mmp.insert({10, "test1"});
mmp.insert({10, "test2"});
map::insert返回pair<iterator, bool>:bool代表插入成功与否(key重复则失败);
multimap::insert仅返回迭代器,无布尔标记,重复key一定插入成功。
3. 在STL map中查找元素
- 下标
[]:key不存在会自动插入默认空值,仅适合读取已知存在的key; find(key):返回目标键迭代器,找不到返回end(),不会新增元素,推荐检索;count(key):统计key存在数量,map只会返回0或1;- 边界查找
lower_bound/upper_bound:获取key区间迭代器。
cpp
map<int, string> mp = {{1,"A"},{2,"B"},{3,"C"}};
auto it = mp.find(2);
if (it != mp.end())
cout << it->first << ":" << it->second;
int exist = mp.count(5); // 0,不存在
4. 在STL multimap中查找元素
multimap允许重复key,find仅返回第一个匹配key 的迭代器,如需取出全部同key元素,搭配lower_bound、upper_bound获取区间:
cpp
multimap<int, string> mmp = {{1,"a"},{1,"b"},{2,"c"}};
// 获取所有key=1的元素区间
auto left = mmp.lower_bound(1);
auto right = mmp.upper_bound(1);
for (auto iter = left; iter != right; ++iter)
cout << iter->second << " ";
count(key)返回当前key对应的元素总个数。
5. 删除STL map或multimap中的元素
三种删除接口:
cpp
map<int, string> mp = {{1,"A"},{2,"B"},{3,"C"}};
// 1. 按键删除,map删除唯一匹配key,multimap删除该key全部元素
mp.erase(2);
// 2. 按迭代器删除单个元素
auto it = mp.find(3);
if(it != mp.end()) mp.erase(it);
// 3. 删除区间内所有元素
mp.erase(mp.begin(), mp.end());
// 4. 清空全部元素
mp.clear();
三、提供自定义的排序谓词
有序map/multimap默认使用less<Key>按键升序,可自定义仿函数/函数指针/lamda作为排序规则:
cpp
// 自定义仿函数:按键降序
template<typename Key>
struct DescSort {
bool operator()(const Key& a, const Key& b) const {
return a > b;
}
};
// 使用自定义排序谓词实例化map
map<int, string, DescSort<int>> mp = {{1,"a"},{3,"c"},{2,"b"}};
自定义类型作为key时,必须提供排序规则,否则容器无法比较键的大小。
四、基于散列表的STL键-值对容器
1. 散列表的工作原理
- 哈希函数:接收key,计算得到哈希值,映射到哈希表的桶位;
- 哈希冲突:多个key算出相同哈希值,桶内用链表/红黑树存储冲突元素;
- 负载因子:元素总数/桶数量,负载过高会扩容重哈希,性能下降;
- 查找流程:key哈希定位桶,遍历桶内链表匹配完整key,平均仅需一次寻址。
2. 使用unordered_map和unordered_multimap
unordered_map(键唯一,无序哈希映射)
cpp
#include <unordered_map>
unordered_map<string, int> um;
// 插入
um["Alice"] = 90;
um.insert({"Bob", 85});
// 查找
auto it = um.find("Alice");
if(it != um.end()) cout << it->second;
// 删除
um.erase("Bob");
unordered_multimap(键可重复,无序哈希映射)
无下标[]运算符,仅能通过insert插入,查找同multimap,利用equal_range获取全部同key元素:
cpp
unordered_multimap<int, string> umm;
umm.insert({1, "x"});
umm.insert({1, "y"});
auto range = umm.equal_range(1);
for(auto iter = range.first; iter != range.second; ++iter)
cout << iter->second;