写循环遍历容器,是每个 C++ 新手每天的日常;但老手更习惯说:这件事交给 <algorithm>。排序、查找、统计、变换,STL 算法库全都有现成的,配合 lambda 表达式,常常一行顶你十行手写循环。本文用成绩处理的实战场景,把这套组合拳拆开讲清楚,学完你就能写出"意图自明"的代码。

1. 算法库解决什么问题

先看一段"新手代码":手动遍历找最大值、手动计数,每个都要写循环、管下标,还容易出错:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> scores{85, 92, 78, 60, 88};
    // 手写:找最高分
    int max_score = scores[0];
    for (std::size_t i = 1; i < scores.size(); ++i) {
        if (scores[i] > max_score) max_score = scores[i];
    }
    // 手写:数及格人数
    int pass = 0;
    for (int s : scores) if (s >= 60) ++pass;
    std::cout << "最高分 " << max_score << ",及格 " << pass << " 人\n";
    return 0;
}

这段代码能跑,但每个"意图"(找最大、数及格)都被循环细节淹没:读代码的人要先理解循环结构,才能猜出作者想干什么。算法的思路是:把"遍历方式"和"判断规则"分离——遍历交给算法,规则交给 lambda,代码读起来就是一句句的意图声明。<algorithm> 里有上百个这样的算法,本文挑最常用的几个讲透。

2. lambda 速成

lambda(匿名函数)是算法的黄金搭档,语法三件套:方括号捕获、圆括号参数、花括号函数体:

#include <iostream>

int main() {
    int base = 10;
    // [base] 按值捕获:lambda 内部是 base 的副本
    auto add_base = [base](int x) { return x + base; };
    // [&base] 按引用捕获:可以修改外部变量
    auto bump = [&base]() { base += 100; };

    std::cout << add_base(5) << "\n";  // 15
    bump();
    std::cout << base << "\n";          // 110
    // [=] 全部按值捕获,[&] 全部按引用捕获,按需使用
    auto all = [=]() { return base + 1; };
    std::cout << all() << "\n";          // 111
    return 0;
}

捕获列表决定 lambda 能用哪些外部变量:[x] 复制、[&x] 引用、[=]/[&] 全部。注意按值捕获的 base 是副本,改它不影响外面;按引用捕获则相反,但引用捕获有"悬空"风险——lambda 存活时间超过被捕获变量的生命周期就会出问题,所以默认优先按值捕获。缺省情况下 lambda 的 operator() 是 const 的,想修改捕获的副本要加 mutable,但实践中很少用。

3. sort:排序与自定义规则

std::sort 默认升序,传第三个参数自定义比较规则。排序"学生"这种自定义结构,规则就得靠 lambda:

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

struct Student {
    std::string name;
    int score;
};

int main() {
    std::vector<Student> students{
        {"张三", 85}, {"李四", 92}, {"王五", 78}, {"赵六", 92}};
    // 按成绩降序
    std::sort(students.begin(), students.end(),
              [](const Student& a, const Student& b) {
                  return a.score > b.score;
              });
    // 成绩相同按姓名升序:stable_sort 保证相等元素的相对顺序
    std::stable_sort(students.begin(), students.end(),
                     [](const Student& a, const Student& b) {
                         if (a.score != b.score) return a.score > b.score;
                         return a.name < b.name;
                     });
    for (const auto& s : students)
        std::cout << s.name << " " << s.score << "\n";
    return 0;
}

比较 lambda 返回"a 是否应该排在 b 前面"。两个 92 分的同学,sort 不保证相对顺序,stable_sort 保证——所以第二次排序后"李四"一定在"赵六"前面。记住比较器必须满足严格弱序(a<b 为真则 b<a 必假),否则 sort 行为未定义,这是新手最容易踩的坑。另外,如果只想取前几名,不必完整排序:std::partial_sort 只保证前 k 个元素有序,复杂度 O(n log k);想快速找"第 k 大"用 std::nth_element,平均 O(n)。这两个算法在排行榜、中位数这类场景里比 sort 快一个量级。

