四招轻松从C++序列容器中移除元素,你学会了吗?

一、简介

本文讨论从集合中删除元素的STL算法。从C++集合中删除一个元素可能不复杂,也可能有点复杂。

删除元素的方法在序列容器和关联容器之间是非常不同的。在序列容器中,vector 和 string 是最常用的。但这里也会介绍 deque 和 list 以供全面了解,尽管在一般情况下可能不会使用它们。

至少有四种方法可以指定从任何容器中删除哪些值:

    1    在给定位置(或在两个给定位置之间)删除元素;

    2    删除等于某个值的元素;

    3    删除满足某个谓词的元素

    4    以及删除重复项。

下面来看看如何在STL序列容器中实现这四种命令。

二、移除给定位置的元素

这是最简单的方法。如果是一个序列容器,可以通过调用erase。比如:

c.erase(position);

要移除由迭代器first和last组成的子范围中的元素,可以这么调用:

c.erase(first, last);

与STL中迭代器表示的所有范围一样,子范围包括first,而不包括last。last指向“past-the-end”元素,类似于容器的结束迭代器。

注意,对于vector和string,所有指向被移除对象所在位置和之后元素的迭代器都无效。因为所有这些元素都被erase函数调用移除了。

对于deque来说,会有一点点不同:参考cppreference.com,所有迭代器和引用都无效,除非被删除的元素位于容器的末尾或开头,在这种情况下,只有迭代器和对被删除元素的引用无效。

    •    如果删除的元素位于deque的中间位置,则所有指向该元素以及之后位置的迭代器和引用都会失效。

    •    如果删除的是末尾元素,那么仅仅指向这个末尾元素的迭代器和引用会失效,其余保持有效。

    •    如果删除的是开头元素,同样只有指向这个开头元素的迭代器和引用会失效。

这erase很简单,只是热身。下面还有复杂的,接着阅读学习吧。

三、移除与某个值相等的元素

3.1、序列容器vector、deque、string

这些容器没有删除值的方法,因此需要使用std::remove算法。该算法取一个要删除的范围和一个值,并上移所有要保留的元素。

例如,在这个整数范围内调用std::remove并带值42,会有以下行为:

注意,在范围末尾剩下的元素的值是未指定的。尽管有些实现可以将最初位于集合末尾的元素保留下来,但这是不可靠的。

要记住,在STL的设计中,算法只与迭代器交互,而不直接与容器交互,因此容器并不知道算法的效果。例如,它的size并没有缩小。

为了有效地从集合中删除元素,需要使用在本文前面讲到的erase方法。为此,要注意到std::remove返回一个迭代器,该迭代器指向不应被删除的元素范围内的“past-the-end”元素。即,要删除的元素位于std::remove返回的迭代器定义的范围和集合的末尾。

因此,要有效地从vector、deque或string对象中删除值,可以这样写:

v.erase(std::remove(begin(v), end(v), 42), end(v));

3.2、封装成模板方法

这是C++的习惯用法,如果在代码中遇到它,必须知道。但是,坦白地讲,不觉得用这么多代码来表达这么简单的事情有点多吗?难道不喜欢像下面这样写吗?

v.remove(42);

// or

v.erase(42);

但我们是没有权限给vector添加方法的。但可以编写一个带有简单接口的自由函数,它接受一个向量并删除它的一些元素!

template<typename T>

void erase(std::vector<T>& vector, T const& value)

{

    vector.erase(std::remove(begin(vector), end(vector), value), end(vector));

}

也可以给它添加一些重载来操作deque和string对象:

template<typename T>

void erase(std::deque<T>& deque, T const& value)

{

    deque.erase(std::remove(begin(deque), end(deque), value), end(deque));

}

 

void erase(std::string& string, char letter)

{

    string.erase(std::remove(begin(string), end(string), letter), end(string));

}

非常建议实现这些辅助函数,特别是对于最常用的vector。这可以避免标准习惯用法所带来的迭代器的纠缠。

甚至在C++标准中,就有学者提出了一个增加这种泛型函数的建议。很遗憾的是,它还没有在C++ 17中实现。

3.3、list的remove成员函数

为了全面起见,这里提一下要从list中删除一个元素,有一个叫做remove的方法,例如:

