STL 算法替代手写循环
手写循环的问题是"每次都从头表达意图"。STL 算法把意图直接写进函数名,同时让编译器更容易优化。本文按用途分组,每条给出手写版本与算法版本对照。
一、查找
// 手写
int found = -1;
for (std::size_t i = 0; i < v.size(); ++i) {
if (v[i] == target) { found = static_cast<int>(i); break; }
}
// 算法
auto it = std::find(v.begin(), v.end(), target);
const bool exists = (it != v.end());
const std::size_t idx = static_cast<std::size_t>(it - v.begin());
条件查找:
auto it = std::find_if(v.begin(), v.end(), [](const Foo& f) { return f.ready(); });
// 查所有匹配
std::vector<Foo> hits;
std::copy_if(v.begin(), v.end(), std::back_inserter(hits),
[](const Foo& f) { return f.ready(); });
返回多个元素时不要 count_if 后再 find_if 一遍,直接用 copy_if。
二、排序与去重
std::sort(v.begin(), v.end()); // 升序
std::sort(v.begin(), v.end(), std::greater<int>()); // 降序
// 稳定排序:相等元素保持原相对顺序
std::stable_sort(records.begin(), records.end(),
[](const Rec& a, const Rec& b) { return a.score > b.score; });
// 去重:必须先排序
std::sort(v.begin(), v.end());
v.erase(std::unique(v.begin(), v.end()), v.end());
std::sort 要求随机访问迭代器(vector/deque/数组可用,list 用成员 sort())。比较函数必须满足严格弱序。
三、变换与聚合
// 逐元素变换
std::vector<int> out(v.size());
std::transform(v.begin(), v.end(), out.begin(),
[](int x) { return x * 2; });
// 原地变换
std::transform(v.begin(), v.end(), v.begin(), [](int x) { return x + 1; });
// 求和
const int sum = std::accumulate(v.begin(), v.end(), 0);
// 乘积累积
const double prod = std::accumulate(d.begin(), d.end(), 1.0,
std::multiplies<double>());
// 计数
const std::size_t n = std::count_if(v.begin(), v.end(), [](int x) { return x > 0; });
// 最值
auto maxIt = std::max_element(v.begin(), v.end());
注意 accumulate 的初值类型决定结果类型:std::accumulate(v.begin(), v.end(), 0) 对 vector<long long> 会溢出,应写 0LL。
四、划分与分组
// 划分:满足条件的在前,不保证各自内部顺序
auto mid = std::partition(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
// 稳定划分:保持各自内部顺序
auto mid2 = std::stable_partition(v.begin(), v.end(), isEven);
C++20 的 std::ranges::partition 可直接返回子区间,无需手动用 mid 划分。
五、修改与填充
std::fill(v.begin(), v.end(), 0);
std::iota(v.begin(), v.end(), 1); // 1,2,3,...(<numeric>)
std::replace(v.begin(), v.end(), oldVal, newVal);
std::replace_if(v.begin(), v.end(), pred, newVal);
std::reverse(v.begin(), v.end());
std::rotate(v.begin(), v.begin() + k, v.end()); // 左旋 k 位
std::shuffle(v.begin(), v.end(), std::mt19937{std::random_device{}()});
std::random_shuffle 在 C++17 被移除,务必改用 std::shuffle + 显式引擎。
六、删除:remove-erase 惯用法
// 删除所有偶数
v.erase(std::remove_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; }), v.end());
名字容易误解:remove_if 不删除元素,只是把保留的元素前移到区间头部,返回新逻辑末尾,erase 才真正缩短 size。
对 list/forward_list,用成员函数 remove_if 更高效(原地摘链):
lst.remove_if([](int x) { return x % 2 == 0; });
七、二分查找族
// 必须在有序区间上
bool has = std::binary_search(v.begin(), v.end(), 42);
auto lb = std::lower_bound(v.begin(), v.end(), 42); // 第一个 >= 42
auto ub = std::upper_bound(v.begin(), v.end(), 42); // 第一个 > 42
std::size_t count = static_cast<std::size_t>(ub - lb); // 42 的出现次数
// 自定义比较
auto it = std::lower_bound(v.begin(), v.end(), key,
[](const Foo& f, int k) { return f.id < k; });
八、C++98 迁移对照
| C++98 写法 | 现代替代 |
|---|---|
| 手写 for 找第一个满足条件的元素 | std::find_if |
| 手写循环求和 | std::accumulate |
for + erase(it) 删元素 | remove-erase / std::erase_if(C++20) |
| 手写递归 or 循环求最大 | std::max_element |
sort + 手写比较类 | std::sort + lambda |
for + push_back 过滤 | std::copy_if + back_inserter |
| 手写二分 | std::lower_bound |
迁移时注意:C++98 的 std::bind1st/bind2nd/mem_fun 已在 C++17 移除,统一改用 lambda。
九、算法使用的边界
- 逻辑跨多个容器、有复杂控制流(
break嵌套、中途返回)时,手写循环更清楚; - 调试友好性:算法调用栈更长,命名 lambda 或抽函数可改善;
- 性能:算法与手写循环通常等价(都会被内联),不必为性能拒绝算法;
- 可读性优先:一个
copy_if比六行循环更能表达意图。
写循环时先问一句:这个循环是不是某个算法已经命名过的动作? 是的话,直接用名字。