4. find 家族:查找元素

find 找"等于某值"的第一个元素,find_if 找"满足条件"的第一个元素,都返回迭代器,找不到就返回 end():

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> names{"张三", "李四", "王五", "李四"};
    auto it = std::find(names.begin(), names.end(), "李四");
    if (it != names.end())
        std::cout << "找到: " << *it << "\n";

    // find_if:找第一个长度大于 2 的名字
    auto it2 = std::find_if(names.begin(), names.end(),
                            [](const std::string& s) { return s.size() > 2; });
    if (it2 != names.end())
        std::cout << "第一个超过 2 字的名字: " << *it2 << "\n";

    // 想数"所有"匹配的,配合循环推进起点
    std::size_t count = 0;
    for (auto p = names.begin(); p != names.end();) {
        p = std::find(p, names.end(), "李四");
        if (p == names.end()) break;
        ++count;
        ++p;
    }
    std::cout << "\"李四\" 出现 " << count << " 次\n";
    return 0;
}

三个要点:一是判断必须用 != end(),直接解引用未找到的迭代器是未定义行为;二是 find_if 接收"谓词",任何能返回 bool 的可调用对象都行;三是 find 只返回第一个匹配,数次数可以循环推进起点,不过更专业的场景有 std::count 一行搞定。性能提示:在无序 vector 上 find 是 O(n),如果查找非常频繁,考虑换 std::set(O(log n))或 std::unordered_set(平均 O(1)),但换来的是插入变慢、内存变大。算法选型要结合整个程序的热点,不能只看单次操作。

5. for_each 与 transform:遍历与变换

for_each 对每个元素执行副作用,transform 把每个元素"映射"成新值:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> scores{85, 92, 78, 60, 88};
    // for_each:打印每个分数
    std::for_each(scores.begin(), scores.end(),
                  [](int s) { std::cout << s << " "; });
    std::cout << "\n";

    // transform:每个分数 +5,存入新容器
    std::vector<int> adjusted(scores.size());
    std::transform(scores.begin(), scores.end(), adjusted.begin(),
                   [](int s) { return s + 5; });

    // 原地变换:目标起点写成源起点即可
    std::transform(scores.begin(), scores.end(), scores.begin(),
                   [](int s) { return s * 2; });
    for (int s : adjusted) std::cout << s << " ";
    std::cout << "| ";
    for (int s : scores) std::cout << s << " ";
    std::cout << "\n";
    return 0;
}

transform 的三参数版本是"源起点、源终点、目标起点",输出到新容器时要提前 resize 出空间;把目标起点写成源起点就是原地变换。两者的分工要分清:for_each 关注"过程"(打印、计数、修改外部状态),transform 关注"结果"(产生新值)。C++20 之后还有 ranges 版本 std::views::transform,写法更优雅,但原理一样。

6. 统计与更多实用算法

count_if 与 accumulate

统计及格人数用 count_if,求总和/平均值用 accumulate(在 <numeric> 里):

#include <algorithm>
#include <iostream>
#include <numeric>
#include <string>
#include <vector>

int main() {
    std::vector<int> scores{85, 92, 78, 60, 88};
    int pass = std::count_if(scores.begin(), scores.end(),
                             [](int s) { return s >= 60; });
    int total = std::accumulate(scores.begin(), scores.end(), 0);
    double avg = static_cast<double>(total) / scores.size();
    std::cout << "及格 " << pass << " 人,平均分 " << avg << "\n";

    // accumulate 的第四参可以自定义"怎么加"
    std::string joined = std::accumulate(
        scores.begin(), scores.end(), std::string(),
        [](const std::string& acc, int s) {
            return acc + (acc.empty() ? "" : ",") + std::to_string(s);
        });
    std::cout << joined << "\n";   // 85,92,78,60,88
    return 0;
}

