C++ numeric 中的算法
C++ 标准库的 <numeric> 头文件是进行数值计算的宝库。它不仅仅包含几个简单的函数,而是提供了一整套强大、高效且灵活的算法,用于处理序列数据。这些算法能够帮助我们写出更简洁、更具表现力且性能更高的代码,尤其是在现代 C++ (C++17 及以后) 中,它们中的许多都支持并行执行。
本文将全面介绍 <numeric> 头文件中的核心算法,分为三大类:
- 累积与归约 (Reduction):将一个序列合并成单个值。
- 扫描与前缀和 (Scan):计算序列的中间累积结果。
- 数值生成与数学工具:用于初始化序列和基础数学计算。
累积与归约 (Reduction)
这类算法的目标是将一个序列中的所有元素通过某个二元操作(如加法、乘法)合并成一个单一的值。
std::accumulate (C++98)
这是最经典、最基础的累积算法。它严格按照从左到右的顺序进行计算。
- 功能:计算一个序列
[first, last)中所有元素的总和,从一个初始值init开始。 - 特点:保证严格的顺序执行,可以通过自定义操作实现求积、字符串拼接等。
std::reduce (C++17)
std::reduce 是 std::accumulate 的现代化、可并行化版本。
- 功能:与
accumulate类似,但不保证计算顺序。 - 核心区别:
- 并行性:允许乱序执行,因此可以利用
std::execution::par等执行策略进行并行计算,从而大幅提升性能。 - 操作要求:由于乱序执行,提供的二元操作必须满足结合律和交换律(如加法、乘法),否则结果未定义。
- 默认初始值:有一个不需要
init参数的版本,它会使用元素类型的默认构造函数(例如int{}即0)作为初始值。
- 并行性:允许乱序执行,因此可以利用
何时选择?
- 需要保证顺序(如减法),或在 C++17 之前的代码中,使用
std::accumulate。- 追求性能,且操作允许乱序(如加法),现代 C++ 代码首选
std::reduce。
std::inner_product (C++98)
- 功能:计算两个序列的内积(点积)。它将两个序列的对应元素相乘,然后将所有乘积累加起来。
- 灵活性:可以自定义累积操作和元素配对操作。
std::transform_reduce (C++17)
这是最强大的归约算法,完美体现了 "map-reduce" 思想。
- 功能:
- 单序列:先对每个元素应用一个转换操作(map),然后再对结果进行归约(reduce)。
- 双序列:先对两个序列的对应元素应用一个二元操作(transform),然后再对结果进行归约(reduce)。
- 特点:可并行化,且将转换和归约合二为一,避免了创建中间容器。
扫描与前缀和 (Scan)
扫描算法不会将序列归约为单个值,而是生成一个包含所有中间累积结果的新序列。
std::partial_sum (C++98)
- 功能:计算序列的“部分和”,即
inclusive_scan的早期版本。结果中的第i个元素是原序列前i+1个元素的和。
std::inclusive_scan & std::exclusive_scan (C++17)
这两个是现代化的、可并行的扫描算法。
inclusive_scan:包含当前元素的扫描,行为与partial_sum相同。exclusive_scan:不包含当前元素的扫描,结果中的第i个元素是原序列前i个元素的和。
std::adjacent_difference (C++98)
- 功能:可以看作是
partial_sum的逆运算,计算序列中每两个相邻元素之间的差。
数值生成与数学工具
std::iota (C++11)
- 功能:用一个从指定值开始连续递增的序列来填充一个范围。非常适合快速初始化。
std::gcd & std::lcm (C++17)
- 功能:计算两个整数的最大公约数(Greatest Common Divisor)和最小公倍数(Least Common Multiple)。
总结
<numeric> 头文件为 C++ 开发者提供了一套声明式、高效且功能强大的工具。通过使用这些算法,我们可以:
- 提高代码可读性:算法的名称(如
accumulate,transform_reduce)清晰地表达了代码的意图。 - 提升代码性能:C++17 引入的并行版本算法可以充分利用现代多核 CPU 的计算能力。
- 减少错误:标准库算法经过了充分的测试和优化,比手写的循环更可靠。
下次当你需要对一个序列进行数值计算时,不妨先查阅一下 <numeric> 头文件,很可能已经有一个完美的算法在等着你了。