cpp 08 erase函数 | 变量声明语法 | 无序容器系列(unordered)

1 erase

C++ 容器类 <set> | 菜鸟教程

C++ vector 容器 | 菜鸟教程

在 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;
}

二、关键注意事项

  1. 迭代器失效问题(最容易踩坑!)

    • 顺序容器(vector/string/deque):删除元素后,当前迭代器及后续迭代器失效,必须用 erase 的返回值更新迭代器。
    • 关联容器(set/map 等):仅被删除元素的迭代器失效,其他迭代器有效,但建议用 erase 的返回值更新迭代器(兼容所有实现)。
  2. erase 的参数有效性

    • 迭代器参数必须是「指向容器内有效元素」的迭代器(不能是 end() 或非法迭代器),否则会触发未定义行为(崩溃)。
    • 按索引删除(string/vector)时,pos 必须小于容器大小,否则越界。
  3. 效率差异

    • 顺序容器:erase 是 O (n) 时间(需移动后续元素),频繁删除中间元素效率低。
    • 关联容器:erase 是 O (log n) 时间(红黑树)或 O (1) 平均时间(哈希表),按键删除效率高。
  4. 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

需转移到其他容器操作

--

核心口诀:顺序容器删后更迭代器,关联容器按键删最方便,适配器无 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 会被初始化为 undefinedright 为 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 不同,但也需要显式给出两个初始值。

三、核心总结:为什么你会混淆?

  1. 把 “赋值逻辑” 和 “声明逻辑” 搞混:C++ 中 int a, b = x; 是「声明两个变量,只给 b 赋值」;而 Python/JS 的 a = b = x 是「赋值语句,a 和 b 都等于 x」,两者语法本质不同。
  2. 忽略了 “未初始化变量” 的风险:C++ 局部变量未初始化时,值是随机的(运行时错误,难排查);而 Java 会编译报错,Python 没有 “未初始化” 概念(变量必须赋值后才能用),导致你没意识到 C++ 的这个坑。

四、避坑建议

无论用哪种语言,声明多个同类型变量并初始化时,明确每个变量的初始值,不要依赖 “逗号分隔 + 单个赋值”:

  • C++/Java:int left = 0, right = 0;(显式给每个变量赋值)
  • Python:left = right = 0 或 left, right = 0, 0(两种都安全)
  • Go:left, right := 0, 0(短变量声明,必须成对赋值)

无序容器系列

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 件事:

  1. 插入元素(右移窗口时,count[c]++);
  2. 查询元素出现次数(count[c] == 0 判断是否首次出现);
  3. 更新元素出现次数(左移窗口时,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 个核心特性,必须理解:

  1. 键不存在时,默认值为 0比如你代码中 count[c] == 0:当 c 是第一次加入窗口时,count 中还没有 c 这个 key,此时访问 count[c] 会自动创建这个键值对,value 默认为 0(因为 value 是 int 类型)。这就是为什么「首次出现的元素会触发 unique++」—— 本质是利用了 unordered_map 键不存在时的默认初始化。

  2. 键的唯一性同一个 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 的核心区别,也是它效率更高的原因

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值