count_if 统计满足条件的个数;accumulate 是"归约":从初值 0 开始,把每个元素按加法合并。第四参数是自定义合并函数,这里用它把整数拼成逗号分隔的字符串——同一个算法,换个"加法"就换了用途。同类的常用组合还有:std::adjacent_find 在有序区间里找相邻重复,std::equal 比较两个区间是否逐元素相同,std::fill 批量赋值。STL 算法的命名非常直白,用到时去 cppreference 按分类查,比硬记快得多。

min/max、区间判断与二分查找

还有一组高频算法:找极值、判断"是否全部/任意/没有满足条件"、在有序区间里二分查找:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> scores{85, 92, 78, 60, 88};
    auto max_it = std::max_element(scores.begin(), scores.end());
    auto min_it = std::min_element(scores.begin(), scores.end());
    std::cout << "最高 " << *max_it << ",最低 " << *min_it << "\n";

    // 区间判断:全部及格?有人上 90?没人低于 50?
    bool all_pass = std::all_of(scores.begin(), scores.end(),
                                [](int s) { return s >= 60; });
    bool any_high = std::any_of(scores.begin(), scores.end(),
                                [](int s) { return s >= 90; });
    bool none_low = std::none_of(scores.begin(), scores.end(),
                                 [](int s) { return s < 50; });
    std::cout << all_pass << " " << any_high << " " << none_low
              << "\n";   // 1 1 1

    // 二分查找:只对有序区间有效,O(log n)
    std::sort(scores.begin(), scores.end());
    bool found = std::binary_search(scores.begin(), scores.end(), 78);
    std::cout << "78 是否存在: " << found << "\n";   // 1
    return 0;
}

max_element/min_element 返回迭代器,记得解引用;三个 *_of 把"区间是否满足性质"表达成一条语句,配合返回的 bool 直接进 if;二分查找比 std::find 快得多,但前提是区间已排序——对无序数据用它结果未定义,这是必须记住的前提条件。

7. 实战:erase-remove 惯用法

从容器里"删除所有满足条件的元素",直接 erase 会让迭代器失效,新手常踩坑。标准做法是 remove_if + erase 两步:

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> scores{85, 92, 78, 60, 88, 45, 59};
    // 删除所有不及格的
    auto new_end = std::remove_if(scores.begin(), scores.end(),
                                  [](int s) { return s < 60; });
    scores.erase(new_end, scores.end());   // 真正缩短容器

    std::cout << "剩余 " << scores.size() << " 人:";
    for (int s : scores) std::cout << " " << s;
    std::cout << "\n";   // 剩余 5 人: 85 92 78 60 88
    return 0;
}

remove_if 不会缩短容器,它把"幸存者"搬到前面,返回新逻辑末尾的迭代器;真正的删除交给 erase。这两步连写就是著名的 erase-remove 惯用法,对所有顺序容器(vector、deque、string)通用,比手写循环删除安全得多——手写循环删除时,erase 会让当前迭代器失效,稍不留神就漏元素或越界。值得说明的是,remove 系列只保证"逻辑删除":被删元素的值还残留在容器尾部,直到 erase 把那段空间真正释放,所以两步缺一不可。

8. 总结与练习

这一篇你掌握了:lambda 的捕获与写法、sort/stable_sort 的自定义排序、find/find_if 的查找、for_each/transform 的遍历变换、count_if/accumulate 的统计、极值与区间判断、二分查找,以及 erase-remove 惯用法。核心心法:遍历交给算法、规则交给 lambda、判断用 != end()、有序才能二分。这些算法组合起来,基本可以覆盖日常数据处理的全部需求。

练习题:

💡 看到"手写循环遍历容器"的冲动时,先想想 <algorithm> 里有没有现成的——标准库算法经过精心优化,正确性也更有保障,你的循环大概率没它快。