l.remove(42);

由于它不提供随机访问迭代器,在列表上使用std::remove算法会使列表变得比现在更慢。

四、删除满足谓词的元素

前面已经看到了如何从序列容器中删除所有等于某个值的元素,比如42。那么,如何移除满足谓词func的元素?其实,这完全一样,只是需要使用 remove_if 而不是 remove。

所以只需要替换:

    •    remove为remove_if;

    •    42为func。

std::remove_if(begin(string), end(string), func)

和上一节一样。依然建议编写一个名为erase_if的自由函数,以避免大量迭代器的出现;并且list同样有一个名为remove_if的成员方法。因此,为了遵循“不要重复”的原则和避免文章篇幅过长,这里不再对remove_if进行更多讨论。

五、从序列容器中删除重复项

从序列容器中删除重复项的STL算法是std::unique。但是要注意!unique只删除相邻的重复项,而不删除整个集合中的重复项。它具有线性复杂度。

除此之外,unique和remove非常相似。它只压缩集合的元素,而不能改变容器本身size。因此,需要在容器上调用erase才能有效地删除重复项:

vector.erase(std::unique(begin(v), end(v)), end(v));

和remove一样,封装一个方便的函数是必要的:

template<typename T>

void unique(std::vector<T>& vector)

{

    vector.erase(std::unique(begin(vector), end(vector)), end(vector));

}

 

template<typename T>

void unique(std::deque<T>& deque)

{

    deque.erase(std::unique(begin(deque), end(deque)), end(deque));

}

 

void unique(std::string& string)

{

    string.erase(std::unique(begin(string), end(string)), end(string));

}

与remove类似,std::list有一个unique的成员方法。

 

六、总结

这就是C++中从序列容器中删除元素的方法。

在C++中,从序列容器中删除元素的方法多种多样,每种方法都有其适用的场景和使用方式。

    1    移除给定位置的元素:可以使用erase方法,通过指定要删除的元素位置或者给定范围的迭代器来实现。

    2    移除与某个值相等的元素:对于vector、deque和string等序列容器,可以使用std::remove算法,并结合erase方法来删除指定值的元素;或者封装成模板函数来简化操作。

    3    删除满足谓词的元素:使用std::remove_if算法结合erase方法,可以删除满足指定谓词条件的元素。

    4    从序列容器中删除重复项:利用std::unique算法可以删除相邻的重复项,但需要注意该算法只删除相邻的重复项,并且不能改变容器的大小,需要配合erase方法来实现删除整个容器中的重复项。

 

