数值计算三板斧:数值算法、复数与数学函数

说实话,很多C++开发者对STL的印象还停留在容器和迭代器上。但STL在数值计算这块,其实藏着不少好东西。我个人做信号处理项目时,就经常跟这些工具打交道。今天咱们就聊聊STL里的数值计算三件套:数值算法、复数运算和数学函数。

一、数值算法:不只是排序和查找

STL的<numeric>头文件里,有几个算法我几乎每个项目都会用到。它们不像sort那么出名,但实战价值很高。

1. iota:批量填充递增序列

这个函数名字有点怪,但功能很直白——给一个范围填充连续递增的值。我在做时间序列分析时,经常用它生成索引序列。

#include <numeric>
#include <vector>

std::vector<int> indices(100);
std::iota(indices.begin(), indices.end(), 0);
// indices = {0, 1, 2, ..., 99}

小技巧:iota的第三个参数是起始值。如果你想生成从1开始的序列,传1就行。我曾经用它配合随机数生成器,快速生成带偏移的采样点。

2. accumulate:不只是求和

很多人以为accumulate只能求和。其实它的第三个参数可以传任意二元操作。我在项目中用它计算向量的点积,比手写循环简洁多了。

#include <numeric>
#include <vector>

std::vector<double> v = {1.0, 2.0, 3.0, 4.0};
double sum = std::accumulate(v.begin(), v.end(), 0.0);
// sum = 10.0

// 自定义操作:计算乘积
double product = std::accumulate(v.begin(), v.end(), 1.0, 
                                 std::multiplies<double>());
// product = 24.0

注意:accumulate的初始值类型决定了返回类型。如果你传0(int)而不是0.0(double),结果会被截断。我踩过这个坑,排查了半天才发现是类型问题。

3. inner_product:内积一步到位

这个函数直接计算两个序列的内积。说白了就是对应元素相乘再求和。做线性代数运算时特别好用。

std::vector<double> a = {1.0, 2.0, 3.0};
std::vector<double> b = {4.0, 5.0, 6.0};
double dot = std::inner_product(a.begin(), a.end(), b.begin(), 0.0);
// dot = 1*4 + 2*5 + 3*6 = 32.0

4. partial_sum与adjacent_difference

这两个是差分和前缀和操作。做信号处理时,我常用adjacent_difference计算离散导数。

std::vector<int> data = {1, 3, 6, 10, 15};
std::vector<int> diff(data.size());
std::adjacent_difference(data.begin(), data.end(), diff.begin());
// diff = {1, 2, 3, 4, 5}  (第一个元素是原值,后面是差值)

二、复数运算:<complex>头文件

做通信或者信号处理的朋友,对复数肯定不陌生。STL的std::complex模板类,说实话设计得相当优雅。它支持所有基本运算,还能跟STL算法无缝配合。

基本用法

#include <complex>

std::complex<double> c1(3.0, 4.0);  // 3 + 4i
std::complex<double> c2(1.0, 2.0);

auto sum = c1 + c2;      // (4, 6)
auto prod = c1 * c2;     // (-5, 10)
auto conj = std::conj(c1); // (3, -4)
auto mag = std::abs(c1);   // 5.0
auto phase = std::arg(c1); // 约0.927弧度

核心要点:复数模板支持float、double、long double三种精度。我建议默认用double,除非你有特殊需求。float精度不够,long double又太慢。

实战:复数向量运算

复数可以放进vector里,配合数值算法使用。我在做FFT预处理时,经常这么写:

std::vector<std::complex<double>> signal(1024);
// 填充信号数据...

// 计算所有复数的模
std::vector<double> magnitudes(signal.size());
std::transform(signal.begin(), signal.end(), magnitudes.begin(),
               [](const auto& c) { return std::abs(c); });

// 计算总能量
double energy = std::accumulate(magnitudes.begin(), magnitudes.end(), 0.0);

三、数学函数:<cmath><numbers>

C++11开始,<cmath>里加了不少新函数。C++20又带来了<numbers>数学常量。这些工具让数值计算变得特别顺手。

常用数学函数一览

分类函数说明
三角函数sin, cos, tan, asin, acos, atan, atan2标准三角函数,参数为弧度
双曲函数sinh, cosh, tanh, asinh, acosh, atanh双曲三角函数
指数对数exp, log, log2, log10, pow, sqrt, cbrt幂、根、对数运算
取整ceil, floor, trunc, round, nearbyint各种取整方式
误差函数erf, erfc, tgamma, lgamma特殊函数,概率统计常用

我的习惯:做浮点数比较时,别用==。用std::abs(a - b) < epsilon,其中epsilon取std::numeric_limits<double>::epsilon()。这个坑我当年栽过好几次。

C++20数学常量

以前写圆周率得自己定义const double PI = 3.1415926535;。现在直接用std::numbers::pi就行,精度有保障。

#include <numbers>

double area = std::numbers::pi * r * r;
double euler = std::numbers::e;
double sqrt2 = std::numbers::sqrt2;

// 还有更冷门的
double phi = std::numbers::phi;  // 黄金分割比
double ln2 = std::numbers::ln2;  // ln(2)

四、知识体系总览

下面这张图把本章的核心内容串起来了。你可以看到数值算法、复数、数学函数这三块是怎么相互配合的。

STL数值计算体系 数值算法 <numeric> iota accumulate inner_product partial_sum 复数运算 <complex> 加减乘除 abs/arg conj/norm polar 数学函数 <cmath> 三角函数 指数对数 取整函数 特殊函数 C++20 新增:<numbers> 数学常量 pi, e, sqrt2, phi, ln2, log10e ...

五、实战经验总结

说了这么多,我分享几个实际项目中的体会:

  • 数值算法优先于手写循环:STL的数值算法经过高度优化,比自己手写for循环快得多。而且代码更清晰,不容易出错。
  • 复数运算注意性能std::complex的运算效率很高,但如果你频繁创建临时对象,还是会有开销。我习惯在热路径上复用对象。
  • 数学常量用标准库的:别自己定义#define PI 3.14。标准库的常量精度更高,而且跨平台一致。
  • 注意浮点误差:数值计算绕不开浮点精度问题。比较时用epsilon,输出时控制精度,这些细节决定了代码的可靠性。

一句话总结:STL的数值计算工具不是摆设。它们能帮你写出更简洁、更高效、更可靠的数值代码。下次遇到数值计算需求,先想想STL有没有现成的工具。

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