c++复习自存--标准模板库STL

标准模板库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_sethash_multisethash_maphash_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()、迭代器三种读取/修改字符的方式:

  1. 下标 []
    直接按索引访问,不做越界检查,运行越界会产生未定义行为
cpp 复制代码
string s = "abc";
char c = s[0];
s[1] = 'x';
  1. at() 成员函数
    带边界校验,索引非法时抛出 out_of_range 异常,安全性更高
cpp 复制代码
char c = s.at(2);
s.at(0) = 'm';
  1. 首尾字符快速访问
cpp 复制代码
char first = s.front();
char last = s.back();
  1. 迭代器遍历
cpp 复制代码
// 正向遍历
for (auto it = s.begin(); it != s.end(); ++it)
    cout << *it;
// 范围for循环
for (char ch : s)
    cout << ch;

三、拼接字符串

三种主流拼接方案,支持字符串、单个字符、字符数组混合拼接:

  1. += 复合赋值运算符(最常用)
cpp 复制代码
string s = "hello";
s += " ";
s += "c++";
s += '!';
  1. append() 成员函数,支持灵活截取拼接
cpp 复制代码
string base = "hello";
base.append(" world");
// 截取目标字符串前3个字符拼接
base.append("testdemo", 3);
// 重复多个字符
base.append(4, '-');
  1. + 运算符拼接,生成新字符串
cpp 复制代码
string res = "a" + string("b") + "c";

四、在string中查找字符或子字符串

string内置find系列函数,全部返回size_t类型;查找失败返回常量string::npos

核心查找接口
  1. 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;
  1. rfind():从右向左反向查找
cpp 复制代码
size_t rpos = s.rfind("abc");
  1. find_first_of():查找任意匹配字符
cpp 复制代码
size_t p = s.find_first_of("0123456789"); // 函数会从字符串 s 的起始位置(默认为索引 0)开始,逐个字符检查,一旦遇到 '0'~'9' 中的任意一个,就会立即停止查找并返回该字符的**索引**。
  1. find_last_of():反向匹配任意字符
  2. find_first_not_of():查找第一个不匹配的字符

五、截短STL string

通过erase删除指定区间字符、substr截取子串生成新字符串,实现截断效果:

  1. erase 删除原字符串内容
cpp 复制代码
string s = "0123456789";
s.erase(5);        // 从下标5开始全部删除,剩余"01234"
s.erase(2, 3);     // 从下标2删除3个字符
s.erase(s.begin()+1); // 删除单个迭代器字符
  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;
  1. std::string = basic_string<char> 普通ASCII字符串
  2. std::wstring = basic_string<wchar_t> 宽字符字符串
  3. std::u8string/u16string/u32string 对应不同编码字符集
    模板参数分工:
  • CharT:字符类型,char、wchar_t等
  • Traits:字符特性,定义字符比较、拷贝、长度计算逻辑
  • Allocator:内存分配器,控制字符串堆内存申请释放
    所有字符串操作接口均基于模板封装,因此一套逻辑可适配任意字符类型。

STL动态数组类

一、std::vector的特点

std::vector是STL提供的连续内存动态数组容器,头文件为<vector>,核心特性:

  1. 内存连续:所有元素在堆上占用一段连续内存,支持下标随机访问,访问速度与普通数组一致;
  2. 自动扩容:无需手动管理内存,元素增多时容器自动重新分配更大内存并拷贝原有数据;
  3. 尾部操作高效push_backpop_back仅修改尾部,时间复杂度O(1);
  4. 中间插入/删除代价高:在数组中间增删元素会导致后方所有元素整体移位,时间复杂度O(n);
  5. 资源自动释放:容器生命周期结束时,自动释放内部存储的所有元素与堆内存,无内存泄漏风险;
  6. 模板泛型:可存储任意数据类型(内置类型、自定义类、容器嵌套)。

二、典型的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中的元素

提供多种删除接口,适配不同删除需求:

  1. pop_back():删除尾部元素,无返回值,O(1)效率
cpp 复制代码
vec.pop_back();
  1. erase():按迭代器删除单个/区间元素,中间删除会移位
cpp 复制代码
vector<int> vec = {1,2,3,4,5};
vec.erase(vec.begin() + 1); // 删除元素2
vec.erase(vec.begin(), vec.begin() + 2); // 删除前两个元素
  1. clear():清空容器内所有元素,不释放底层内存容量
cpp 复制代码
vec.clear();

三、理解大小和容量

vector区分两个核心属性:sizecapacity

  1. size() :当前容器内有效元素个数,随增删元素实时变化;
  2. capacity() :容器底层堆内存总容纳上限,是预分配的内存空间,仅扩容时增大;
  3. 扩容规则:当size == capacity,执行添加操作时容器自动扩容,通常扩容至原容量1.5~2倍;
  4. 配套控制接口:
    • 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头部操作低效的缺陷:

  1. 底层内存分段连续,并非一整块堆内存;
  2. 首尾插入/删除均为O(1)效率,支持push_frontpop_front
  3. 同样支持随机下标访问,但寻址速度略慢于vector;
  4. 适用场景:需要频繁在头尾增删数据(任务队列、缓冲队列);
  5. 基础操作示例:
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>,底层由双向链表节点构成,每个节点存储数据、前驱指针、后继指针。

  1. 内存非连续 :元素分散在堆内存,不存在整块连续空间,不支持下标随机访问,只能通过迭代器顺序遍历;
  2. 任意位置增删高效:头部、尾部、链表中间插入/删除元素仅修改相邻节点指针,时间复杂度 O(1),不会发生元素整体移位;
  3. 迭代器不会失效:插入、删除其他节点时,未操作元素的迭代器保持有效;仅删除节点自身的迭代器失效;
  4. 自带专属排序/反转接口 :内置 sort()reverse() 成员函数,适配链表结构,效率高于全局 std::sort
  5. 额外内存开销:每个节点需要存储两个指针,存储相同数据时内存占用高于 vector、deque;
  6. 遍历效率偏低:随机访问需要顺着链表一步步跳转,遍历速度弱于连续内存容器。

