20. 修改算法:copy、replace、fill、transform、remove、unique

修改算法,说白了就是「改数据」的那一类函数。它们不会改变容器的大小,但会改变容器里元素的值,或者把元素挪来挪去。我个人习惯把它们分成两类:一类是原地修改,直接改原容器;另一类是拷贝式修改,把结果写到另一个地方去。

这一章我们重点聊六个:copyreplacefilltransformremoveunique。嗯,这里面有几个名字容易让人误解——比如 remove 其实并没有真的删除元素,只是把不要的挪到后面去了。你想想看,这坑我当年踩过不止一次。

核心要点:修改算法不会改变容器的大小,它们只负责改值或重排。要真正删除,得配合容器的 erase 成员函数。

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));
小技巧:copy 的底层实现其实就是循环赋值,但 STL 可能会针对 POD 类型调用 memcpy 做优化。所以别自己手写循环,交给标准库就好。

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
注意:fill 不会改变容器大小。如果 vector 是空的,fill 什么也做不了。先确保有元素再填。

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
避坑指南:我曾经在项目里用 transform 做原地修改,结果源区间和目标区间重叠了。虽然标准库允许重叠,但行为是未定义的。安全起见,原地修改用 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
经验之谈:unique 也支持自定义比较器。比如对结构体按某个字段去重,可以传一个 lambda。我在做数据清洗时经常这么干。

知识体系图

下面这张图帮你理清这六个算法的关系:

修改算法 拷贝式修改 原地修改 copy 复制到目标区间 transform 一元/二元变换 replace 替换指定值 fill 批量填充 remove 逻辑删除(挪到末尾) unique 相邻去重 注意:remove 和 unique 需配合 erase 才能真正删除元素

总结

这六个算法是 STL 修改操作的核心。我个人的使用频率排序是:transform > copy > remove > replace > fill > unique。但每个都有它的用武之地。

最后提醒一句:永远记住 STL 算法不管理内存。remove 和 unique 返回的迭代器只是逻辑结尾,要真正删除,必须调用容器的 erase。这是 C++ 新手最容易犯的错误,也是面试常考题。嗯,记住了就不会踩坑。

一句话总结:copy 复制,replace 替换,fill 填充,transform 变换,remove 逻辑删除,unique 相邻去重。配合 erase 才是完全体。

公众号:蓝海资料掘金营,微信 deep3321