简介:数值分析上机题配套MATLAB实现文档,面向学习数值分析课程的工科学生与研究生,可配套东南大学版教材使用。内容围绕三次样条插值函数、重积分计算和常微分方程初值问题数值解三大经典上机题展开,对每个题目均给出公式推导、MATLAB算法流程、运行结果和误差对比。其中,三次样条部分按第一类边界条件编制通用程序,完成汽车门曲线型值点插值;重积分部分采用复化梯形公式逐次二分步长并结合外推思想,实现精度控制;常微分方程部分实现RK4、AB4、AB4-AM4及改进预测校正四种方法,以误差范数对比精度并总结结论。文档为docx格式,共1个文件,压缩包约566KB,已有904人学习下载,适合需要快速理解算法细节、对照编写MATLAB程序或参考上机报告的同学。
1. 数值分析上机题是什么:不是作业,是给算法做压力测试
你对着教材背了三个小时的牛顿迭代公式,打开编辑器却连第一个根都没求出来。这不是你笨,而是手算可以取巧,程序不会——草稿纸上你能挑一个顺眼的初值再试一次,程序却会把每一次错误都放大给你看。数值分析上机题考的就是这个:把课本算法写成可运行代码之后,还能不能让迭代不震荡、误差可控、结果经得起换数据检验。
它不只是一道编程作业,而是逼你在同一套题目里处理三个层面的问题:算法层选对方法,数值层设对判据,工程层防住浮点数的小动作。多数人前半小时写得出代码,后两小时全耗在「为什么结果不对」上,而那些不对的根因,往往不在数学推导里,而在初值、判据和精度处理这些看似不起眼的地方。
适合读这篇的人有三类:正在准备某课程上机题的学生、需要补数值实现能力的开发、以及用现成库但搞不懂收敛参数含义的调参型选手。下文按一套完整上机题的流程走一遍,从选语言、定误差口径,到写最小实现、排坑、验证收敛阶,全程是可复现的操作。
2. 动手写数值分析上机题前的四个决策:语言、误差口径、测试数据与验收标准
写代码前先做四个决策,能省掉后面大半的调试时间。这四个决策分别是编程语言、误差口径、测试数据和验收标准,每一项都直接影响你能不能从一堆数字里判断出「程序是对的还是凑巧没崩」。
2.1 编程语言怎么选:不是哪个熟用哪个
很多人的第一反应是「哪个熟用哪个」,但上机题的瓶颈从来不是写代码的速度,而是调试和验证的速度。我一般选 Python,三个理由:numpy 负责向量和矩阵部分,算法主体自己写,不会被库函数掩盖真实实现;matplotlib 画迭代轨迹和误差曲线,能直接看到数值行为;调试迭代循环时,print 和断言几乎没有额外成本。C/C++ 的优势在性能和精度控制,适合同一道题后续做规模扩展,但对一门以「算法原理加数值行为」为考察目标的上机题,编译运行看输出的循环会拖慢排查速度。MATLAB 的矩阵语法确实舒服,但离开课程环境后复用成本偏高。
| 语言 | 适合的上机题类型 | 不推荐的场景 |
|---|---|---|
| Python | 求根、数值积分、插值拟合、画误差曲线 | 超大规模线性方程组性能对比 |
| C/C++ | 精度边界测试、内存受限的矩阵运算 | 大量可视化和快速原型验证 |
| MATLAB | 矩阵类题目、与课程讲义语法接近 | 脱离课程环境后的长期维护 |
如果课程强制要求交 C 代码,我的建议是先用 Python 把算法验证对,再翻译成 C。这不是偷懒,而是把「算法正确性」和「语言正确性」两个问题分开排查。否则一个数值 bug 和一个指针 bug 混在一起,足够让人怀疑人生。上机题的评分点在数值行为是否合理、报告是否完整,不在代码用了多少高级特性。
2.2 误差口径怎么定:绝对误差、相对误差与有效数字
上机题里最常见的错误写法,是把收敛条件统一写成 abs(x_new - x_old) < 1e-6。这个判据在解的量级是 0.1 到 10 时勉强能用,一旦解的量级到 1e4,绝对误差很容易造成假收敛;解的量级到 1e-4 时,这个条件又可能永远满足不了,迭代直接跑满上限。
数值分析里的误差口径分三种:绝对误差、相对误差、有效数字。相对误差等于绝对误差除以真实值的绝对值,有效数字对应相对误差的十进制位数。上机题里真实值通常未知,所以实践中用相邻两步迭代值的差来估计误差,再用相对形式判断是否达到精度。我的默认做法是同时检查两步差和残差 f(x_k),任何一个满足阈值就停,另外用最大迭代次数兜底。阈值取 1e-10 比较稳妥,双精度浮点下再小没有意义,迭代上限取 100 对大多数求根题足够。
不能只靠残差判断收敛,因为牛顿法里残差和误差之间隔着一个导数因子。如果当前点附近导数值很小,残差可能已经很小了但误差还很大。所以「步长加残差」双判据才算完整,这一点在第 4 章会有对应的修复代码。
2.3 测试数据从哪来:带已知答案的两类题目
不要在作业原始数据上直接调试。先准备「自带正确答案」的测试用例,这是把「程序写对」和「题目算对」两件事分开的关键。我常用的测试数据有两类:已知根的多项式,和已知解析解的积分。
已知根多项式可以直接构造,比如 f(x) = (x - 1)(x - 2)(x - 3),展开成 x^3 - 6x^2 + 11x - 6,根就是 1、2、3。用牛顿法跑这个函数,初值分别取 0.5、1.5、2.5,三个区间应该各收敛到一个根。这个用例同时验证了初值对收敛方向的影响。已知解析解的积分更简单,∫0π sin(x) dx = 2,∫0^1 e^x dx = e - 1,教科书自带答案。跑复化辛普森时,n 从 4 翻倍到 256,误差应该严格递减而且比值稳定。
如果直接在作业数据上跑出一个奇怪结果,你很难区分是代码有 bug 还是数据本身病态。先用已知解的用例确认实现正确,再上作业数据,排查范围就小了一半。
2.4 验收标准怎么立:先有真解,再谈收敛
验收不是「跑通了没有」,而是「跑出的数字有没有资格称为答案」。我一般按四步验收:在已知解用例上跑通;看误差是否按理论阶数下降;换初值或换区间看稳定性;最后才上作业的原始数据。理论阶数是现成的标尺:梯形法是 2 阶,误差随步长平方下降;复化辛普森是 4 阶,误差随步长四次方下降;牛顿迭代在单根附近是 2 阶收敛,误差逐次平方级缩小。
如果误差曲线不符合理论阶数,宁可修代码,也不要靠调大迭代次数把结果硬凑成「看起来对」。上机题翻车很大一部分出在这里:把 bug 当成参数问题,参数调到当前用例能过,换一道题又崩。验收标准的第一条永远是「有没有对照真解」,没有真解对照的验收都是自欺欺人。
3. 把上机题公式变成可跑程序:牛顿迭代与复化辛普森的最小实现
这一章用一套典型组合题走通实现流程。题目结构是虚构的,但拆法和代码可以直接抄:第一问用牛顿法求非线性方程的实根,第二问用复化辛普森算定积分近似值。这类组合在数值分析上机题里非常常见。
3.1 一道典型上机题怎么拆
拆题分三步:从题干抽出输入,包括函数表达式、区间端点、初值、精度要求;抽出输出,包括根的近似值、积分近似值、误差估计、迭代次数;抽出约束,包括最大迭代次数、输出小数点位数、是否要求画图。
拆完之后,代码骨架就清楚了。典型签名如下:
def newton_solve(f, df, x0, tol=1e-10, max_iter=100): """用牛顿迭代求 f(x) = 0 的根""" def composite_simpson(f, a, b, n): """用复化辛普森公式求 f 在 [a, b] 上的定积分"""一个容易犯的误拆是把「误差估计」理解成「最后一步的步长」。误差估计在报告里要单独分析和展示,不能拿迭代步长冒充。后面几节的实现都围绕这两个签名展开。
3.2 牛顿迭代的最小实现
def newton_solve(f, df, x0, tol=1e-10, max_iter=100): x = x0 for k in range(max_iter): fx = f(x) if abs(fx) < tol: # 残差判据:函数值已经足够小 return x, k + 1, True d = df(x) if abs(d) < 1e-14: # 导数接近 0,切线几乎水平,交点会飞远 raise ValueError("derivative too small at x={}".format(x)) x_new = x - fx / d # 牛顿迭代主公式:x - f(x)/f'(x) if abs(x_new - x) < tol: # 步长判据:迭代已经不怎么动了 return x_new, k + 1, True x = x_new return x, max_iter, False # 达到上限仍未收敛,返回失败标志函数返回三个值:近似根、迭代次数、是否收敛。返回迭代次数是为了后面做收敛阶分析,这个设计在验证阶段会很有用。循环里先用残差判据,再用步长判据,两者满足其一就停。导数接近 0 时直接抛异常而不是继续迭代,这是防止结果变成 inf 或 NaN 的关键。
参数说明:tol 取 1e-10 是双精度下的稳妥值;max_iter 取 100 对大多数求根题足够。x0 是整个方法的命门,牛顿法没有全局收敛保证,初值靠近根时收敛极快,离得远时可能震荡甚至发散。调用时尽量传解析导数,比如 f = lambda x: x3 - x - 1,df = lambda x: 3*x2 - 1。不要一上来就用数值差分代替解析导数,那会引入额外误差,还会破坏理论上的二阶收敛性。
3.3 复化辛普森的最小实现
def composite_simpson(f, a, b, n): if n % 2 != 0: n += 1 # 辛普森要求区间数为偶数,自动修正 h = (b - a) / n s = f(a) + f(b) # 两个端点权重为 1 for i in range(1, n, 2): # 奇数下标节点,权重 4 s += 4 * f(a + i * h) for i in range(2, n, 2): # 偶数下标节点,权重 2 s += 2 * f(a + i * h) return s * h / 3复化辛普森的权重模式是 1、4、2、4、2、……、4、1,把奇数下标和偶数下标分开循环,可以避免权重写混。返回值等于加权和乘以 h/3。这个方法的误差是 O(h^4),区间数 n 翻倍时误差大约降到原来的 1/16,这是第 5 章验证程序正确性的核心依据。
参数说明:n 的选取不能拍脑袋。先用小的 n 跑一遍,再翻倍跑一遍,比较两位结果;如果差别大就继续翻倍。n 取 100 到 1000 通常足够,但被积函数在区间内有剧烈变化时,均匀节点再多也可能被局部区域欺骗,这个问题在第 4 章单独展开。如果题目要求画图,可以在区间上均匀取点,画出 f 的曲线和辛普森逼近的分段抛物线,这是报告里很直观的加分项。
3.4 让结果可复现:输出与留痕
上机题报告要求结果可复现。最简单有效的三件事:固定随机种子,只在用到随机数时才需要;限制浮点输出格式;保存每次迭代的中间值。迭代中间留痕的代码很小:
history = [] def newton_solve(f, df, x0, tol=1e-10, max_iter=100, history=None): x = x0 for k in range(max_iter): fx = f(x) if history is not None: history.append({"k": k, "x": x, "fx": fx}) # 后续迭代逻辑与 3.2 相同把 history 导出成 JSON 或 CSV,之后画迭代轨迹和误差曲线就不用重跑程序。浮点输出用 print(f"{x:.15g}"),不要用默认的 str 转换,否则 1e-16 级别的小数会显示成一串看着像整数的尾数。这个习惯在写报告时能省大量时间,尤其是在需要放收敛图的题目里。
4. 数值分析上机题避坑清单:五个典型翻车现场与修复方式
这一章是长期做数值类练习攒下的血泪经验。每一条我都见过不止一次在自己机器或同学机器上出现,按「现象、原因、解决」三部分写,可以直接对照排查。
4.1 初值选在驻点附近:牛顿迭代不收敛的经典原因
现象:牛顿迭代卡在某个值附近震荡,或者直接跳到一个很远的值,结果变成 inf 或 NaN。
原因:牛顿法在当前点做切线,用切线与 x 轴的交点作为下一次估计。当前点恰好接近驻点时,导数接近 0,切线几乎水平,交点会被抛到很远的地方。初值一旦落在这个陷阱区域,后续迭代很难自己拉回来。
解决:换初值是最直接的。更稳的做法是给导数加保护,abs(d) < 1e-14 时抛异常提示换初值。如果题目给的是闭区间求根,可以先用二分法在区间里找一个大致位置,再把这个位置作为牛顿法的初值。上机题里「二分法粗找加牛顿法精化」是非常实用的混合策略,不算作弊,是工程上普遍的做法。
4.2 被积函数变化剧烈,辛普森结果却「温柔地错」
现象:n 从 100 涨到 10000,结果在小数点后第三位还是不变。这不是收敛,是程序对变化剧烈的区域根本没采到样。
原因:复化辛普森把区间均匀切分,隐含假设是被积函数在区间内「处处差不多难」。遇到快速振荡函数或窄尖峰时,峰值落在网格之间,积分直接被跳过。教科书上的 O(h^4) 误差估计要求函数四阶导数有界,真实题目里这个条件经常不成立。
解决:先做 n 和 2n 的对比实验,如果前六位有效数字稳定,就认为结果可用。如果一直不稳定,就要做分段加密,或者改用自适应积分类库。不要硬加 n 到上万,因为浮点舍入误差会随计算次数累积,节点太多反而可能更差。
4.3 只写绝对误差判据,解是大数时假收敛
现象:迭代第三步就停了,输出一个 x,代回 f(x) 一验,偏差还很大。
原因:收敛条件只写了 abs(x_new - x_old) < 1e-6,而真实解在 1e6 量级。迭代每步变化 1e4,相对变化很小,绝对变化却远大于阈值,程序在误差还很大时就判定收敛。
解决:改成步长加残差的双判据,这是所有迭代型上机题通用的写法:
if (abs(x_new - x) < tol * max(1.0, abs(x_new)) or abs(f(x_new)) < tol): break注意其中的 max(1.0, abs(x_new)),作用是把绝对判据变成带量级感知的混合判据,同时避免解本身很小时把相对误差放大成无穷大。这个写法可以直接背下来用。
4.4 高斯消元不选主元:残差小误差大的怪相
现象:解线性方程组时,算残差 r = b - A x 很小,但和参考答案一比差很远。
原因:线性方程组的求解过程是乘除混合运算。主元接近 0 时,消元过程会产生极大的中间系数,浮点舍入误差被逐级放大。残差小只能说明代入后左右两边符合得好,不代表解接近真实解。特别是矩阵病态时,残差大小和误差大小可以完全脱钩。
解决:用列主元消去。每次消元前,在当前列中选绝对值最大的元素所在行,交换到主元位置:
# 在第 k 列寻找绝对值最大元素所在行,并交换到当前行 p = k + max(range(n - k), key=lambda i: abs(A[k + i, k])) A[[k, p]] = A[[p, k]] b[[k, p]] = b[[p, k]]这是上机题里最容易被扣分的一项:答案算对了,但实现里没有选主元,报告一翻就露馅。只要写了高斯消元,选主元这一步是必须有的。
4.5 导数公式抄错但测试用例恰好能过
现象:牛顿法在第一个测试用例上一切正常,换到第二个题目立刻发散。
原因:代码里 df 的解析表达式抄错了,比如漏了系数或漏了某一项。第一个用例函数特殊,错误导数与真实导数在初值附近的符号和大小恰好一致,错误被掩盖;换一个函数,错误立刻暴露。
解决:用中心差分自检解析导数,对比 df(x0) 和 (f(x0+h) - f(x0-h)) / (2h),h 取 1e-6 左右。两者差值在 1e-6 量级说明导数实现正确,差很多就是抄错了。这个自检函数在 3.2 的代码基础上加几行就能写完。我每次手写解析导数之后都会先自检一轮,这已经是固定习惯。
5. 用收敛阶实验验证上机题程序:误差比值不会骗人
5.1 用误差比值快速判断实现有没有写错
复化辛普森的理论误差阶是 4,区间数 n 翻倍、步长减半时,误差应该缩小到原来的约 1/16。找一个有解析解的积分跑一遍,看误差比值就能判断实现是否正确。下面是一次典型输出的误差序列,数值随平台略有差异,但比值形态是固定的:
| n | 误差近似值 | 相邻比值 |
|---|---|---|
| 4 | 2.3e-4 | 无 |
| 8 | 1.4e-5 | 约 16.4 |
| 16 | 8.9e-7 | 约 15.7 |
| 32 | 5.6e-8 | 约 15.9 |
误差比值稳定接近 16,这就是四阶实现的指纹。如果比值稳定接近 4,说明你实际上写成梯形法了,权重多半写错。如果比值没有规律,说明实现有 bug,不要继续调参数,先查循环和权重。
牛顿迭代也同理,在单根附近应该看到误差逐次平方级缩小:e1 是 1e-2,e2 约 1e-4,e3 约 1e-8,下一轮直接到 1e-16,这是典型的二阶收敛形态。如果达不到这个节奏,先怀疑初值是否落在根的局部收敛域里。
5.2 把收敛阶实验做进报告里
上机报告别只贴最终结果,贴上误差随步长变化的表格,再写一句「n 翻倍误差降至约 1/16,符合四阶收敛理论」,这就是证明程序正确的硬证据,比任何截图都有说服力。自动计算误差序列的脚本很短:
import math def f(x): return math.sin(x) results = [] for n in [4, 8, 16, 32]: approx = composite_simpson(f, 0, math.pi, n) error = abs(approx - 2.0) results.append((n, error)) for i in range(1, len(results)): ratio = results[i - 1][1] / results[i][1] print(results[i][0], results[i][1], ratio)输出里相邻误差比值稳定接近 16,就直接写进报告当证据。这个方法把「黑匣子式运行」变成「可解释的数值验证」,评审一看就明白你是理解方法本身的人。
我现在拿到任何上机题,第一步都是先写一个带已知解的验证用例,第二步才做作业本身的数据。这个习惯替我挡掉过不少数值翻车现场。希望帮到你。
本文还有配套的精品资源,点击获取