二、基本的 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()

  1. 无参 sort():默认升序排列,使用 < 比较元素;
cpp 复制代码
list<int> lst{5,2,9,1,3};
lst.sort(); // 结果 1,2,3,5,9
  1. 传入自定义比较函数/仿函数,实现降序、自定义规则排序:
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 的轻量化版本:

  1. 底层结构:仅存储后继指针,无前驱指针,内存开销比双向 list 更小;
  2. 功能限制:仅支持头部增删,不支持尾部操作、反向遍历,无 size() 接口;
  3. 插入删除:仅能在迭代器后一位 插入元素(insert_after)、删除后一位元素(erase_after);
  4. 适用场景:仅从前到后单向遍历、追求极低内存占用的场景;
  5. 基础示例:
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::setstd::multiset 是STL有序关联容器,头文件为 <set>,底层默认采用红黑树实现,核心共性特征:

  1. 容器内元素自动保持升序有序,插入、查找、删除操作平均时间复杂度 O(logN);
  2. 不支持下标随机访问,仅能通过双向迭代器遍历;
  3. 插入、删除操作不会引发内存整体移位,仅调整树节点指针,操作后未操作元素的迭代器保持有效;
  4. 二者核心区分:
    • 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中查找元素

提供两类查找接口,依托红黑树快速检索:

  1. count(val):统计容器内等于val的元素个数
    • set:返回0或1;
    • multiset:返回重复元素总数量。
  2. 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
  1. 边界查找(有序容器专属)
    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的优缺点
优点
  1. 自动排序:插入后始终有序,无需手动调用排序算法;
  2. 高效查找:红黑树O(logN)查询效率,远优于链表、动态数组的O(N)遍历;
  3. 元素唯一性控制:set天然去重,无需额外判断;
  4. 迭代器稳定:增删元素不影响无关节点的迭代器,遍历过程安全;
  5. multiset原生支持重复有序数据存储。
缺点
  1. 无随机访问:不支持下标[],无法快速获取第N个元素;
  2. 内存开销大:红黑树每个节点存储左右/父指针、颜色标记,内存占用高于vector;
  3. 插入删除存在常数开销:需要维护树平衡,大批量连续数据读写性能弱于连续内存容器;
  4. 自定义类型存储要求严格:必须重载<比较运算符或传入自定义比较器,否则无法编译。

STL映射类

一、STL映射类简介

映射类是存储键值对(key-value) 的关联容器,分为两大类:

  1. 有序映射std::map / std::multimap,底层红黑树,容器内按键自动升序排列,增删查时间复杂度 O(log⁡N)O(\log N)O(logN);
  2. 无序哈希映射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中查找元素
  1. 下标[]:key不存在会自动插入默认空值,仅适合读取已知存在的key;
  2. find(key) :返回目标键迭代器,找不到返回end(),不会新增元素,推荐检索;
  3. count(key):统计key存在数量,map只会返回0或1;
  4. 边界查找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_boundupper_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. 散列表的工作原理
  1. 哈希函数:接收key,计算得到哈希值,映射到哈希表的桶位;
  2. 哈希冲突:多个key算出相同哈希值,桶内用链表/红黑树存储冲突元素;
  3. 负载因子:元素总数/桶数量,负载过高会扩容重哈希,性能下降;
  4. 查找流程: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;
相关推荐
努力努力再努力wz1 小时前
【高性能网络库与HTTP Server系列】:基于主从 Reactor 模型实现高性能 C++ 网络库与 HTTP Server
开发语言·网络·数据结构·数据库·c++·网络协议·http
什巳1 小时前
JAVA练习275-乘积最大子数组
java·开发语言·数据结构·算法
lang201509281 小时前
Apache POI Word(docx) 实战教程:从入门到精通表格合并、图片插入与文档合并
java·开发语言·word
Robot_Nav2 小时前
C++ 面试常问问题汇总:基础语法、面向对象、STL、多线程与设计模式
c++·设计模式·面试
盐焗鹌鹑蛋2 小时前
【C++】二叉搜索树
c++
哎呦,帅小伙哦2 小时前
C++工程实战: 预处理宏、宏重定义避坑、LOG_TAG业务实践与__COUNTER__深度解析
c++
鱼毓屿御2 小时前
Python 装饰器与函数调用机制(复习笔记 · 2026-07-07)
开发语言·python
浩瀚地学2 小时前
【Java基础复习】IO流(二)
java·开发语言·经验分享·笔记·学习
IVVi0jToe2 小时前
高效C++线程池设计与实现
java·c++·安全