40、STL算法(数值与变换):accumulate、transform、copy、replace
说实话,STL算法库是我日常开发中使用频率最高的工具之一。尤其是数值计算和元素变换这组算法,几乎每个项目都会碰到。今天我们就来聊聊四个最常用的家伙:accumulate、transform、copy 和 replace。
嗯,先别急着看代码。我建议你先理解它们的定位——accumulate 是「归约」,transform 是「映射」,copy 是「搬运」,replace 是「替换」。说白了,这四兄弟覆盖了数据处理中最常见的四种操作模式。
1. accumulate:累加与自定义归约
std::accumulate 定义在 头文件中。它的核心作用是对一个范围内的元素做「累积」操作。默认是加法,但你可以传入任意二元操作符。
#include <numeric>
#include <vector>
#include <iostream>
int main() {
std::vector<int> data = {1, 2, 3, 4, 5};
// 默认累加:1+2+3+4+5 = 15
int sum = std::accumulate(data.begin(), data.end(), 0);
std::cout << "sum = " << sum << std::endl;
// 自定义操作:累乘 1*2*3*4*5 = 120
int product = std::accumulate(data.begin(), data.end(), 1,
std::multiplies<int>());
std::cout << "product = " << product << std::endl;
return 0;
}
我的经验:初始值类型决定了返回类型。如果你传 0(int),返回 int;如果传 0.0(double),返回 double。我在项目中曾因为初始值类型写错,导致浮点精度丢失,排查了半天。
你想想看,accumulate 的第三个参数不仅仅是初始值,它还参与了类型推导。这一点很多人会忽略。
2. transform:批量映射
std::transform 是「一对一」变换的利器。它遍历输入范围,对每个元素应用函数,结果写入输出范围。输出范围可以和输入范围重叠(就地变换)。
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst(src.size());
// 一元变换:每个元素乘以2
std::transform(src.begin(), src.end(), dst.begin(),
[](int x) { return x * 2; });
// 二元变换:两个序列对应元素相加
std::vector<int> a = {1, 2, 3};
std::vector<int> b = {10, 20, 30};
std::vector<int> c(a.size());
std::transform(a.begin(), a.end(), b.begin(), c.begin(),
[](int x, int y) { return x + y; });
return 0;
}
关键点:transform 有两种重载——一元版本和二元版本。二元版本需要两个输入范围,第三个参数是第二个范围的起始迭代器。注意:第二个范围的长度必须 >= 第一个范围。
我个人习惯用 transform 替代手写 for 循环。代码更简洁,意图更明确。而且编译器对 STL 算法的优化通常比手写循环更好。
3. copy:元素搬运工
std::copy 是最基础的复制算法。它把一段范围内的元素复制到另一段连续内存中。听起来简单,但有几个坑需要注意。
#include <algorithm>
#include <vector>
#include <iostream>
#include <iterator>
int main() {
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst;
// 错误做法:dst为空,copy会崩溃
// std::copy(src.begin(), src.end(), dst.begin());
// 正确做法1:提前分配空间
dst.resize(src.size());
std::copy(src.begin(), src.end(), dst.begin());
// 正确做法2:使用back_inserter
std::vector<int> dst2;
std::copy(src.begin(), src.end(), std::back_inserter(dst2));
// 输出到控制台
std::copy(dst.begin(), dst.end(),
std::ostream_iterator<int>(std::cout, " "));
return 0;
}
我曾经踩过的坑:用 copy 往空 vector 里写数据,忘记提前 resize 或使用 back_inserter,结果程序直接崩溃。记住:copy 不会自动扩容,目标容器必须有足够的空间。
为什么会这样?因为 copy 只是做内存级别的复制,它不关心目标容器的容量管理。这是 C++ 的设计哲学——不为你做你不明确要求的事情。
4. replace:条件替换
std::replace 和 std::replace_if 用于替换范围内的元素。前者替换特定值,后者替换满足条件的值。
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> data = {1, 2, 3, 2, 4, 2, 5};
// 将所有2替换为99
std::replace(data.begin(), data.end(), 2, 99);
// 将所有大于3的元素替换为0
std::replace_if(data.begin(), data.end(),
[](int x) { return x > 3; }, 0);
// 注意:replace是就地修改,不会创建新容器
for (int x : data) {
std::cout << x << " ";
}
return 0;
}
小技巧:如果你需要保留原数据,先 copy 一份再 replace。或者用 std::transform 配合条件表达式实现「复制+替换」一步到位。
知识体系总览
下面这张图帮你理清这四个算法的关系和使用场景:
综合示例:数据清洗流水线
在实际项目中,这四个算法经常组合使用。下面是一个数据清洗的例子:
#include <numeric>
#include <algorithm>
#include <vector>
#include <iostream>
#include <iterator>
int main() {
std::vector<int> raw = {1, -2, 3, -4, 5, 0, -6, 7};
// 第一步:copy 一份原始数据
std::vector<int> cleaned;
std::copy(raw.begin(), raw.end(), std::back_inserter(cleaned));
// 第二步:replace_if 将所有负数替换为0
std::replace_if(cleaned.begin(), cleaned.end(),
[](int x) { return x < 0; }, 0);
// 第三步:transform 将每个元素加1
std::transform(cleaned.begin(), cleaned.end(), cleaned.begin(),
[](int x) { return x + 1; });
// 第四步:accumulate 计算总和
int total = std::accumulate(cleaned.begin(), cleaned.end(), 0);
std::cout << "清洗后数据: ";
std::copy(cleaned.begin(), cleaned.end(),
std::ostream_iterator<int>(std::cout, " "));
std::cout << "\n总和: " << total << std::endl;
return 0;
}
输出结果:
清洗后数据: 2 1 4 1 6 1 1 8
总和: 24
你看,四个算法配合起来,数据处理的流水线非常清晰。每个步骤只做一件事,代码可读性极高。这也是我为什么一直推荐团队多用 STL 算法——它强迫你把逻辑拆分成小的、可组合的步骤。
最后说一句:accumulate 在 C++17 之后有了并行版本 std::reduce,如果你处理的数据量很大,可以考虑升级。不过那是另一个话题了,今天先到这里。
公众号:蓝海资料掘金营,微信deep3321