一、算法概述:STL 算法的分类与设计哲学

STL 算法,说白了就是一组操作容器的函数模板。它们不依赖具体容器,只通过迭代器工作。这个设计非常巧妙——你写一次 sort,就能对 vector、deque、array 甚至原生数组排序。

我个人习惯把算法分成四大类:

分类 特点 典型代表
只读算法 不修改元素,只做查找、计数、判断 find, count, for_each, all_of
修改算法 会改变容器内容 copy, replace, fill, remove
排序与相关操作 排序、二分查找、合并、堆操作 sort, stable_sort, binary_search
数值算法 数学计算,定义在 <numeric> 中 accumulate, inner_product, partial_sum

嗯,这里要注意:只读算法是日常开发中使用频率最高的。你想想看,大部分业务逻辑其实就是在数据里找东西、数个数、做判断。所以这一章我们重点啃它。

核心原则:STL 算法通过迭代器与容器解耦。同一个 find 函数,可以查找 vector、list、set,甚至 C 风格数组。这就是泛型编程的魅力。

STL 算法分类体系 只读算法 修改算法 排序算法 数值算法 find / find_if count / count_if for_each all_of / any_of / none_of copy / replace fill / generate remove / unique transform sort / stable_sort partial_sort nth_element binary_search accumulate inner_product partial_sum adjacent_diff 核心设计:通过迭代器与容器解耦,同一算法可作用于不同容器 本章重点:只读算法 —— 查找、计数、遍历、条件判断

二、只读算法详解

2.1 find / find_if:查找元素

find 是最基础的线性查找。它从迭代器范围中找出第一个等于目标值的元素,返回指向它的迭代器。如果没找到,返回 end()。

我在项目中遇到过一个问题:用 find 在 vector 里查找自定义类型,结果死活找不到。后来才发现——忘了重载 operator==。嗯,这是个经典坑。

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

struct User {
    int id;
    std::string name;
    // 必须重载 ==,find 才能工作
    bool operator==(const User& other) const {
        return id == other.id;
    }
};

int main() {
    std::vector<User> users = {{1, "Alice"}, {2, "Bob"}, {3, "Charlie"}};
    auto it = std::find(users.begin(), users.end(), User{2, ""});
    if (it != users.end()) {
        std::cout << "找到用户: " << it->name << "\n";
    }
    return 0;
}

find_if 更灵活——它接受一个谓词(函数或 lambda),只要条件满足就算找到。

// 查找第一个名字长度大于 5 的用户
auto it = std::find_if(users.begin(), users.end(),
    [](const User& u) { return u.name.length() > 5; });

我的习惯:能用 find_if 就别用裸循环。lambda 表达式让意图一目了然,而且编译器优化起来更顺手。

2.2 count / count_if:统计出现次数

count 返回等于某个值的元素个数。count_if 返回满足条件的元素个数。它们都是 O(n) 的线性扫描。

你想想看,什么时候用 count 而不是 find?

  • 需要知道「有多少个」而不是「有没有」
  • 判断重复数据是否超过阈值
  • 统计日志中某种级别的错误数量
std::vector<int> scores = {85, 92, 78, 92, 88, 92};
int count_92 = std::count(scores.begin(), scores.end(), 92);
// count_92 == 3

int count_above_90 = std::count_if(scores.begin(), scores.end(),
    [](int s) { return s > 90; });
// count_above_90 == 3

我曾经踩过的坑:在 unordered_set 上用 count 判断元素是否存在。虽然能工作,但语义不对——set 里元素唯一,count 返回 0 或 1。更清晰的做法是用 findcontains(C++20)。

2.3 for_each:遍历并执行操作

for_each 对范围内的每个元素执行一个可调用对象。它是最「古老」的遍历算法,C++11 之后有了范围 for 循环,但 for_each 仍有其价值。

我个人习惯在两种场景下用 for_each

  1. 需要把遍历逻辑封装成函数对象,方便复用
  2. 配合并行策略(C++17),实现并行遍历
// 打印每个元素
std::for_each(scores.begin(), scores.end(),
    [](int s) { std::cout << s << " "; });

// 累加(虽然用 accumulate 更直接)
int sum = 0;
std::for_each(scores.begin(), scores.end(),
    [&sum](int s) { sum += s; });

注意:for_each 返回传入的可调用对象。这意味着你可以在 lambda 中维护状态,最后通过返回值获取。不过说实话,这个特性用得不多,我更喜欢用引用捕获。

2.4 all_of / any_of / none_of:范围判断三兄弟

这三个算法是 C++11 引入的,专门用来回答「范围内元素是否全部/任意/没有满足条件」。它们返回 bool,语义非常清晰。

