数值计算三板斧:数值算法、复数与数学函数
说实话,很多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的数值算法经过高度优化,比自己手写for循环快得多。而且代码更清晰,不容易出错。
- 复数运算注意性能:
std::complex的运算效率很高,但如果你频繁创建临时对象,还是会有开销。我习惯在热路径上复用对象。 - 数学常量用标准库的:别自己定义
#define PI 3.14。标准库的常量精度更高,而且跨平台一致。 - 注意浮点误差:数值计算绕不开浮点精度问题。比较时用epsilon,输出时控制精度,这些细节决定了代码的可靠性。
一句话总结:STL的数值计算工具不是摆设。它们能帮你写出更简洁、更高效、更可靠的数值代码。下次遇到数值计算需求,先想想STL有没有现成的工具。