20. 修改算法:copy、replace、fill、transform、remove、unique
修改算法,说白了就是「改数据」的那一类函数。它们不会改变容器的大小,但会改变容器里元素的值,或者把元素挪来挪去。我个人习惯把它们分成两类:一类是原地修改,直接改原容器;另一类是拷贝式修改,把结果写到另一个地方去。
这一章我们重点聊六个:copy、replace、fill、transform、remove、unique。嗯,这里面有几个名字容易让人误解——比如 remove 其实并没有真的删除元素,只是把不要的挪到后面去了。你想想看,这坑我当年踩过不止一次。
20.1 copy —— 最朴素的复制
copy 的用法非常直白:把一段区间里的元素,复制到另一个地方去。它的签名长这样:
OutputIt copy(InputIt first, InputIt last, OutputIt d_first);
注意,目标区间必须有足够的空间。我曾经在项目里看到有人直接往空的 vector 里 copy,结果程序崩得莫名其妙。正确的做法是先 resize 或者用 back_inserter。
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dst(5); // 提前分配好空间
std::copy(src.begin(), src.end(), dst.begin());
for (int x : dst) std::cout << x << " ";
// 输出: 1 2 3 4 5
}
如果你不想提前算大小,用 std::back_inserter 会更安全:
std::vector<int> dst2;
std::copy(src.begin(), src.end(), std::back_inserter(dst2));
20.2 replace —— 替换指定值
replace 用来把区间里所有等于某个值的元素,换成另一个值。签名:
void replace(ForwardIt first, ForwardIt last,
const T& old_value, const T& new_value);
举个例子:
std::vector<int> v = {1, 2, 3, 2, 4, 2, 5};
std::replace(v.begin(), v.end(), 2, 99);
// v 变成: 1 99 3 99 4 99 5
还有 replace_if,它接受一个谓词,满足条件的才替换。我记得有一次做日志清洗,需要把所有负数替换成 0,用 replace_if 一行搞定。
std::replace_if(v.begin(), v.end(),
[](int x) { return x < 0; }, 0);
20.3 fill —— 批量填充
fill 把区间里的所有元素设成同一个值。签名:
void fill(ForwardIt first, ForwardIt last, const T& value);
最常见的用法是初始化数组或 vector:
std::vector<int> v(10);
std::fill(v.begin(), v.end(), 42);
// v 里全是 42
还有 fill_n,只填前 n 个:
std::fill_n(v.begin(), 5, 100); // 前5个变成100
20.4 transform —— 最灵活的修改器
transform 是我个人用得最多的修改算法。它接受一个一元或二元操作函数,对每个元素做变换,结果可以写到原区间,也可以写到另一个区间。
一元版本:
OutputIt transform(InputIt first, InputIt last,
OutputIt d_first, UnaryOp unary_op);
二元版本:
OutputIt transform(InputIt first1, InputIt last1,
InputIt first2, OutputIt d_first, BinaryOp binary_op);
看个实际例子:
std::vector<int> nums = {1, 2, 3, 4, 5};
std::vector<int> squared(5);
std::transform(nums.begin(), nums.end(), squared.begin(),
[](int x) { return x * x; });
// squared: 1 4 9 16 25
二元版本可以用来做逐元素相加:
std::vector<int> a = {1, 2, 3};
std::vector<int> b = {10, 20, 30};
std::vector<int> sum(3);
std::transform(a.begin(), a.end(), b.begin(), sum.begin(),
std::plus<int>());
// sum: 11 22 33
std::for_each 配合引用,或者确保目标区间不重叠。
20.5 remove —— 它其实没删
这是最容易被误解的算法。remove 不会删除元素,它只是把「要保留的」元素往前挪,把「要删除的」元素放到区间末尾。然后返回一个迭代器,指向新逻辑结尾。
ForwardIt remove(ForwardIt first, ForwardIt last, const T& value);
看代码:
std::vector<int> v = {1, 2, 3, 2, 4, 2, 5};
auto new_end = std::remove(v.begin(), v.end(), 2);
// v 的内容变成: 1 3 4 5 ? ? ?
// new_end 指向第4个位置(从0开始算第4个)
// 真正删除:
v.erase(new_end, v.end());
// v: 1 3 4 5
这就是经典的 erase-remove 惯用法。我刚开始学的时候,以为 remove 直接删了,结果打印出来发现 size 没变,一脸懵。后来才明白,STL 算法不负责内存管理,它只管逻辑。
还有 remove_if,按条件移除:
v.erase(std::remove_if(v.begin(), v.end(),
[](int x) { return x % 2 == 0; }),
v.end());
// 删除所有偶数
20.6 unique —— 去重,但只去相邻的
unique 把相邻的重复元素「移除」到末尾,返回新逻辑结尾。注意,它只处理相邻的重复。如果重复元素不相邻,得先排序。
ForwardIt unique(ForwardIt first, ForwardIt last);
例子:
std::vector<int> v = {1, 1, 2, 3, 3, 3, 4, 2, 2};
auto new_end = std::unique(v.begin(), v.end());
v.erase(new_end, v.end());
// v: 1 2 3 4 2
看到了吗?最后的 2 还在,因为它和前面的 4 不相邻。要全局去重,先排序:
std::sort(v.begin(), v.end());
v.erase(std::unique(v.begin(), v.end()), v.end());
// v: 1 2 3 4
知识体系图
下面这张图帮你理清这六个算法的关系:
总结
这六个算法是 STL 修改操作的核心。我个人的使用频率排序是:transform > copy > remove > replace > fill > unique。但每个都有它的用武之地。
最后提醒一句:永远记住 STL 算法不管理内存。remove 和 unique 返回的迭代器只是逻辑结尾,要真正删除,必须调用容器的 erase。这是 C++ 新手最容易犯的错误,也是面试常考题。嗯,记住了就不会踩坑。