数值计算方法:收敛性、稳定性与算法选择简述
数值计算方法:收敛性、稳定性与算法选择简述
数值计算研究的是如何用有限精度的机器运算,近似解决通常难以直接得到解析解的问题。一个可靠的数值算法不仅要给出结果,还必须回答三个问题:
- 结果是否趋向目标问题的真实解;
- 输入扰动和舍入误差是否会被放大;
- 结果是否能够通过残差、误差估计或独立方法验证。
本文从统一的误差概念出发,依次介绍非线性方程求根、线性方程组、插值、数值积分和最小二乘拟合,并给出适合工程实现的算法框架。
1. 数值计算的三个基本概念
1.1 误差、残差与条件数
设精确结果为 \(x\),计算结果为 \(\hat{x}\)。绝对误差为:
相对误差为:
当精确解未知时,通常使用残差。例如方程 \(f(x)=0\) 的残差是:
线性系统 \(Ax=b\) 的残差是:
残差小并不自动等价于解误差小。问题本身的条件数决定了输入扰动对输出的敏感程度。对线性系统,有近似关系:
条件数很大时,即使算法稳定,输入数据的微小误差也可能导致解发生明显变化。
1.2 收敛性与稳定性
- 收敛性:随着迭代次数增加,算法是否趋向目标解。
- 稳定性:输入误差、舍入误差和中间计算误差是否被过度放大。
- 一致性:离散算法是否在步长趋于零时逼近原问题。
对于迭代算法,收敛速度通常分为:
- 线性收敛:误差大致按固定比例缩小;
- 超线性收敛:后期收敛速度超过线性;
- 二次收敛:误差近似满足
e_{k+1}=Ce_k^2。
1.3 浮点数与停止条件
浮点数不能使用“两个结果完全相等”作为收敛判断。统一的停止条件应采用绝对容差和相对容差:
同时设置最大迭代次数,并在出现 NaN、无穷大、残差持续增长或中间量超出范围时停止计算。
2. 非线性方程求根
2.1 问题形式
非线性方程求根要求找到满足:
的 x。例如:
其导数为:
2.2 二分法:可靠的区间方法
如果连续函数在 [a,b] 上满足:
根据介值定理,区间内至少存在一个根。每轮取中点:
根据 f(a)f(m) 的符号决定保留左半区间还是右半区间。
第 k 轮后区间长度为:
因此二分法的误差具有明确上界。它的缺点是只具有线性收敛速度,但优点是对初值不敏感、过程稳定、容易验证。
2.3 固定点迭代
将方程改写成:
然后使用:
对于示例函数,可以构造:
局部收敛通常要求根附近满足:
更强的充分条件是在闭区间 I 上满足:
且:
此时 φ 是压缩映射,迭代从区间内任意初值出发都收敛到唯一不动点。
固定点迭代的关键不在于“能否改写”,而在于如何选择具有收敛性的 φ。
2.4 Newton 法
在 x_k 处用切线近似函数:
得到:
当根是单根、初值足够接近根且函数满足一定光滑性条件时,Newton 法具有局部二次收敛:
但 Newton 法可能遇到:
- 导数接近零;
- 迭代点跳出有效区间;
- 初值落入另一个根的吸引域;
- 函数存在多个根或高阶根。
2.5 混合求根策略
实际应用中可以采用两阶段策略:
- 使用二分法或其他区间方法保证迭代点进入目标根附近;
- 使用 Newton 法快速提高精度;
- 如果 Newton 步长跳出区间,则退回二分步;
- 同时检查步长、残差、最大次数和数值合法性。
| 方法 | 导数 | 区间 | 局部速度 | 稳健性 |
|---|---|---|---|---|
| 二分法 | 否 | 是 | 线性 | 高 |
| 固定点迭代 | 否 | 否 | 取决于 φ |
中 |
| Newton 法 | 是 | 否 | 二次 | 依赖初值 |
| 混合方法 | 是或否 | 是 | 兼顾速度和稳定性 | 高 |
3. 线性方程组
3.1 增广矩阵与消元
线性系统写作:
通过增广矩阵:
可以同时对系数和右端项进行初等行变换。
3.2 Gauss 消元
第 i 列对第 j 行的消元因子为:
行变换为:
前向消元得到上三角矩阵后,通过回代计算:
稠密矩阵的时间复杂度为 O(n^3),存储复杂度为 O(n^2)。
3.3 列主元与 LU 分解
为了避免小主元放大误差,在第 i 列选择:
并交换第 p 行和第 i 行。带列主元的 LU 分解可以表示为:
当矩阵 A 不变而右端项有多个时,只需分解一次,然后对每个 b 执行:
这比每次重新消元更高效。
实现约束如下:
- 矩阵元素和交换临时变量统一使用浮点类型;
- 每次除法前检查主元是否接近零;
- 奇异矩阵直接返回失败状态;
- 求解完成后计算
r=A\hat{x}-b; - 多右端项复用同一份分解结果。
3.4 迭代法的适用场景
大型稀疏系统不适合直接保存完整稠密矩阵。此时使用 Jacobi、Gauss-Seidel、SOR 或更高级的 Krylov 子空间方法,并结合稀疏存储和预条件技术。
4. 迭代法求解线性系统
4.1 统一迭代形式
线性迭代法可统一写作:
误差满足:
收敛的核心条件为:
其中 ρ(B) 是迭代矩阵的谱半径。严格对角占优和对称正定等性质,可以作为部分方法的充分条件。
4.2 Jacobi 迭代
将矩阵分解为:
Jacobi 迭代为:
逐分量形式为:
每个新分量都使用上一轮的旧向量,因此适合并行计算。
4.3 Gauss-Seidel 迭代
Gauss-Seidel 立即使用已经更新的分量:
它通常比 Jacobi 收敛更快,但计算顺序存在依赖。
4.4 SOR 迭代
SOR 引入松弛因子 ω:
其中:
ω=1为 Gauss-Seidel;0<ω<1为欠松弛;1<ω<2为超松弛。
迭代器统一使用以下终止逻辑:
- 计算更新量
||x_{k+1}-x_k||; - 计算残差
||Ax_{k+1}-b||; - 两者都低于阈值时返回成功;
- 残差持续增长、出现非法浮点值或达到最大次数时返回未收敛。
5. 插值方法
设节点为:
插值要求构造函数 p(x),使其满足 p(x_i)=y_i。
5.1 Lagrange 插值
误差为:
直接计算一个查询点的复杂度为 O(n^2)。当查询点较多时,使用重心形式:
预处理权重需要 O(n^2),每个查询点的计算降为 O(n)。节点必须非空、互异;查询点若恰好等于某个节点,则直接返回该节点值。
5.2 三次样条
三次样条在每个区间 [x_i,x_{i+1}] 上使用:
相邻区间满足函数值、一阶导数和二阶导数连续。自然边界条件为:
以节点二阶导数 M_i 为未知量时,内部节点形成三对角系统:
其中:
三对角系统使用追赶法求解,建表和求解复杂度均为 O(n)。节点必须严格递增;查询点必须位于 [x_0,x_{n-1}] 内;最右端点单独返回 y_{n-1};区间定位使用二分查找。
与高次 Lagrange 多项式相比,三次样条通过局部多项式降低了端点振荡,代价是需要明确边界条件。
6. 数值积分
6.1 复化梯形公式
对区间 [a,b] 等分为 n 段:
复化梯形公式为:
当 f 二阶连续可导时:
6.2 Romberg 外推
若:
则可以递推消除误差项:
前几级分别产生分母 3、15 和 63。
Romberg 实现使用滚动数组保存递推表,相邻层结果满足:
时停止。分段数使用正整数,最大外推层数固定,超过上限则返回未收敛状态。
对于:
在 x=0 处使用极限值 f(0)=1,避免直接计算 0/0。
7. 最小二乘多项式拟合
给定数据 (x_i,y_i),用 m 次多项式:
拟合系数通过最小化:
得到,其中 V 是 Vandermonde 矩阵。
7.1 QR 分解
拟合流程采用 Householder QR:
将最小二乘问题转化为:
该过程不显式构造 V^TV,避免条件数平方放大。计算完成后输出:
7.2 秩亏与 SVD
当设计矩阵接近秩亏时,QR 分解会暴露出很小的对角元素。此时切换到 SVD:
根据奇异值阈值确定有效秩,并截断过小奇异值对应的方向,从而抑制噪声放大。
7.3 模型评价
三次多项式的参数更多,在同一批数据上的 SSE 通常不会大于二次多项式,但模型选择不只依赖 SSE。最终评价同时使用:
- 验证集误差;
- 残差是否存在系统性趋势;
- 区间边界处是否出现异常振荡;
- 模型参数数量与数据量是否匹配。
当观测数据具有不同方差时,使用加权最小二乘:
8. 统一的验证框架
不同算法使用不同的误差指标,但验证流程可以统一:
- 验证输入合法性:维度、节点、区间、主元和容差;
- 执行有最大次数或最大层数限制的算法;
- 记录每次迭代的近似值、步长和残差;
- 根据停止条件判定成功、失败或未收敛;
- 对最终结果执行独立残差检查;
- 在已知参考值时额外计算绝对误差和相对误差。
各类问题的核心指标如下:
| 问题 | 主要指标 |
|---|---|
| 方程求根 | ` |
| 线性方程组 | ` |
| 迭代线性系统 | 残差曲线、谱半径、迭代次数 |
| 插值 | 最大绝对误差、节点误差、边界振荡 |
| 数值积分 | 外推差值、参考值误差、层数 |
| 曲线拟合 | SSE、验证集误差、有效秩 |
9. 一组可运行的参考实现
下面给出一组相互独立的 C++17 参考实现。它们不是某个特定项目的补丁,而是按照前文的数值约束重新组织的最小实现:
- 迭代过程不使用递归;
- 所有迭代都有最大次数;
- 结果携带收敛状态和诊断信息;
- 线性系统使用列主元;
- 插值节点经过合法性检查;
- 积分过程使用最大外推层数限制。
9.1 统一的迭代结果
将算法结果和诊断信息放在同一个结构中,可以避免通过特殊数值表示失败状态:
#include <algorithm>
#include <cmath>
#include <functional>
#include <limits>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
struct IterationResult {
double value = std::numeric_limits<double>::quiet_NaN();
double residual = std::numeric_limits<double>::infinity();
int iterations = 0;
bool converged = false;
std::string status;
};
9.2 二分法与 Newton 混合求根
下面的实现先验证变号区间,再优先执行 Newton 步。如果 Newton 步越出当前区间,或者导数过小,就回退到二分步。这样既保留了二分法的区间约束,也利用了 Newton 法的局部二次收敛。
IterationResult solve_root(
const std::function<double(double)>& f,
const std::function<double(double)>& df,
double left,
double right,
double x0,
double abs_tol = 1e-12,
double residual_tol = 1e-12,
int max_iter = 100)
{
const double nan = std::numeric_limits<double>::quiet_NaN();
if (!(left < right) || max_iter <= 0 || abs_tol <= 0 || residual_tol <= 0)
return { nan, nan, 0, false, "invalid_input" };
double fa = f(left);
double fb = f(right);
if (!std::isfinite(fa) || !std::isfinite(fb) || fa * fb > 0.0)
return { nan, nan, 0, false, "invalid_bracket" };
double x = std::clamp(x0, left, right);
double fx = f(x);
for (int k = 1; k <= max_iter; ++k)
{
double next = std::numeric_limits<double>::quiet_NaN();
double dfx = df(x);
if (std::isfinite(dfx) && std::abs(dfx) > 1e-14)
next = x - fx / dfx;
if (!std::isfinite(next) || next <= left || next >= right)
next = 0.5 * (left + right);
double fnext = f(next);
if (!std::isfinite(fnext))
return { next, fnext, k, false, "non_finite" };
if (std::abs(fnext) < residual_tol &&
std::abs(next - x) < abs_tol)
return { next, std::abs(fnext), k, true, "converged" };
if (fa * fnext <= 0.0)
{
right = next;
fb = fnext;
}
else
{
left = next;
fa = fnext;
}
x = next;
fx = fnext;
}
return { x, std::abs(fx), max_iter, false, "max_iterations" };
}
对于:
调用时传入 f 和 f',同时提供一个已经验证过的变号区间。函数返回值中,status 区分输入错误、非有限数值、达到最大次数和正常收敛。
9.3 带列主元的 Gauss-LU 求解
下面的实现原地保存 L 和 U,并通过行交换记录置换。它适合中小规模稠密矩阵;多组右端项时,可以复用分解后的矩阵。
using Matrix = std::vector<std::vector<double>>;
std::vector<double> solve_lu(Matrix a, std::vector<double> b)
{
const int n = static_cast<int>(a.size());
if (n == 0 || static_cast<int>(b.size()) != n)
throw std::invalid_argument("invalid linear system");
for (const auto& row : a)
if (static_cast<int>(row.size()) != n)
throw std::invalid_argument("matrix must be square");
for (int k = 0; k < n; ++k)
{
int pivot = k;
for (int i = k + 1; i < n; ++i)
if (std::abs(a[i][k]) > std::abs(a[pivot][k]))
pivot = i;
if (std::abs(a[pivot][k]) < 1e-14)
throw std::runtime_error("singular or ill-conditioned matrix");
std::swap(a[k], a[pivot]);
std::swap(b[k], b[pivot]);
for (int i = k + 1; i < n; ++i)
{
a[i][k] /= a[k][k];
for (int j = k + 1; j < n; ++j)
a[i][j] -= a[i][k] * a[k][j];
b[i] -= a[i][k] * b[k];
}
}
std::vector<double> x(n);
for (int i = n - 1; i >= 0; --i)
{
double value = b[i];
for (int j = i + 1; j < n; ++j)
value -= a[i][j] * x[j];
x[i] = value / a[i][i];
}
return x;
}
该实现的消元阶段复杂度为 O(n^3),回代为 O(n^2)。实际应用中还应单独计算 ||Ax-b||,因为求解器返回成功并不等于问题本身条件良好。
9.4 重心 Lagrange 插值
重心形式将节点权重预计算,使多次查询更加高效:
class BarycentricInterpolator {
public:
BarycentricInterpolator(std::vector<double> x,
std::vector<double> y)
: x_(std::move(x)), y_(std::move(y)), w_(x_.size())
{
if (x_.empty() || x_.size() != y_.size())
throw std::invalid_argument("invalid interpolation data");
for (size_t i = 0; i < x_.size(); ++i)
{
w_[i] = 1.0;
for (size_t j = 0; j < x_.size(); ++j)
{
if (i == j)
continue;
double gap = x_[i] - x_[j];
if (gap == 0.0)
throw std::invalid_argument("duplicate interpolation node");
w_[i] /= gap;
}
}
}
double operator()(double query) const
{
double numerator = 0.0;
double denominator = 0.0;
for (size_t i = 0; i < x_.size(); ++i)
{
if (query == x_[i])
return y_[i];
double term = w_[i] / (query - x_[i]);
numerator += term * y_[i];
denominator += term;
}
return numerator / denominator;
}
private:
std::vector<double> x_, y_, w_;
};
预处理复杂度为 O(n^2),单次查询复杂度为 O(n)。节点数量很大或节点高度聚集时,还应考虑节点缩放、Chebyshev 节点以及更稳定的基函数表示。
9.5 Romberg 积分
Romberg 表可以使用一维数组滚动更新:
double romberg(
const std::function<double(double)>& f,
double a,
double b,
double tol = 1e-10,
int max_level = 12)
{
if (!(a < b) || tol <= 0 || max_level < 1)
throw std::invalid_argument("invalid integration parameters");
std::vector<double> table(max_level + 1);
double h = b - a;
table[0] = 0.5 * h * (f(a) + f(b));
for (int level = 1; level <= max_level; ++level)
{
const int new_points = 1 << (level - 1);
const double step = h / (1 << level);
double midpoint_sum = 0.0;
for (int i = 1; i <= new_points; ++i)
midpoint_sum += f(a + (2 * i - 1) * step);
const std::vector<double> previous = table;
table[0] = 0.5 * table[0] + step * midpoint_sum;
for (int order = 1; order <= level; ++order)
{
const double factor = std::pow(4.0, order);
table[order] = table[order - 1] +
(table[order - 1] - previous[order - 1]) / (factor - 1.0);
}
if (std::abs(table[level] - table[level - 1]) < tol)
return table[level];
h *= 0.5;
}
throw std::runtime_error("Romberg integration did not converge");
}
每一层新增采样点,避免重复计算已有的梯形节点。滚动数组使额外空间为 O(level);函数求值次数仍随外推层数指数增长,因此 max_level 必须受到限制。
10. 算法选择
| 问题特征 | 算法选择 | 关键条件 |
|---|---|---|
| 只有变号区间 | 二分法 | 函数连续且端点异号 |
| 有可靠初值和导数 | Newton 法 | 导数不能过小 |
| 稠密线性系统 | 列主元 LU | 主元和条件数可接受 |
| 多个右端项 | LU 分解复用 | 系数矩阵不变 |
| 大型稀疏系统 | 迭代法 | 收敛条件和预条件有效 |
| 少量低阶节点 | Lagrange | 节点互异、次数较低 |
| 需要平滑局部曲线 | 三次样条 | 节点有序、边界条件明确 |
| 光滑函数积分 | Romberg | 梯形误差展开有效 |
| 小规模拟合 | Householder QR | 设计矩阵满秩或近似满秩 |
| 秩亏拟合 | 截断 SVD | 奇异值阈值明确 |
11. 工程化实现
数值程序遵循以下结构:
- 输入校验层:检查维度、范围、节点、容差和数据类型;
- 算法层:只执行数学计算,不直接打印;
- 诊断层:统一返回近似值、残差、迭代次数、停止原因和收敛状态;
- 验证层:独立计算残差、参考误差和稳定性指标;
- 展示层:根据诊断结果生成表格、曲线或日志。
每个迭代算法都具有固定的停止状态:
converged:满足步长和残差条件;max_iterations:达到最大次数;invalid_input:输入不满足算法要求;singular_or_ill_conditioned:主元或奇异值过小;non_finite:出现NaN或无穷大。
测试覆盖以下边界:
- 初值已经是精确解;
- 导数或主元接近零;
- 奇异矩阵和近奇异矩阵;
- 插值点位于左右端点和区间外;
- 节点重复或未排序;
- 误差容差过小;
- 迭代不收敛;
- 设计矩阵秩亏。
12. 总结
数值计算算法的可靠性来自完整的闭环:
求根问题关注不动点、导数和初值;线性系统关注主元、条件数和残差;插值问题关注节点分布和边界条件;积分问题关注误差展开和外推层数;拟合问题关注矩阵秩、模型复杂度和泛化误差。
本文采用的实现体系是:二分法与 Newton 的混合求根、列主元 LU、带残差检测的迭代线性求解、重心 Lagrange、自然三次样条、滚动数组 Romberg、Householder QR 以及秩亏时的截断 SVD。它们共同构成了一套从理论、实现到验证都闭合的数值计算方法。

浙公网安备 33010602011771号