数值算法与集合算法:accumulate, inner_product, adjacent_difference, set_union, set_intersection

数值计算和集合操作,是日常开发中绕不开的两类场景。STL 在 <numeric><algorithm> 头文件里,给我们准备了一套相当趁手的工具。今天我就带大家把这几个算法彻底捋一遍。

1. accumulate:累加,但不止于累加

std::accumulate<numeric> 里最基础的算法。它的作用很简单:从一个初始值开始,依次对序列中的每个元素执行二元操作,最终得到一个累积结果。

#include <numeric>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    
    // 默认加法:1+2+3+4+5 = 15
    int sum = std::accumulate(v.begin(), v.end(), 0);
    std::cout << sum << std::endl;  // 15
    
    // 自定义操作:乘法,初始值1
    int product = std::accumulate(v.begin(), v.end(), 1, 
                                  [](int a, int b) { return a * b; });
    std::cout << product << std::endl;  // 120
    
    return 0;
}

这里有个细节很多人会踩坑:初始值的类型决定了返回类型。如果你传 0,返回 int;传 0.0,返回 double。我曾经在项目里用 accumulate 累加一堆浮点数,结果因为初始值写了 0,精度全丢了。嗯,从那以后我每次都会检查初始值类型。

小技巧:accumulate 的第三个参数是初始值,也是整个累加过程的「种子」。你可以用它来做字符串拼接、自定义对象的累加,甚至实现一个简单的 reduce 操作。

2. inner_product:内积,两个序列的「点乘」

std::inner_product 计算两个序列的内积。默认行为是:对应元素相乘,然后全部加起来。说白了就是数学里的点积。

#include <numeric>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> a = {1, 2, 3};
    std::vector<int> b = {4, 5, 6};
    
    // 1*4 + 2*5 + 3*6 = 4 + 10 + 18 = 32
    int dot = std::inner_product(a.begin(), a.end(), b.begin(), 0);
    std::cout << dot << std::endl;  // 32
    
    return 0;
}

你想想看,如果两个序列长度不一致会怎样?inner_product 只遍历到第一个序列的末尾,第二个序列如果更短,就会越界访问。所以调用前务必保证第二个序列长度 ≥ 第一个序列。我在做图像处理时,用这个算两个特征向量的相似度,就因为这个细节排查了半天。

它还有两个可选的二元操作参数:一个用于「加法」步骤,一个用于「乘法」步骤。你可以用它实现自定义的「内积」逻辑,比如字符串的某种匹配度计算。

3. adjacent_difference:相邻元素的差

std::adjacent_difference 计算序列中相邻元素的差值。第一个元素直接复制到输出,从第二个开始,每个元素等于当前元素减去前一个元素。

#include <numeric>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {2, 5, 9, 14, 20};
    std::vector<int> diff(v.size());
    
    std::adjacent_difference(v.begin(), v.end(), diff.begin());
    // diff = {2, 3, 4, 5, 6}
    
    for (int x : diff) std::cout << x << " ";
    // 输出:2 3 4 5 6
    
    return 0;
}

这个算法有个很妙的特性:它是可逆的。如果你对差值结果再做一次 partial_sum,就能恢复原始序列。我在做时间序列分析时,经常用这个算法把原始数据转成增量数据,方便做差分检测。

注意:输出迭代器不能是输入迭代器本身(即不能原地操作),否则行为未定义。如果你需要原地计算,请先拷贝一份。

4. set_union:并集,有序序列的合并

std::set_union 计算两个已排序序列的并集。注意前提:两个序列必须已经按升序排好。它会把所有出现在任一序列中的元素,按顺序写入输出。

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

int main() {
    std::vector<int> v1 = {1, 2, 3, 4, 5};
    std::vector<int> v2 = {3, 4, 5, 6, 7};
    std::vector<int> result;
    
    std::set_union(v1.begin(), v1.end(),
                   v2.begin(), v2.end(),
                   std::back_inserter(result));
    // result = {1, 2, 3, 4, 5, 6, 7}
    
    return 0;
}

这里有个坑:如果两个序列中有重复元素,set_union 会保留出现次数较多的那个。比如 v1 有两个 3,v2 有一个 3,结果里会有两个 3。我个人习惯在调用前先用 std::unique 去重,除非你明确需要保留重复。

5. set_intersection:交集,取公共部分

std::set_intersection 计算两个已排序序列的交集。只有同时出现在两个序列中的元素才会被写入输出。

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

int main() {
    std::vector<int> v1 = {1, 2, 3, 4, 5};
    std::vector<int> v2 = {3, 4, 5, 6, 7};
    std::vector<int> result;
    
    std::set_intersection(v1.begin(), v1.end(),
                          v2.begin(), v2.end(),
                          std::back_inserter(result));
    // result = {3, 4, 5}
    
    return 0;
}

这个算法的时间复杂度是 O(m+n),比暴力 O(m*n) 快得多。我在做权限系统时,用这个算法快速计算用户角色和资源角色的交集,效率非常高。

核心要点:所有集合算法都要求输入序列是已排序的。如果序列未排序,结果会完全错误,而且不会报错。这是 C++ 里典型的「未定义行为」——编译器不会提醒你,但结果就是错的。

知识体系总览

下面这张图帮你理清这几个算法的关系和使用场景:

数值算法与集合算法知识体系 数值算法 (<numeric>) accumulate 累加/累乘/自定义归约 初始值决定返回类型 inner_product 内积/点乘/自定义二元操作 注意两个序列长度一致 adjacent_difference 相邻元素差值/可逆操作 不可原地操作 集合算法 (<algorithm>) set_union 并集/合并两个有序序列 保留重复次数多的 set_intersection 交集/取公共元素 O(m+n) 高效实现 ⚠ 共同前提 输入序列必须已排序 否则结果未定义 同属 STL

避坑指南与实战建议

这几个算法虽然简单,但用起来有几个地方特别容易翻车:

  • 迭代器范围要合法:所有算法都要求传入的迭代器范围是有效的,[first, last) 必须是一个合法区间。空区间是允许的,但不要传反了。
  • 输出空间要足够:如果你直接传一个普通迭代器(而不是 back_inserter),必须确保目标容器有足够的空间。否则就是越界写入。
  • 集合算法要求有序:这是最容易被忽略的一点。我见过有人把未排序的 vector 传给 set_union,结果出来一堆乱序数据,排查了半天才发现是排序问题。
  • 自定义比较器要一致:如果你用了自定义比较器,所有相关算法(排序、集合操作)必须使用同一个比较器,否则结果不可预测。
我的个人习惯:在写集合操作前,先加一行 assert(std::is_sorted(v.begin(), v.end())); 来确保输入有序。debug 模式下能帮你快速定位问题。

好了,这几个数值算法和集合算法就讲到这里。它们虽然基础,但用好了能写出非常简洁高效的代码。下次遇到累加、内积、差分、并集、交集这些需求时,别自己手写循环了——STL 已经帮你准备好了。


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