第14章 数值计算
计算的目的在于洞悉事物,而非数字本身。
—— 理查德·卫斯里·汉明
……但对学生而言,
数据往往是开启洞察力的最佳途径。
—— Anthony Ralston
14.1 导言
C++在设计之初并未做数值计算方面着重考虑过。 但数值计算却常穿插于其它业务——比如数据库访问、网络系统、仪器控制、图形学、 仿真及金融分析等等——因此对于较大系统中的计算部分,C++就成了香饽饽。 此外,数值方法早已远非遍历浮点数向量这样简单的任务。 在参与计算的数据结构日益复杂之处,C++的威力变得举足轻重。 这导致C++广泛用于科学、工程、金融及其它涉及复杂计算的领域。 因此,此类计算的辅助构件和技术则应运而生。 本章讲述标准库中有关数值计算的部分。
14.2 数学函数
头文件<cmath>
提供了 标准数学函数(standard mathematical functions) ,
例如针对参数类型float
、double
以及long double
的sqrt()
、log()
和sin()
等:
标准数学函数
abs(x)
绝对值
ceil(x)
>=x
的最小整数
floor(x)
<=x
的最大整数
sqrt(x)
平方根;x
不能是负数
cos(x)
余弦函数
sin(x)
正弦函数
tan(x)
正切函数
acos(x)
反余弦函数;结果不为负
asin(x)
反正弦函数;返回最靠近0的结果
atan(x)
反正切函数
sinh(x)
双曲正弦函数
cosh(x)
双曲余弦函数
tanh(x)
双曲正切函数
exp(x)
e(自然常数)的x次幂
log(x)
自然对数,以e为底;x
必须是正数
log10(x)
以10为底的对数
针对complex
(§14.4)的版本在<complex>
中。
以上函数的返回值类型与参数相同。
错误报告的方式是将errno
设置为<cerrno>
中的值,
定义域超出范围设为EDOM
,值域超出范围设为ERANGE
。例如:
有些被称为 特殊数学函数(special mathematical functions)
的数学函数在<cstdlib>
里,
还有几个在<cmath>
比如 beta()
、rieman_zeta()
、sph_bessel()
。
14.3 数值算法
<numeric>
里有几个泛化过的数值算法,比如accumulate()
。
数值算法
x=accumulate(b,e,i)
x
是i
与[b
:e
)间元素的和
x=accumulate(b,e,i,f)
调用accumulate
时用f
替换+
x=inner_product(b,e,b2,i)
x
是[b
:e
)与
[b2
:b2+(e-b)
)的内积,
即i
与(*p1) * (*p2)
的和,
其中p1
是[b
:e
)
中的元素,且对应来自[b2
:b2+(e-b)
)
中的元素p2
x=inner_product(b,e,b2,i,f,f2)
调用inner_product
时用f
和f2
分别替换+
和*
p=partial_sum(b,e,out)
[out
:p
)的第i
个元素是
[b
:b+i
]间所有元素的和
p=partial_sum(b,e,out,f)
调用partial_sum
时以f
替换+
p=adjacent_difference(b,e,out)
i>0
时,[out
:p
)
的第i
个元素是 *(b+i)-* (b+i-1)
;e-b>0
时, *out
就是* b
p=adjacent_difference(b,e,out,f)
调用adjacent_difference
时以f
替换-
iota(b,e,v)
把++v
依次赋值给[b
:e
)
之间的元素,因此元素序列就变成v+1
,v+2
,……
x=gcd(n,m)
x
是整数n
和m
的最大公约数
x=lcm(n,m)
x
是整数n
和m
的最小公倍数
这些算法泛化了常见运算,比如求和运算被应用到所有类型的元素序列上了。 也把应用在元素序列上的操作参数化了。 对于每个算法,最常规的版本是将常规运算代入到通用版本得到的。例如:
这些算法适用于标准库中的所有元素序列,可以将操作以参数的形式传入(§14.3)。
14.3.1 并行算法
<numeric>
中,数值算法具有略带差异的并行版本(§12.9):
并行数值算法
x=reduce(b,e,v)
无序执行的x=accumulate(b,e,v)
x=reduce(b,e)
x=reduce(b,e,V{})
,其中V
是b
的值类型
x=reduce(pol,b,e,v)
采用执行策略pol
的x=reduce(b,e,v)
x=reduce(pol,b,e)
x=reduce(pol,b,e,V{})
,其中V
是b
的值类型
p=exclusive_scan(pol,b,e,out)
按照pol
策略执行p=partial_sum(b,e,out)
计算第i个和的时候,第i个输入元素不参与计算
p=inclusive_scan(pol,b,e,out)
按照pol
策略执行p=partial_sum(b,e,out)
计算第i个和的时候,第i个输入元素参与计算
并行数值算法(续表)
p=transform_reduce(pol,b,e,f,v)
对[b
:e
)中的每个x
执行f(x)
,
而后执行reduce
p=transform_exclusive_scan(pol,b,e,out,f,v)
对[b
:e
)中的每个x
执行f(x)
,
而后执行exclusive_scan
p=transform_inclusive_scan(pol,b,e,f,v)
对[b
:e
)中的每个x
执行f(x)
,
而后执行inclusive_scan
为简化叙述,此处没有提及那些采用仿函数参数替代+
和=
算法版本。
除reduce()
意外,采用默认(顺序)执行策略和缺省值的版本也未提及。
此处的算法和<algorithm>
里的并行算法一样,可以指定执行策略:
(reduce()
之类的)并行算法区别于顺序版本(即accumulate()
)之处在于:
并行算法中针对元素的操作执行顺序不定。
14.4 复数
标准库提供了一系列的复数类型,它们符合§4.2.1中complex
的描述。
为了让其中的标量能支持单精度浮点数(float
)、双精度浮点数(double
)等类型,
标准库的complex
是个模板:
复数支持常见的算术操作和大多数的数学函数。例如:
sqrt()
和pow()
(幂运算)属于<complex>
中定义的常见数学函数。
14.5 随机数
许多领域需要随机数,比如测试、游戏、仿真以及安全系统。
标准库在<random>
中提供了种类繁多的随机数发生器,它们反映了应用领域的多样性。
随机数发生器由两部分组成:
[1] 引擎(engine) ,负责生成随机值或伪随机值的序列
[2] 分布器(distribution) ,负责将这些值映射到特定的数学分布
分布器的例子有:uniform_int_distribution
(生成所有可能值的概率相同)、normal_distribution
(正态分布,即“铃铛曲线”)、exponential_distribution
(指数分布);它们都可以指定生成的随机数范围。
例如:
出于对标准库中随机数组件通用性和性能的持续关注,
一位专家称其为“每个随机数程序库成长的榜样”。
但是要论“新手之友”的称号,它可就愧不敢当了。
前述代码示例借助using
语句和lambda表达式,稍稍提升了一点代码可读性。
对于(任何背景的)新手而言,随机数程序库那个完整的通用接口绝对是个大坑。 一个简洁统一的随机数生成器往往就足以起步了。例如:
但到哪儿去找这个东西呢?我们得弄个跟die()
差不多的东西,
把引擎和分布器撮合起来,装进一个Rand_int
类:
这个定义仍然是“专家级”的,但是Rand_int()
的 使用 ,
学习C++第一周的新手就可以轻松掌握了。例如:
输出是个(索然无味的)均匀分布(具有合理的统计波动)。
C++没有标准的图形库,所以这里用了“ASCII图形”。 毫无疑问,C++有众多开源以及商业的图形和GUI库, 但我在本书中限定自己仅使用ISO标准的构件。
14.6 矢量算术
§11.2叙述的vector
设计目的是作为一个承载值的通用机制,
要灵活,并且能融入容器、迭代器、算法这套架构。
可惜它不支持数学矢量(vector)的运算。
给vector
添加这些运算没什么难度,
但它的通用性和灵活性跟繁重的数值作业所需的优化格格不入。
因此,标准库(在<valarray>
中)提供了一个类似vector
的模板,
被称为valarray
,它的通用性不济,但在数值计算所需的优化方面却精益求精:
valarray
支持常见的算术运算和大多数的数学函数,例如:
除算术运算之外,valarray
还支持跨步访问,以辅助多维计算的实现。
14.7 数值范围
标准库在<limits>
中提供了一些类,用于描述内建类型的属性——
比如float
指数部分的最大值,或者每个int
的字节数。
比如说,可以断言char
是有符号的:
请留意,第二个断言能够运行的原因,来(且仅来)自于numeric_limits<int>::max()
是个constexpr
函数(§1.6)这一事实。
14.8 忠告
[1] 数值计算问题通常很微妙,如果对这类问题的某一方面没有100%的确信, 要么尝试咨询专家建议,要么实践检验,或者干脆双管齐下;§14.1。
[2] 别把繁重的数值计算建立在编程语言的基础构件上,请采用程序库;§14.1。
[3] 如果要从序列里计算出一个值,尝试写循环之前,请先考虑
accumulate()
、inner_product()
、partial_sum()
、adjacent_difference()
;§14.3。[4] 为复数运算采用
std::complex
;§14.4。[5] 把随机数引擎和一个分布器组合起来创建随机数生成器;§14.5。
[6] 确保你的随机数足够随机;§14.5。
[7] 别用C标准库的
rand()
;对于实际应用而言,它的随机度不够;§14.5。[8] 如果运行时的效率压倒操作和元素类型方面的灵活性, 请为数值计算采用
valarray
;§14.6。[9] 数值类型的属性可以通过
numeric_limits
获取;§14.7。[10] 请使用
numeric_limits
查询数值类型的属性,确保它们够用;§14.7。
最后更新于