<think>我们讨论的是从C++容器移除数值偏差较大的元素。偏差较大通常指远离数据集的中心趋势(如均值或中位数),可能通过标准差或其他离群值检测方法来判断。 常见方法: 1. 计算均值和标准差,移除偏离均值超过k个标准差的元素(例如k=2或3)。 2. 使用中位数和绝对中位差(MAD)作为更稳健的离群值检测。 步骤: a) 计算数据集的统计量(均值、标准差,或中位数、MAD)。 b) 设定阈值,判断每个元素是否偏离过大。 c) 移除离群值。 注意:C++标准库没有内置的离群值移除函数,需要自己实现。 我们将以两种方式展示: 方式1:使用均值和标准差(对离群值敏感,因为离群值会影响均值)。 方式2:使用中位数和MAD(更稳健,因为中位数不受极端值影响)。 假设我们有一个vector<double>,并移除离群值。 方式1:基于均值和标准差 步骤: 1. 计算均值(mean)和标准差(stddev)。 2. 设定阈值,例如:threshold = mean ± k * stddev(通常k=2或3)。 3. 遍历容器移除不在[mean - k*stddev, mean + k*stddev]范围内的元素。 方式2:基于中位数和绝对中位差(MAD) 步骤: 1. 计算中位数(median)。 2. 计算每个元素与中位数的绝对偏差:|x_i - median|。 3. 计算这些绝对偏差的中位数,记为MAD。 4. 设定阈值:通常用中位数 ± k * MAD(k常取3,因为对于正态分布,3倍MAD大约相当于3个标准差)。 5. 移除不在[median - k*MAD, median + k*MAD]范围内的元素。 注意:MAD需要乘以一个常数(约1.4826)来估计标准差,但如果我们只是用k来调整阈值,可以不用乘,而是直接调整k值(比如取较大的k)。不过,为了与标准差方法可比,通常会将MAD调整为与标准差同尺度:调整后的MAD = 1.4826 * MAD。但这里我们为了简单,直接使用MAD,并调整k。 在C++中,我们可以使用标准库中的算法(如sort,accumulate)和数学函数。 由于移除元素会导致迭代器失效,我们可以使用erase-remove惯用法。 我们将分别实现这两种方法。 注意:离群值检测方法有很多,这里只展示两种基本方法。 代码示例: 方式1:基于均值和标准差</think>### 从C++容器移除数值偏差较大的元素C++移除数值偏差较大的元素(离群值),通常需要以下步骤: 1. 计算数据集的**中心趋势**(均值或中位数) 2. 计算**离散程度**(标准差或绝对中位差) 3. 设定偏差阈值 4. 使用标准库算法移除元素 以下是两种常用方法: #### 方法1:基于均值和标准差(适用于正态分布) ```cpp #include <vector> #include <algorithm> #include <cmath> #include <numeric> void remove_outliers_stddev(std::vector<double>& data, double k) { if (data.empty()) return; // 计算均值 double mean = std::accumulate(data.begin(), data.end(), 0.0) / data.size(); // 计算标准差 double sq_sum = std::inner_product(data.begin(), data.end(), data.begin(), 0.0); double stddev = std::sqrt(sq_sum / data.size() - mean * mean); // 移除偏离均值超过k倍标准差的元素 data.erase(std::remove_if(data.begin(), data.end(), [=](double x) { return std::abs(x - mean) > k * stddev; }), data.end()); } ``` #### 方法2:基于中位数和绝对中位差(MAD,鲁棒性更强) ```cpp #include <vector> #include <algorithm> #include <cmath> void remove_outliers_mad(std::vector<double>& data, double k) { if (data.empty()) return; // 计算中位数 std::vector<double> sorted = data; std::sort(sorted.begin(), sorted.end()); double median = sorted[sorted.size() / 2]; // 计算绝对中位差(MAD) std::vector<double> abs_deviations; for (double x : data) { abs_deviations.push_back(std::abs(x - median)); } std::sort(abs_deviations.begin(), abs_deviations.end()); double mad = abs_deviations[abs_deviations.size() / 2] * 1.4826; // 正态分布修正系数 // 移除偏离中位数超过k倍MAD的元素 data.erase(std::remove_if(data.begin(), data.end(), [=](double x) { return std::abs(x - median) > k * mad; }), data.end()); } ``` #### 使用示例 ```cpp int main() { std::vector<double> data = {1.0, 1.1, 1.2, 5.0, 1.3, 1.4, 10.0}; // 含离群值5.0和10.0 // 方法1:移除偏离均值2个标准差以上的元素 remove_outliers_stddev(data, 2.0); // 方法2:移除偏离中位数3倍MAD的元素(更鲁棒) // remove_outliers_mad(data, 3.0); } ``` ### 关键说明 1. **阈值选择**: - 标准差方法:`k=2` 约排除5%离群值,`k=3` 约排除0.3%[^1] - MAD方法:对极端值不敏感,更适合存在显著离群值的数据集 2. **算法复杂度**: - 方法1:$O(n)$ 时间复杂度 - 方法2:$O(n \log n)$ 主要来自排序 3. **容器适配**: - 上述代码针对`vector`,其他容器需调整删除逻辑 - 关联容器(如`set`)可直接使用`erase_if`(C++20) > 当处理关键数据时,建议结合可视化工具验证离群值移除效果[^2]。 --- ### 相关问题 1. 如何在C++中实现动态调整离群值检测阈值? 2. 对于时间序列数据,有哪些特殊的离群值检测技术? 3. 如何为多维数据(如点云)实现离群值移除? 4. C++的`erase-remove`惯用法在关联容器中如何实现? [^1]: 基于正态分布的3σ原则。 [^2]: 可使用Matplotlib或C++的绘图库进行数据分布可视化。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值