算法 返回 true 的条件 空范围时的返回值
all_of 所有元素都满足谓词 true(空集的所有元素都满足)
any_of 至少一个元素满足谓词 false
none_of 没有元素满足谓词 true
std::vector<int> data = {2, 4, 6, 8, 10};

bool all_even = std::all_of(data.begin(), data.end(),
    [](int x) { return x % 2 == 0; });
// true

bool any_negative = std::any_of(data.begin(), data.end(),
    [](int x) { return x < 0; });
// false

bool none_odd = std::none_of(data.begin(), data.end(),
    [](int x) { return x % 2 != 0; });
// true

我的建议:写条件判断时,优先用这三个算法而不是手写循环。代码更短,意图更明确。比如「检查所有用户是否都已激活」——all_of 一行搞定,比 for 循环加 flag 变量优雅得多。

三、实战:日志分析工具

理论说完了,我们来点实际的。假设你有一个日志文件,每行是一条日志记录。你需要分析:

  • 统计不同级别的日志数量
  • 查找第一条 ERROR 日志
  • 检查是否所有日志都包含时间戳
  • 输出所有 WARNING 日志

下面是我写的一个简化版日志分析工具,用到了本章所有只读算法。

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

struct LogEntry {
    std::string level;   // INFO, WARNING, ERROR
    std::string message;
    bool has_timestamp;
};

int main() {
    // 模拟从文件读取的日志数据
    std::vector<LogEntry> logs = {
        {"INFO",    "Server started",           true},
        {"WARNING", "Disk usage above 80%",     true},
        {"ERROR",   "Connection timeout",       true},
        {"INFO",    "Request processed",        false},
        {"ERROR",   "Out of memory",            true},
        {"WARNING", "CPU temperature high",     true}
    };

    // 1. 统计 ERROR 数量
    int error_count = std::count_if(logs.begin(), logs.end(),
        [](const LogEntry& e) { return e.level == "ERROR"; });
    std::cout << "ERROR 数量: " << error_count << "\n";

    // 2. 查找第一条 ERROR 日志
    auto it = std::find_if(logs.begin(), logs.end(),
        [](const LogEntry& e) { return e.level == "ERROR"; });
    if (it != logs.end()) {
        std::cout << "第一条 ERROR: " << it->message << "\n";
    }

    // 3. 检查是否所有日志都有时间戳
    bool all_timestamped = std::all_of(logs.begin(), logs.end(),
        [](const LogEntry& e) { return e.has_timestamp; });
    std::cout << "所有日志都有时间戳: " 
              << (all_timestamped ? "是" : "否") << "\n";

    // 4. 检查是否有 WARNING 日志
    bool has_warning = std::any_of(logs.begin(), logs.end(),
        [](const LogEntry& e) { return e.level == "WARNING"; });
    std::cout << "存在 WARNING 日志: " 
              << (has_warning ? "是" : "否") << "\n";

    // 5. 输出所有 WARNING 日志(用 for_each)
    std::cout << "WARNING 日志列表:\n";
    std::for_each(logs.begin(), logs.end(),
        [](const LogEntry& e) {
            if (e.level == "WARNING") {
                std::cout << "  - " << e.message << "\n";
            }
        });

    // 6. 检查是否没有 FATAL 级别日志
    bool no_fatal = std::none_of(logs.begin(), logs.end(),
        [](const LogEntry& e) { return e.level == "FATAL"; });
    std::cout << "没有 FATAL 日志: " 
              << (no_fatal ? "是" : "否") << "\n";

    return 0;
}

输出结果:

ERROR 数量: 2
第一条 ERROR: Connection timeout
所有日志都有时间戳: 否
存在 WARNING 日志: 是
WARNING 日志列表:
  - Disk usage above 80%
  - CPU temperature high
没有 FATAL 日志: 是

实战要点:

  • count_if 替代手写计数循环,代码量减少 50%
  • find_if 找到第一个匹配项后立即停止,比遍历整个容器高效
  • all_of / any_of / none_of 让条件判断变成一句话,可读性极强
  • for_each 配合 lambda,适合需要「副作用」的遍历操作

说实话,这个例子虽然简单,但覆盖了日常开发中 80% 的只读算法使用场景。你想想看,日志分析、数据校验、报表统计——这些业务逻辑本质上就是查找、计数、判断、遍历。掌握了这几个算法,你写出来的代码会更简洁、更不容易出错。

我的经验:刚开始用 STL 算法时,总觉得不如手写循环「直观」。但用习惯了就会发现,算法把循环的控制逻辑和业务逻辑分开了——你不用关心怎么遍历,只关心对每个元素做什么。这就是抽象的力量。


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