简介:这份资源面向学习数值分析与科学计算的学生及工程人员,提供一套用 Matlab 实现经典计算方法的完整代码集合,覆盖常微分方程数值解、线性方程组求解、特征值计算与最优化等典型问题,适合课程实验、算法对照与自学练手。压缩包共 12 个文件,全部为 m 脚本,整体约 5KB,体积轻便,便于直接放入 Matlab 环境运行调试。内容涵盖欧拉法、高斯消元、豪斯霍尔德变换与 QR 分解、线性最小二乘、牛顿迭代、幂迭代法,以及 Runge-Kutta 家族的 RKF 与 RKFA 等算法,并配有辅助函数组织计算流程。已有 691 人学习,说明其在教学与实践中具有一定参考价值。读者可借此对照教材理解各算法的迭代公式与实现细节,观察不同步长与初值对精度和收敛性的影响,并在此基础上改写扩展,用于曲线拟合、方程求根或特征值估计等实际场景。
1. 数值计算课设救急:一套能直接跑的 Matlab 算法库
期末周最怕什么?不是理论推导,是导师甩过来一句「用 Newton 法解这个非线性方程,顺便把 Euler 和 Householder 的对比图也画出来」,而你对着空白脚本发呆。这套「计算方法 Matlab 全套代码」就是为这种场景准备的——它把数值分析课里那些经典算法,从方程求根、线性方程组迭代、数值积分到常微分方程初值问题,全部落成了可运行的.m文件。适合两类人:一是正在上计算方法/数值分析课、需要交作业和课设的学生;二是想把算法快速验证一遍、不想从零推公式的工程开发者。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序,把这套代码拆开讲。
2. 算法地图与目录结构:先看清这套代码覆盖了哪些数值方法
拿到一堆.m文件,第一件事不是急着run,而是先搞清楚它到底覆盖了哪些知识点、文件之间怎么组织。这套代码的定位很明确:它是一套「按算法分类」的教学型实现,而不是一个带 GUI 的成品软件。所以它的目录结构大概率是按数值方法的类别来分的,每个方法一个或几个函数文件,配一个演示脚本。
2.1 按方法分类的目录组织
常见的组织方式是这样(我按这类课设代码的典型结构还原,实际文件名可能略有出入,但分类逻辑基本一致):
计算方法Matlab代码/ ├── 01_非线性方程求根/ │ ├── newton.m % Newton 迭代法 │ ├── bisection.m % 二分法 │ ├── secant.m % 弦截法 │ └── demo_root.m % 求根演示脚本 ├── 02_线性方程组/ │ ├── gauss_elimination.m % 高斯消元 │ ├── lu_decompose.m % LU 分解 │ ├── jacobi.m % Jacobi 迭代 │ └── sor.m % 逐次超松弛迭代 ├── 03_数值积分/ │ ├── trapezoid.m % 梯形公式 │ ├── simpson.m % Simpson 公式 │ └── romberg.m % Romberg 积分 ├── 04_常微分方程/ │ ├── euler.m % Euler 法 │ ├── improved_euler.m % 改进 Euler 法 │ ├── rk4.m % 四阶 Runge-Kutta │ └── householder.m % Householder 相关实现 └── utils/ └── plot_compare.m % 通用对比绘图这种「一个方法一个文件」的布局有个好处:你想验证哪个算法,直接进对应目录跑 demo 就行,不用在几百行的巨型脚本里找函数。坏处是函数之间没有统一接口,参数顺序得逐个看文件头注释。我一般会先扫一遍每个文件开头的注释块,那里通常写着函数签名和用法示例。
2.2 函数签名与调用约定
教学型 Matlab 代码的函数签名往往长这样,以 Newton 法为例:
function [x, iter, err] = newton(f, df, x0, tol, maxiter) % NEWTON Newton 迭代法求解非线性方程 f(x)=0 % 输入: % f - 目标函数句柄,如 @(x) x^3 - x - 1 % df - 导数函数句柄,如 @(x) 3*x^2 - 1 % x0 - 初始猜测值 % tol - 收敛容差,默认 1e-6 % maxiter - 最大迭代次数,默认 100 % 输出: % x - 近似根 % iter - 实际迭代次数 % err - 每次迭代的误差序列 if nargin < 4, tol = 1e-6; end if nargin < 5, maxiter = 100; end x = x0; err = []; for iter = 1:maxiter fx = f(x); dfx = df(x); if abs(dfx) < eps error('导数接近零,Newton 法失效'); end x_new = x - fx / dfx; err(end+1) = abs(x_new - x); if err(end) < tol x = x_new; return; end x = x_new; end warning('达到最大迭代次数仍未收敛'); end这里有几个参数值得注意:f和df都是函数句柄,意味着你可以传任意匿名函数进去,不用改源码;tol和maxiter用了nargin做默认值处理,调用时只传前三个参数也能跑。err返回的是相邻两次迭代的差,画收敛曲线时直接semilogy(err)就能看出收敛阶。导数接近零的判断用eps兜底,这是防止除零的常见做法,但实际用的时候如果初始值选得离根太远,Newton 法照样会发散,这时候就得换二分法或者调整x0。
2.3 演示脚本怎么串起整个流程
每个分类目录下的demo_*.m是入口。以求根为例,演示脚本一般会做三件事:定义测试函数、调用算法、画对比图。
% demo_root.m 求根算法对比演示 f = @(x) x.^3 - x - 1; % 测试方程 df = @(x) 3*x.^2 - 1; % 导数 [x1, it1, e1] = newton(f, df, 1.5); [x2, it2, e2] = bisection(f, 1, 2, 1e-6); fprintf('Newton: x=%.8f, 迭代 %d 次\n', x1, it1); fprintf('二分法: x=%.8f, 迭代 %d 次\n', x2, it2); figure; semilogy(e1, 'o-'); hold on; semilogy(e2, 's--'); xlabel('迭代次数'); ylabel('误差'); legend('Newton', '二分法'); title('收敛速度对比');这段脚本的价值在于它把「算法调用」和「结果可视化」绑在一起了。你改一下f和df,整条链路就跟着变,不用单独写绘图代码。semilogy用对数纵轴是数值分析里的标准操作,因为误差通常是指数下降的,线性坐标看不出收敛阶的差别。跑完这个 demo,你基本就能理解这套代码的使用节奏:改函数 → 调算法 → 看误差曲线。
3. 核心算法拆解:Newton、Euler 与 Householder 的实现细节
这一章挑三个关键词里的算法重点讲:Newton 迭代、Euler 法、Householder 变换。它们分别对应非线性方程求根、常微分方程初值问题、矩阵分解三个方向,也是计算方法课里最容易考、最容易在课设里被要求对比的几个点。每个算法我都会贴核心代码、讲参数、说清楚哪里容易翻车。
3.1 Newton 迭代法:收敛快但挑初始值
上一节已经贴了 Newton 的完整实现,这里补几个实现层面的细节。第一,err记录的是abs(x_new - x),这是「步长判据」,严格来说还应该加一个「残量判据」abs(f(x_new)) < tol,两者同时满足才更稳。教学代码为了简洁通常只用一个,但如果你拿它去解病态方程,建议把两个判据都加上:
if abs(x_new - x) < tol && abs(f(x_new)) < tol x = x_new; return; end第二,Newton 法的收敛是二次的,前提是初始值落在根的吸引域内。x0选得不好,迭代序列可能直接飞出去。血泪经验是:先用二分法在区间[a,b]上缩到一个较小的范围,再拿中点当 Newton 的x0,这样基本不会翻车。第三,df必须手动传,这是这套代码的一个小坑——它没有做数值微分兜底。如果你只有f没有解析导数,可以临时用中心差分代替:
df = @(x) (f(x+1e-7) - f(x-1e-7)) / 2e-7;精度会掉一点,但对付课设够用了。
3.2 Euler 法与改进 Euler:步长是命门
Euler 法是常微分方程数值解里最朴素的方法,代码短到可以背下来:
function [t, y] = euler(f, tspan, y0, h) % EULER 显式 Euler 法求解 y' = f(t,y) % 输入: % f - 右端函数句柄 @(t,y) % tspan - 时间区间 [t0, tf] % y0 - 初值 % h - 步长 t = tspan(1):h:tspan(2); n = length(t); y = zeros(1, n); y(1) = y0; for i = 1:n-1 y(i+1) = y(i) + h * f(t(i), y(i)); end end逻辑很直白:用当前点的斜率线性外推下一个点。问题在于它是「一阶」方法,局部截断误差是O(h^2),全局误差O(h)。步长h稍微大一点,结果就偏得离谱。改进 Euler 法(也叫 Heun 法)用预测-校正补了一刀:
function [t, y] = improved_euler(f, tspan, y0, h) t = tspan(1):h:tspan(2); n = length(t); y = zeros(1, n); y(1) = y0; for i = 1:n-1 k1 = f(t(i), y(i)); % 预测斜率 yp = y(i) + h * k1; % 预测值 k2 = f(t(i+1), yp); % 校正斜率 y(i+1) = y(i) + h/2 * (k1 + k2); % 平均斜率 end end改进版每步多算一次f,但精度从一阶提到二阶。实际跑的时候,我一般会把h取成(tf-t0)/100到(tf-t0)/1000之间,然后用plot(t,y)看曲线是否光滑。如果曲线出现锯齿或者明显偏离解析解,先缩步长,别急着怀疑代码——Euler 法对步长就是这么敏感。这套代码里如果同时有euler.m和improved_euler.m,演示脚本大概率会把两条曲线画在一张图上对比,那个图就是课设报告里最好用的素材。
3.3 Householder 变换:矩阵分解里的正交化利器
Householder 变换在计算方法课里通常出现在 QR 分解和特征值计算的章节。它的核心是用一个反射矩阵H = I - 2vv^T/(v^Tv)把向量映射到坐标轴上,从而逐步把矩阵化成上三角。一个典型的 Householder QR 实现长这样:
function [Q, R] = householder_qr(A) % HOUSEHOLDER_QR 用 Householder 变换做 QR 分解 [m, n] = size(A); Q = eye(m); R = A; for k = 1:min(m-1, n) x = R(k:m, k); e1 = zeros(length(x), 1); e1(1) = 1; % 构造反射向量,注意符号选择避免相消 v = x + sign(x(1)) * norm(x) * e1; v = v / norm(v); % 应用到当前子矩阵 R(k:m, k:n) = R(k:m, k:n) - 2 * v * (v' * R(k:m, k:n)); % 累积到 Q Q(:, k:m) = Q(:, k:m) - 2 * (Q(:, k:m) * v) * v'; end end这段代码有两个关键点。第一,sign(x(1))那个符号选择不是可有可无的——如果直接取v = x + norm(x)*e1,当x(1)本身是正数且接近norm(x)时,两个大数相减会产生严重的相消误差,数值稳定性直接崩掉。取同号相加才是正确做法,这是 Householder 实现里最经典的坑。第二,Q的累积是从右往左乘反射矩阵的转置,因为H是对称正交阵,H' = H,所以代码里直接用了v'。跑完之后验证一下norm(Q*R - A)是否接近机器精度,再验证norm(Q'*Q - eye(m)),两个都过才算实现正确。如果这套代码里 Householder 是用在特征值计算上的(比如化 Hessenberg 形),思路类似,只是反射作用的对象换成了整个矩阵而不是列向量。
4. 跑通与验证:从零开始把代码跑起来并确认结果可信
代码能跑和结果可信是两回事。这一章讲怎么把环境配好、怎么逐个模块验证、以及怎么用解析解对照检查数值结果。新手最容易犯的错是「跑完没报错就以为对了」,结果交上去被导师一句「你这个误差量级不对」打回来。
4.1 环境准备与路径配置
这套代码只依赖 Matlab 基础功能,不需要额外工具箱(Optimization Toolbox、Symbolic Math Toolbox 都用不上),所以兼容性很好。R2016a 之后的版本基本都能跑。如果你用的是 Octave,大部分语法也兼容,但fprintf的格式化和某些绘图属性可能有细微差别,建议优先用 Matlab。
第一步是把整个文件夹加到搜索路径里,否则跨目录调用函数会报Undefined function:
% 在 Matlab 命令行里执行,把路径换成你实际解压的位置 addpath(genpath('D:\课程\计算方法Matlab代码')); savepath; % 可选,永久保存路径genpath会递归把子目录都加进去,这样你在任何目录下都能直接调newton、euler这些函数。如果不想污染全局路径,也可以在每个 demo 脚本开头临时addpath,跑完rmpath。我一般用前者,省事。
第二步是确认函数能正常调用。随便找个函数试一下:
>> f = @(x) x^2 - 2; >> df = @(x) 2*x; >> [x, iter] = newton(f, df, 1) x = 1.4142 iter = 4如果输出1.4142且迭代 4 次左右,说明 Newton 实现没问题。sqrt(2)的精确值是1.41421356...,你可以用abs(x - sqrt(2))看误差量级,正常应该在1e-8以下。
4.2 逐模块验证:用已知解析解对照
每个模块都要有独立的验证手段,不能只看图好不好看。下面这张表是我常用的对照清单:
| 模块 | 测试问题 | 解析解/基准 | 验证方式 |
|---|---|---|---|
| 方程求根 | x^3 - x - 1 = 0 | x ≈ 1.324718 | 代入f(x)看残量 |
| 线性方程组 | Ax = b,A取 3x3 可逆阵 | x = A\b | 比较norm(x_num - x_exact) |
| 数值积分 | ∫₀¹ 4/(1+x²) dx | π ≈ 3.141593 | 误差随n增大应下降 |
| Euler 法 | y' = -y, y(0)=1 | y = e^{-t} | 比较终点误差 |
| QR 分解 | 随机 4x4 矩阵 | [Q,R]=qr(A) | norm(Q*R-A)和正交性 |
以数值积分为例,Simpson 公式跑∫₀¹ 4/(1+x²) dx,如果分段数n取偶数且足够大,结果应该逼近π。你可以写个小循环看误差收敛:
for n = [4, 8, 16, 32, 64] I = simpson(@(x) 4./(1+x.^2), 0, 1, n); fprintf('n=%3d, 误差=%.2e\n', n, abs(I - pi)); end正常输出应该是误差每翻倍一次降大约 16 倍(Simpson 是四阶方法,误差O(h^4),h减半误差降 16 倍)。如果误差不降反升,八成是分段数传错了或者函数句柄写错了。这种「收敛阶验证」比单纯看结果对不对更有说服力,课设报告里放一张误差收敛表,比十张曲线图都管用。
4.3 结果可视化与报告素材整理
跑通之后,把关键结果整理成能直接贴进报告的图。这套代码的 demo 脚本已经带了基础绘图,但报告里通常需要更规整的格式。我一般会统一设置一下:
set(groot, 'DefaultAxesFontSize', 12); set(groot, 'DefaultLineLineWidth', 1.5); figure('Position', [100 100 600 400]);groot是 Matlab 的图形根对象,这样设置之后所有新画的图都统一字号和线宽,不用每张图单独调。对比类图建议用legend标清楚方法名,误差图用semilogy,收敛阶对比可以在图标题里直接写「Newton 二次收敛 vs 二分法线性收敛」。另外记得把err序列存下来,save('newton_err.mat', 'err'),万一报告要重画图不用重新跑。
5. 避坑与排查:跑这套代码时最容易翻车的几个地方
教学代码看着简单,实际跑起来坑不少。下面这几条是我和周围人踩过的,按「现象 → 原因 → 解决」列出来,你对号入座就行。
5.1 现象:函数报「未定义」或「输入参数不足」
原因通常有两个:一是路径没加,Matlab 找不到.m文件;二是函数签名和你传的参数对不上,比如某个函数要求传函数句柄,你传了字符串'x^2-2'。教学代码里函数句柄和字符串混用的情况很常见,老版本 Matlab 支持字符串当函数传,新版本逐步废弃了。
解决:先which newton确认函数在路径里;再help newton看函数头的参数说明。如果传的是字符串,改成匿名函数@(x) x.^2 - 2。注意匿名函数里要用点运算.^、./,否则传向量进去会报矩阵维度错误。
5.2 现象:迭代不收敛,结果是NaN或Inf
原因:Newton 法导数接近零、Euler 法步长过大、迭代法(Jacobi/SOR)的迭代矩阵谱半径大于 1。数值方法都有收敛条件,不是随便传个初值就能出结果。
解决:Newton 先换初始值或者用二分法缩区间;Euler 把h缩小一个量级再试;迭代法检查系数矩阵是不是对角占优,不占优的话 Jacobi 本来就不保证收敛,换 SOR 并调松弛因子ω(一般取 1 到 2 之间,ω=1退化成 Gauss-Seidel)。如果NaN出现在第一次迭代,多半是初值让f或df算出了非法值,打印一下f(x0)和df(x0)看看。
5.3 现象:QR 分解结果Q*R和原矩阵对不上
原因:Householder 实现里符号选择写错,或者Q累积顺序反了。前面 3.3 节说过,v = x + sign(x(1))*norm(x)*e1里的符号不能随便改,改成减号在特定输入下会丢精度。另一个常见错误是把Q的更新写成了左乘而不是右乘。
解决:用norm(Q*R - A)和norm(Q'*Q - eye(size(A,1)))两个指标卡。第一个应该接近eps量级(比如1e-15),第二个也应该接近1e-15。如果第一个大但第二个小,说明R算错了;如果两个都大,说明Q累积逻辑有问题。拿一个 2x2 的简单矩阵手算一遍对照,比盯着代码看快。
5.4 现象:画出来的收敛曲线不降反升
原因:误差序列的定义搞反了,或者把「相邻步长差」当成了「真实误差」。教学代码里err通常记的是abs(x_new - x),这个量在接近收敛时会变小,但如果算法发散,它也会变大——这时候曲线上升是正常的,说明方法失效了,不是画图错了。
解决:先确认算法本身收不收敛(看最终x代入f(x)的残量),再检查semilogy的输入是不是全正数(对数轴不能有零或负数)。如果err里有零,semilogy会警告并跳过,可以在画图前err(err==0) = eps兜一下。
5.5 现象:Octave 下跑报绘图属性错误
原因:Matlab 和 Octave 的图形对象属性支持不完全一致,比如某些set(groot, ...)的默认属性 Octave 不认。
解决:把全局图形设置注释掉,或者用if exist('OCTAVE_VERSION', 'builtin')判断环境后跳过。数值计算部分两者基本兼容,绘图部分建议还是在 Matlab 里跑,省得折腾。
6. 二次开发:把这套代码改成自己的课设工具链
这套代码最大的价值不是「能跑」,而是「能改」。它结构清晰、每个方法独立,非常适合作为二次开发的起点。我一般会做三件事:统一接口、加一个总控脚本、把结果导出成报告能用的格式。
6.1 统一函数接口,做成可配置的算法调度
原始代码每个函数参数顺序不一样,调用时容易记混。一个实用的改造是写一个统一的调度函数,用switch按方法名分发:
function [x, info] = solve_root(method, f, df, x0, opts) % SOLVE_ROOT 统一求根接口 % method: 'newton' | 'bisection' | 'secant' if nargin < 5, opts = struct(); end tol = getfield_default(opts, 'tol', 1e-6); maxiter = getfield_default(opts, 'maxiter', 100); switch lower(method) case 'newton' [x, iter, err] = newton(f, df, x0, tol, maxiter); case 'bisection' [x, iter, err] = bisection(f, x0(1), x0(2), tol); case 'secant' [x, iter, err] = secant(f, x0(1), x0(2), tol, maxiter); otherwise error('未知方法: %s', method); end info.iter = iter; info.err = err; end function v = getfield_default(s, name, default) if isfield(s, name), v = s.(name); else, v = default; end end这样上层脚本只需要改method字符串就能切换算法,对比实验写起来干净很多。opts用结构体传可选参数,扩展新参数不用改函数签名。这个模式在课设里特别实用——导师让你「对比三种方法」,你写一个循环遍历{'newton','bisection','secant'}就出三组结果。
6.2 加一个批量实验脚本,自动生成对比表
手动一个个跑太慢,写个批量脚本把所有测试用例跑一遍,结果存成表格:
methods = {'newton', 'bisection', 'secant'}; problems = { {@(x) x.^3 - x - 1, @(x) 3*x.^2 - 1, [1, 2]}, ... {@(x) cos(x) - x, @(x) -sin(x) - 1, [0, 1]} }; results = []; for p = 1:length(problems) f = problems{p}{1}; df = problems{p}{2}; interval = problems{p}{3}; for m = 1:length(methods) [x, info] = solve_root(methods{m}, f, df, interval); results = [results; {methods{m}, p, x, info.iter, info.err(end)}]; end end T = cell2table(results, 'VariableNames', {'method','problem','root','iter','final_err'}); writetable(T, 'compare_results.xlsx');跑完直接得到一个 Excel,贴进报告就是现成的对比表。cell2table和writetable是 R2013b 之后的标准功能,不用额外工具箱。注意problems里每个用例的x0传的是区间[a,b],Newton 用第一个元素当初始值,二分法用整个区间,这样统一接口能兼容不同方法对初值的需求。
6.3 改造时的验证习惯
改完代码一定要回归验证,别改一处崩三处。我的习惯是每次改完跑一遍 4.2 节那张对照表,确认所有基准问题的误差量级没变。特别是统一接口之后,参数传递多了一层,很容易出现tol没传下去、maxiter被覆盖这类低级错误。从那以后我每次动接口层,都强制走一遍「基准问题 + 误差量级检查」,宁可多花五分钟,也不想到交报告前一晚才发现结果全错。希望这套拆解能帮你把这套代码真正用起来,而不是躺在文件夹里吃灰。
本文还有配套的精品资源,点击获取