跳到主要内容

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 比六行循环更能表达意图。

写循环时先问一句:这个循环是不是某个算法已经命名过的动作? 是的话,直接用名字。