1 erase
在 C++ 中,erase 是用于删除容器中元素的成员函数,不同容器的 erase 用法差异较大(核心取决于容器的底层实现)。以下按常用容器分类,详细讲解 erase 的用法、注意事项及示例。
一、核心容器的 erase 用法
1. 顺序容器(vector/string/deque)
顺序容器底层是连续内存,erase 会删除指定元素 / 区间,并自动调整后续元素的位置(导致后续元素前移),返回值为「删除后下一个元素的迭代器」(关键!用于遍历删除时避免迭代器失效)。
(1)vector 的 erase
vector 有两种重载:
- ① 删除单个元素:
iterator erase(iterator pos)传入要删除元素的迭代器,返回删除后下一个元素的迭代器。 - ② 删除区间元素:
iterator erase(iterator first, iterator last)传入区间[first, last)(左闭右开),删除该区间内所有元素,返回删除后下一个元素的迭代器。
示例 1:删除单个元素
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<int> vec = {1, 2, 3, 4, 5};
// 删除元素 3(先通过 find 找到迭代器,需包含 <algorithm>)
auto it = find(vec.begin(), vec.end(), 3);
if (it != vec.end()) { // 必须先判断迭代器是否有效(避免越界)
vec.erase(it); // 删除后,vec 变为 [1,2,4,5]
}
for (int x : vec) cout << x << " "; // 输出:1 2 4 5
return 0;
}
示例 2:删除区间元素
vector<int> vec = {1, 2, 3, 4, 5};
// 删除索引 1~3 的元素(即 2、3、4),区间是 [begin+1, begin+4)
vec.erase(vec.begin() + 1, vec.begin() + 4);
for (int x : vec) cout << x << " "; // 输出:1 5
示例 3:遍历删除特定元素(关键!避免迭代器失效)顺序容器删除元素后,当前迭代器及后续迭代器会失效(因为元素前移),直接 it++ 会出错。正确做法是用 erase 的返回值更新迭代器:
vector<int> vec = {1, 2, 2, 3, 2, 4};
// 删除所有值为 2 的元素
for (auto it = vec.begin(); it != vec.end(); ) { // 注意:循环内不写 it++
if (*it == 2) {
it = vec.erase(it); // 删除后,it 指向后续有效元素
} else {
it++; // 不删除则正常迭代
}
}
for (int x : vec) cout << x << " "; // 输出:1 3 4
(2)string 的 erase
string 的 erase 用法与 vector 类似,但额外支持「按索引 + 长度」删除(更便捷),重载如下:
- ①
iterator erase(iterator pos):删除单个字符(迭代器版) - ②
iterator erase(iterator first, iterator last):删除区间字符 - ③
string& erase(size_t pos = 0, size_t len = npos):按索引删除(最常用)pos:起始索引(默认从 0 开始)len:要删除的字符数(默认npos,表示删除到字符串末尾)
示例:string 的 erase 用法
#include <string>
#include <iostream>
using namespace std;
int main() {
string s = "hello world";
// 1. 按索引删除:从索引 5 开始删除 1 个字符(删除空格)
s.erase(5, 1);
cout << s << endl; // 输出:helloworld
// 2. 迭代器删除:删除第一个 'l'
auto it = find(s.begin(), s.end(), 'l');
if (it != s.end()) {
s.erase(it);
}
cout << s << endl; // 输出:heloworld
// 3. 区间删除:删除前 3 个字符
s.erase(s.begin(), s.begin() + 3);
cout << s << endl; // 输出:oworld
return 0;
}
(3)deque 的 erase
deque(双端队列)的 erase 用法与 vector 完全一致(支持单个元素 / 区间删除,返回下一个迭代器),但注意:deque 不支持 begin() + n 直接偏移(需用 next 函数)。
示例:deque 删除元素
#include <deque>
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
deque<int> dq = {10, 20, 30, 40};
// 删除元素 30
auto it = find(dq.begin(), dq.end(), 30);
if (it != dq.end()) {
dq.erase(it); // dq 变为 [10,20,40]
}
// 删除区间(从第 2 个元素到末尾,即 20、40)
dq.erase(next(dq.begin(), 1), dq.end()); // next 函数偏移迭代器
for (int x : dq) cout << x << " "; // 输出:10
return 0;
}
2. 关联容器(set/map/unordered_set/unordered_map)
关联容器底层是红黑树(set/map)或哈希表(unordered_*),元素按键有序 / 无序存储,删除元素后,仅被删除元素的迭代器失效,其他迭代器仍有效。
erase 有三种重载(更灵活):
- ①
iterator erase(iterator pos):删除迭代器指向的元素,返回下一个元素的迭代器 - ②
size_type erase(const key_type& key):按「键」删除元素,返回删除的元素个数(0 或 1,因为关联容器键唯一) - ③
iterator erase(iterator first, iterator last):删除区间[first, last)元素
(1)set/unordered_set(无键值对,仅存元素)
示例:set 删除元素
#include <set>
#include <iostream>
using namespace std;
int main() {
set<int> st = {1, 2, 3, 4, 5};
// 1. 按键删除(最常用)
size_t cnt = st.erase(3); // 删除键为 3 的元素,返回 1(删除成功)
cout << "删除个数:" << cnt << endl; // 输出:1
// 2. 迭代器删除
auto it = st.find(4); // 找到键为 4 的迭代器
if (it != st.end()) {
st.erase(it); // 删除后 st 变为 {1,2,5}
}
// 3. 区间删除(删除 2~5,左闭右开)
auto first = st.find(2);
auto last = st.find(5);
st.erase(first, last); // 删除 2,st 变为 {1,5}
for (int x : st) cout << x << " "; // 输出:1 5
return 0;
}
(2)map/unordered_map(键值对 key-value)
map 的 erase 按「键」删除时,会删除整个键值对;迭代器指向的是 pair<const key_type, mapped_type>。
示例:map 删除元素
#include <map>
#include <iostream>
using namespace std;
int main() {
map<string, int> mp = {{"a", 1}, {"b", 2}, {"c", 3}};
// 1. 按键删除
mp.erase("b"); // 删除键 "b" 的键值对,mp 变为 {{"a",1}, {"c",3}}
// 2. 迭代器删除
auto it = mp.find("a");
if (it != mp.end()) {
mp.erase(it); // 删除后 mp 变为 {{"c",3}}
}
// 3. 遍历删除(键大于 "c" 的元素,此处无)
for (auto it = mp.begin(); it != mp.end(); ) {
if (it->first > "c") {
it = mp.erase(it); // 关联容器也建议用返回值更新迭代器(兼容部分实现)
} else {
it++;
}
}
for (auto& [k, v] : mp) cout << k << ":" << v << " "; // 输出:c:3
return 0;
}
3. 容器适配器(stack/queue/priority_queue)
容器适配器(栈、队列、优先队列)没有 erase 成员函数!因为它们的设计目标是「限制访问方式」(仅允许从两端操作),不支持随机删除元素。
若需删除适配器中的元素,需先将元素转移到其他容器(如 vector),删除后再重新构造适配器:
#include <stack>
#include <vector>
#include <iostream>
using namespace std;
int main() {
stack<int> st;
st.push(1);
st.push(2);
st.push(3);
// 转移到 vector
vector<int> vec;
while (!st.empty()) {
vec.push_back(st.top());
st.pop();
}
// 删除元素 2
vec.erase(find(vec.begin(), vec.end(), 2));
// 重新构造 stack
stack<int> new_st;
for (int x : vec) new_st.push(x);
// 输出 stack(注意:栈是后进先出,输出顺序为 3、1)
while (!new_st.empty()) {
cout << new_st.top() << " ";
new_st.pop();
} // 输出:3 1
return 0;
}
二、关键注意事项
-
迭代器失效问题(最容易踩坑!)
- 顺序容器(
vector/string/deque):删除元素后,当前迭代器及后续迭代器失效,必须用erase的返回值更新迭代器。 - 关联容器(
set/map等):仅被删除元素的迭代器失效,其他迭代器有效,但建议用erase的返回值更新迭代器(兼容所有实现)。
- 顺序容器(
-
erase的参数有效性- 迭代器参数必须是「指向容器内有效元素」的迭代器(不能是
end()或非法迭代器),否则会触发未定义行为(崩溃)。 - 按索引删除(
string/vector)时,pos必须小于容器大小,否则越界。
- 迭代器参数必须是「指向容器内有效元素」的迭代器(不能是
-
效率差异
- 顺序容器:
erase是 O (n) 时间(需移动后续元素),频繁删除中间元素效率低。 - 关联容器:
erase是 O (log n) 时间(红黑树)或 O (1) 平均时间(哈希表),按键删除效率高。
- 顺序容器:
-
list的erase(补充)list(双向链表)的erase用法与vector类似(支持单个 / 区间删除,返回下一个迭代器),但删除时仅当前迭代器失效,其他迭代器有效,且效率为 O (1)(无需移动元素):list<int> lst = {1,2,3,4}; auto it = find(lst.begin(), lst.end(), 2); if (it != lst.end()) { lst.erase(it); // O(1) 效率 }
三、总结
| 容器类型 | erase 支持形式 | 迭代器失效情况 | 时间复杂度 |
|---|---|---|---|
vector/string | 单个元素、区间 | 当前及后续迭代器失效 | O(n) |
deque | 单个元素、区间 | 中间删除时两端迭代器可能失效 | O(n) |
set/map | 单个元素、区间、按键删除 | 仅被删除元素迭代器失效 | O(log n) |
unordered_* | 单个元素、区间、按键删除 | 仅被删除元素迭代器失效(哈希表) | O (1) 平均 |
| 适配器(栈 / 队列) | 不支持 需转移到其他容器操作 | - | - |
核心口诀:顺序容器删后更迭代器,关联容器按键删最方便,适配器无 erase 需中转。
2 'int left , right = 0 ;'变量声明语法的混淆
其实 C++ 是允许 int left, right = 0; 这种变量声明语法的,但它的行为和你想的不一样 —— 这是你混淆的核心原因!
一、先明确:C++ 中 int left, right = 0; 的真实含义
这种声明属于 “变量声明的逗号分隔语法”,规则是:
- 逗号是声明分隔符,不是赋值运算符;
- 只有最后一个变量(
right)被显式赋值为0; - 前面的变量(
left)没有被初始化,其值是「随机垃圾值」(取决于内存当前状态)。
简单说:
int left, right = 0;
// 等价于:
int left; // 未初始化(值随机)
int right = 0; // 初始化值为0
这和你想要的「left 和 right 都初始化为 0」完全不同!你想要的正确写法是:
int left = 0, right = 0; // 两个变量都初始化
二、哪些语言允许「逗号分隔声明时,统一赋值」?
你混淆的是其他语言的语法 —— 以下语言中,int left, right = 0; 会让两个变量都初始化为 0,和 C++ 不一样:
1. Python(最常见的混淆来源)
Python 中没有 “变量声明”,只有 “赋值语句”,逗号表示「解包赋值」:
left, right = 0 # 错误(解包数量不匹配)
left, right = 0, 0 # 正确:left=0,right=0
left = right = 0 # 正确:链式赋值,两个都为0
很多人会把 Python 的 left = right = 0 误套到 C++ 中,写成 int left, right = 0;,但两者语法完全不同。
2. JavaScript
JS 中逗号是「表达式分隔符」,let left, right = 0; 里 left 会被初始化为 undefined,right 为 0—— 其实和 C++ 类似?不对,再看:
let left = right = 0; // 正确:left=0,right=0(链式赋值)
let left, right = 0; // left=undefined,right=0(和 C++ 行为一致)
JS 的「链式赋值」和 Python 一样,但「逗号分隔声明」和 C++ 行为相同(前面变量未初始化)。
3. Java(和 C++ 行为完全一致)
Java 的变量声明规则和 C++ 几乎一样:
int left, right = 0;
// 等价于:int left;(未初始化,编译时若直接使用会报错)
// int right = 0;
Java 甚至比 C++ 严格:如果 left 是局部变量,未初始化就使用(比如 right - left),会直接编译报错;而 C++ 是 “运行时垃圾值”,更难排查。
4. Go 语言(行为特殊)
Go 中变量声明的「短变量声明」(:=)会自动推导类型,逗号分隔时所有变量都会被初始化:
left, right := 0, 0 // 正确:left=0,right=0(推导为int)
left, right := 0 // 错误(语法不允许,必须对应数量)
var left, right int = 0 // 错误(赋值数量不匹配)
var left, right int = 0, 0 // 正确
Go 的规则和 C++/Java 不同,但也需要显式给出两个初始值。
三、核心总结:为什么你会混淆?
- 把 “赋值逻辑” 和 “声明逻辑” 搞混:C++ 中
int a, b = x;是「声明两个变量,只给 b 赋值」;而 Python/JS 的a = b = x是「赋值语句,a 和 b 都等于 x」,两者语法本质不同。 - 忽略了 “未初始化变量” 的风险:C++ 局部变量未初始化时,值是随机的(运行时错误,难排查);而 Java 会编译报错,Python 没有 “未初始化” 概念(变量必须赋值后才能用),导致你没意识到 C++ 的这个坑。
四、避坑建议
无论用哪种语言,声明多个同类型变量并初始化时,明确每个变量的初始值,不要依赖 “逗号分隔 + 单个赋值”:
- C++/Java:
int left = 0, right = 0;(显式给每个变量赋值) - Python:
left = right = 0或left, right = 0, 0(两种都安全) - Go:
left, right := 0, 0(短变量声明,必须成对赋值)
3 无序容器系列
Leetcode 66 几乎唯一子数组的最大和-CSDN博客
这道题目用到了。
C++ 标准库中的 无序容器系列(以 unordered_ 为前缀),最常用的就是你代码里的 unordered_map—— 它是实现「元素计数」的关键工具,我们结合你的代码场景,把 unordered 相关的核心知识点讲清楚:
一、先明确:unordered_map 是什么?
unordered_map 是 C++ 中的 无序键值对容器,本质是「哈希表(Hash Table)」实现的,核心特点是:
- 存储结构:
key -> value(键值对),比如你代码中count[c] = 出现次数,c是key(数组元素),value是该元素的出现次数; - 核心优势:查找、插入、删除的时间复杂度接近 O (1)(比有序的
map快很多,map是 O (log n)); - 「无序」的含义:键值对的存储顺序不固定(不按
key排序),只关心key和value的映射关系,不关心顺序。
二、为什么代码不用 map,而用 unordered_map?
你的场景是「统计滑动窗口中元素的出现次数」,需要频繁做 3 件事:
- 插入元素(右移窗口时,
count[c]++); - 查询元素出现次数(
count[c] == 0判断是否首次出现); - 更新元素出现次数(左移窗口时,
count[d]--)。
这 3 件事用 unordered_map 都是 O (1) 效率,而如果用有序的 map(红黑树实现),是 O (log n) 效率 —— 对于数组长度较大的情况,unordered_map 会快很多,这也是代码选择它的核心原因。
三、unordered 系列的其他常用容器
除了 unordered_map,C++ 还有几个常用的 unordered 容器,用途不同,对比看更清晰:
| 容器名 | 本质 | 核心用途 | 代码中可替代场景? |
|---|---|---|---|
unordered_map | 哈希表 (键值对) | 存储「键 - 值」映射(如计数) | 代码核心,不可替代 |
unordered_set | 哈希表 (单值集合) | 存储不重复的元素(如去重) | 若只需判断元素是否存在,可替代(但无法存出现次数) |
unordered_multimap | 哈希表 (多键值对) | 允许同一个 key 对应多个 value | 你的场景用不上(只需统计次数,一个 key 对应一个次数值) |
unordered_multiset | 哈希表 (多值集合) | 允许存储重复元素(但无次数) | 你的场景用不上(需要知道出现次数,而非仅存在) |
四、unordered_map 的关键特性(结合你的代码)
你代码中用到了 unordered_map 的 2 个核心特性,必须理解:
-
键不存在时,默认值为 0比如你代码中
count[c] == 0:当c是第一次加入窗口时,count中还没有c这个key,此时访问count[c]会自动创建这个键值对,value默认为 0(因为value是int类型)。这就是为什么「首次出现的元素会触发unique++」—— 本质是利用了unordered_map键不存在时的默认初始化。 -
键的唯一性同一个
key只能对应一个value,比如你代码中count[c]++:如果c已经存在,只会更新它的value(次数 + 1),不会重复创建key,保证了计数的准确性。
五、unordered_map 与 map 的核心区别(避免用混)
| 特性 | unordered_map(哈希表) | map(红黑树) |
|---|---|---|
| 存储顺序 | 无序(不按 key 排序) | 有序(按 key 升序) |
| 时间复杂度 | 插入 / 查询 / 删除 O (1)(平均) | 插入 / 查询 / 删除 O (log n) |
| 键的要求 | key 必须支持哈希函数 | key 必须支持比较运算符(如 <) |
| 适用场景 | 计数、快速查找(无需排序) | 需按 key 排序的场景(如字典序输出) |
你的代码是「计数 + 快速查找」,完全不需要排序,所以 unordered_map 是最优选择。
六、补充:unordered_set 在类似场景的用法
如果题目只需要判断「元素是否在窗口中」,不需要统计出现次数,就可以用 unordered_set(单值集合,自动去重):
unordered_set<int> exist; // 存储窗口中存在的元素
// 加入元素 c
if (exist.find(c) == exist.end()) { // c 不在集合中(首次出现)
unique++;
exist.insert(c);
}
// 移除元素 d
exist.erase(d);
if (exist.find(d) == exist.end()) { // d 移除后不在集合中
unique--;
}
但你的代码需要统计出现次数(比如元素重复出现时,unique 不能增加),所以必须用 unordered_map 存次数,而不能用 unordered_set。
总结:你代码中的 unordered_map 是「无序哈希表」,核心作用是 O (1) 效率统计元素出现次数,完美匹配滑动窗口的需求;unordered 前缀代表「无序」,是它和有序容器 map 的核心区别,也是它效率更高的原因
1061

被折叠的 条评论
为什么被折叠?



