简介:《实用数值计算方法》配套 C 语言源码包,面向学习数值计算的学生、教师及需要将算法落地到工程中的开发者,提供与甄西丰教材章节对应的可编译示例,用于理解算法原理并动手调试。压缩包共 702 个文件,约 13.83MB,其中 239 个 .c 源文件构成主体,另含 .txt 说明与日志、.exe 可执行程序、.vcxproj/.dsp 等工程配置及 .pdb/.obj 编译中间文件,兼顾源码阅读与直接运行验证。内容覆盖矩阵运算、数值积分、常微分方程、方程求根、无约束优化、插值与曲线拟合、数值微分、偏微分方程、随机数生成及非线性方程组求解等主题,读者可对照教材按章节查阅、编译和修改,既可作为课程实验模板,也能为实际数值程序开发提供参考。对于入门者,源码中的迭代流程、误差控制和函数封装方式便于逐步模仿;进阶读者则可参考其模块划分与工程组织,改造成自己的实验或项目骨架。已有 619 人学习。
1. 一份教材配套的数值计算源码,为什么我还留着它
去年给一台采样设备做数据后处理,板子上的 RAM 只有几十 KB,现成的数学库动辄几百 KB,塞进去就会把程序顶到溢出。最后救场的是我硬盘里存了很久的一套 C 语言数值计算源码——它是某本数值计算方法教材的配套代码,一个算法一个.c文件,没有第三方依赖,gcc一行命令就能编出来。插值、数值积分、线性方程组、非线性方程求根、常微分方程初值问题这些最常见的计算任务,它都覆盖到了。这套代码适合两类人:一是刚学完数值计算方法、需要把公式抄成能跑的代码的学生或者转行做工程的人;二是像我这样要在资源受限环境里塞一个轻量计算模块,又不想自己从头推导和调误差的开发者。它不是可以直接上生产的产品库,但作为算法原型、教学对照和二次开发的起点,性价比很高。
2. 源码全景:纯C单文件结构、模块划分与编译方式
2.1 目录与文件清单:十来个 .c 文件各自负责什么
这套代码的组织方式非常朴素,没有src/、include/、test/这种工程化目录,打开压缩包基本就是一层平铺的源文件。命名也直白,算法叫什么,文件就叫什么。下面这张表是我自己整理过的,按功能把文件分了类,第一次接触时对着看能省不少时间:
| 模块类别 | 典型源文件 | 核心算法 | 上手难度 |
|---|---|---|---|
| 插值与拟合 | lagrange.c、newton_interp.c、leastsq.c | 拉格朗日插值、牛顿差商插值、最小二乘多项式拟合 | 低 |
| 数值积分 | trapezoid.c、simpson.c、romberg.c | 复化梯形、复化辛普森、龙贝格外推 | 低到中 |
| 线性方程组 | gauss.c、lu.c、jacobi.c、sor.c | 列主元高斯消去、LU 分解、雅可比迭代、松弛迭代 | 中 |
| 非线性方程 | bisection.c、newton_root.c、secant.c | 二分法、牛顿迭代、弦截法 | 低 |
| 常微分方程 | euler.c、rk4.c | 欧拉法、四阶龙格-库塔 | 中 |
| 特征值 | power_method.c、qr_eigen.c | 幂法求主特征值、QR 迭代 | 高 |
每个文件基本都是“一个算法函数 + 一个main演示”的结构。演示用的main里写死了算例,比如插值那个文件里给的是一组固定的节点表,积分那个文件里给的是一个具体的被积函数。这种写法在工程上是不合格的,但对读代码的人来说很友好——打开文件、编译、运行,立刻能看到一组输入对应的输出,不用先搞清楚调用约定。
有一点要提前说明:这些文件相互之间几乎没有依赖,不存在一个公共的mathutil.h把工具函数抽出去。求绝对值的fabs、交换两个数的swap、打印矩阵的函数,在每个需要它的文件里各写一份。我第一次看觉得这是重复劳动,后来改代码才发现这是有意为之——每个文件都能单独拿走,扔进任何一个没有构建系统的老项目里都能用。
2.2 为什么坚持纯 C 和定长数组:教学代码的取舍
这套代码全篇用 C89/C99 风格写,没有 C++ 的类,没有 STL,也不用malloc动态分配(少数文件除外)。矩阵一律用二维定长数组传参,最大维度用一个宏定义卡死,形如#define N 20。这种写法放到今天会被静态检查工具骂,但放在它的使用场景里是合理的。
先说好处。第一是可移植,C 语言加上<stdio.h>、<math.h>两个头文件,几乎任何带 C 编译器的平台都能跑,包括很多嵌入式工具链。第二是可读,公式怎么写的,代码就怎么抄,a[i][j]和数学上的 $a_{ij}$ 一一对应,读代码的时候脑子不用做额外映射。第三是安全,定长数组不会出现堆碎片和内存泄漏,这是新手最容易翻车的地方,教学代码干脆把这条路堵死。
代价也很明确。维度上限写死以后,超过N的方程组直接跑不了;函数签名里一旦出现double a[][N],这个N就绑死在编译期,想改成运行期才知道的规模,必须动整个调用链。这是这套代码从“能算”到“能进产品”之间最大的一道坎,后面讲二次开发的时候会重点说怎么迈过去。
提示:如果你只是想验证某个算法是不是对的,不要急着改数组结构,先用它原本的定长写法把结果跑出来对照解析解,确认算法无误之后再动工程化改造,否则出了问题分不清是公式错了还是改造引入了 bug。
2.3 编译与运行:gcc 一行命令和一份最小 Makefile
单个文件编译是最常见的用法。比如要跑复化辛普森积分那个文件,进到目录后直接:
# -O2 开优化,-lm 链接数学库,这两个都不能少 gcc -O2 -Wall -o simpson simpson.c -lm ./simpson-lm必须放在源文件后面,放在前面在某些链接器上会报undefined reference to 'sin'之类找不到数学函数的错误,这是我被坑过好几次的地方。-Wall建议一直开着,这套代码里不少地方存在隐式类型转换和未使用变量的告警,虽然不影响运行,但能帮你提前发现int和double混用这类隐蔽问题。
文件多了以后手敲命令很烦,写个最小 Makefile 更省事:
# 每个 .c 编成一个同名可执行文件,不做库 CC = gcc CFLAGS = -O2 -Wall -std=c99 LDLIBS = -lm SRCS = $(wildcard *.c) BINS = $(SRCS:.c=) all: $(BINS) %: %.c $(CC) $(CFLAGS) -o $@ $< $(LDLIBS) clean: rm -f $(BINS)这个 Makefile 用了通配符和模式规则,make一次把所有源文件都编出来,make clean清干净。$(wildcard *.c)在 GNU make 下有效,如果你用的是 BSD make 或者某些精简工具链,把SRCS这一行改成手写文件列表更稳妥。
跑起来之后你会看到类似这样的输出:
n=2 T=1.71886 S=1.71886 n=4 S=1.71828 n=8 S=1.71828数值对不对、精度有没有随网格加密提高,从这个输出就能大致判断。下一章开始拆具体算法,每个模块挑一到两个核心函数讲。
3. 核心算法拆解:从插值、积分到线性方程组的 C 实现
3.1 插值模块:拉格朗日与牛顿插值的差别在哪
拉格朗日插值是这套代码里最好懂的一个,公式直接翻译成双重循环就行。函数签名通常是double lagrange(double x[], double y[], int n, double t),x、y是节点坐标数组,n是节点个数,t是待求点的横坐标,返回值是插值结果。核心实现长这样:
/* 拉格朗日插值:n 个节点,求 t 处的函数值 */ double lagrange(const double x[], const double y[], int n, double t) { double s = 0.0; int i, j; for (i = 0; i < n; i++) { double p = 1.0; /* 第 i 个基函数 l_i(t) */ for (j = 0; j < n; j++) { if (j != i) p *= (t - x[j]) / (x[i] - x[j]); /* 连乘,跳过分母为 0 的项 */ } s += y[i] * p; } return s; }这里有两层循环,外层遍历节点,内层构造基函数 $l_i(t)=\prod_{j\neq i}\frac{t-x_j}{x_i-x_j}$。if (j != i)这一句是必须的,少了它分母会出现x[i]-x[i]=0,直接得到NaN,而且这种错误在节点少的时候不一定立刻暴露,属于典型的玄学 bug。
参数上要注意n的含义是节点个数,不是区间长度;t可以落在节点之间(内插),也可以落在节点之外(外推),但外推的误差会随距离迅速放大,工程上一般不推荐把t取到节点范围以外太远的地方。
牛顿插值在同一个目录下的另一个文件里,思路是先构造差商表,再用秦九韶式的嵌套乘法求值:
/* 牛顿插值:先算差商表,再自下而上累乘 */ double newton_interp(const double x[], const double y[], int n, double t) { double d[20][20]; /* d[i][0] 为 y[i],d[i][j] 为 j 阶差商 */ int i, j; for (i = 0; i < n; i++) d[i][0] = y[i]; for (j = 1; j < n; j++) for (i = j; i < n; i++) d[i][j] = (d[i][j-1] - d[i-1][j-1]) / (x[i] - x[i-j]); double s = d[n-1][n-1]; /* 从最高阶差商开始 */ for (i = n - 2; i >= 0; i--) s = d[i][i] * 0 + s * (t - x[i]) + d[i][i]; /* 等价于 s = d[i][i] + s*(t-x[i]) */ return s; }差商表的构造是这两层循环的关键:d[i][j]表示从第i-j个节点到第i个节点的j阶差商,递推式是 $f[x_{i-j},\dots,x_i]=\frac{f[x_{i-j+1},\dots,x_i]-f[x_{i-j},\dots,x_{i-1}]}{x_i-x_{i-j}}$。最后的求值循环从最高阶开始往下套,等价于霍纳法则,比直接展开成幂级数少算很多乘法。
两者怎么选?如果所有节点一次性给定、只求几个点,拉格朗日写法更短更不容易错;如果节点是逐步加入的(比如实时采集数据流),牛顿插值能利用已有差商表增量更新,不用从头重算。我在做传感器标定时用的就是牛顿这一版,因为标定点是一批一批测出来的。
3.2 数值积分模块:复化辛普森与龙贝格的外推逻辑
数值积分这块,复化梯形简单但精度一般,复化辛普森是性价比最高的选择。代码用函数指针传被积函数,这样同一个积分器能处理任意函数:
/* 复化辛普森:把 [a,b] 等分 n 份,n 必须为偶数 */ double simpson(double (*f)(double), double a, double b, int n) { if (n % 2 != 0) n++; /* 奇数自动补一,避免调用方踩坑 */ double h = (b - a) / n; double s = f(a) + f(b); /* 两个端点权重 1 */ int i; for (i = 1; i < n; i++) { double xi = a + i * h; s += (i % 2 == 1) ? 4.0 * f(xi) : 2.0 * f(xi); /* 奇数点权重 4,偶数点权重 2 */ } return s * h / 3.0; }辛普森公式的权重规律是“端点 1、奇点 4、偶点 2”,最后统一乘h/3。这里的n是区间份数,不是节点个数,节点数是n+1。如果传入奇数,函数内部做了自增处理,这是个很实用的防御——我见过太多人直接传n=100当然没问题,但传n=101的时候结果会莫名其妙偏掉,查半天查不出原因。
针对精度要求更高的情况,代码里还有一份龙贝格积分,用理查森外推把梯形序列加速:
/* 龙贝格积分:T 表按行外推,k 为外推阶数 */ double romberg(double (*f)(double), double a, double b, int k) { double T[10][10]; int i, j, n = 1; T[0][0] = 0.5 * (b - a) * (f(a) + f(b)); /* 一分段梯形 */ for (i = 1; i <= k; i++) { n *= 2; double h = (b - a) / n; double sum = 0.0; for (j = 1; j < n; j += 2) /* 只加新增的中点,复用旧值 */ sum += f(a + j * h); T[i][0] = 0.5 * T[i-1][0] + h * sum; /* 递推公式,省掉重复计算 */ for (j = 1; j <= i; j++) T[i][j] = T[i][j-1] + (T[i][j-1] - T[i-1][j-1]) / (pow(4.0, j) - 1.0); } return T[k][k]; }这段代码有两个值得学的点。一是T[i][0]用递推而不是重算全部节点,每次只计算新增的中点,计算量减半;二是外推公式(4^j - 1)的分母,j阶外推对应误差项 $h^{2j}$,所以系数是 4 的幂。k一般取 4 到 6 就够,再往上受浮点舍入误差限制,精度反而会下降。
3.3 线性方程组模块:列主元高斯消去为什么是底线
线性方程组求解是这套代码里最接近工业用途的部分。最朴素的顺序高斯消去只做行消元不换行,一旦主元接近 0,舍入误差会被放大到结果完全失真。列主元版本在每一步选列内绝对值最大的元素换到对角线上:
/* 列主元高斯消去:a 为系数矩阵,b 为右端项,x 返回解,n 为阶数 */ int gauss_elim(double a[][N], double b[], int n, double x[]) { int k, i, j, p; for (k = 0; k < n - 1; k++) { p = k; for (i = k + 1; i < n; i++) /* 找列主元 */ if (fabs(a[i][k]) > fabs(a[p][k])) p = i; if (fabs(a[p][k]) < 1e-12) return -1; /* 奇异或接近奇异,直接报错 */ if (p != k) { /* 换行,右端项一起换 */ for (j = 0; j < n; j++) { double tmp = a[k][j]; a[k][j] = a[p][j]; a[p][j] = tmp; } double tb = b[k]; b[k] = b[p]; b[p] = tb; } for (i = k + 1; i < n; i++) { /* 消元 */ double m = a[i][k] / a[k][k]; a[i][k] = 0.0; for (j = k + 1; j < n; j++) a[i][j] -= m * a[k][j]; b[i] -= m * b[k]; } } if (fabs(a[n-1][n-1]) < 1e-12) return -1; for (i = n - 1; i >= 0; i--) { /* 回代 */ double s = b[i]; for (j = i + 1; j < n; j++) s -= a[i][j] * x[j]; x[i] = s / a[i][i]; } return 0; }函数的返回值承担了错误码的角色:0表示成功,-1表示矩阵奇异。这一点很重要,原始版本很多地方是不检查主元的,遇到退化矩阵就默默输出一个天文数字,调用方毫不知情。1e-12这个阈值不是绝对的,如果你处理的矩阵元素量级在 $10^6$ 以上,阈值要相应放大到1e-6左右,用相对量判断更稳。
矩阵用double a[][N]传参,第一维省略、第二维必须给定,这是 C 语言二维数组传参的语法限制,编译器需要第二维长度才能算出a[i][j]的地址偏移。所以N这个宏定得多大,整个程序的可用规模上限就是多大。
除了直接法,目录里还有雅可比迭代和 SOR 迭代这类迭代解法。它们适合大型稀疏矩阵,用定长二维数组反而浪费内存,所以这部分代码在产品化时优先考虑改数据结构和存储格式,比如压缩稀疏行。
3.4 非线性方程求根与初值问题:牛顿法与四阶龙格-库塔
非线性方程求根的两个代表是二分法和牛顿法。二分法稳但慢,牛顿法快但对初值敏感,还要求能提供导数:
/* 牛顿迭代:f 为函数,df 为导数,x0 为初值 */ double newton_root(double (*f)(double), double (*df)(double), double x0, double tol, int maxit) { double x = x0; int i; for (i = 0; i < maxit; i++) { double fx = f(x); double dfx = df(x); if (fabs(dfx) < 1e-14) return NAN; /* 导数接近 0,切线法失效 */ double x1 = x - fx / dfx; /* 牛顿迭代公式 */ if (fabs(x1 - x) < tol) return x1; /* 用相邻两次迭代差判断收敛 */ x = x1; } return NAN; /* 超过最大迭代次数,宣告失败 */ }这里有两个参数需要解释清楚。tol是收敛容差,判定用的是两次迭代值的差|x1 - x|,不是残差|f(x1)|,两者在导数很小的区域差别很大,如果要更严谨,可以两个条件都加。maxit是最大迭代次数,必须有,否则遇到发散序列程序会一直卡着不动。返回NAN是这套代码里比较少见的现代写法,好处是调用方用isnan就能判断失败,不像返回-1那样和真实解混在一起。
常微分方程初值问题里最值得看的是四阶龙格-库塔,它用四次斜率估计拼出一个四阶精度的单步格式:
/* 四阶 RK:f(x,y) 为右端函数,h 为步长,n 为步数,y 数组返回解 */ void rk4(double (*f)(double, double), double x0, double y0, double h, int n, double y[]) { double x = x0, yy = y0; int i; y[0] = yy; for (i = 0; i < n; i++) { double k1 = f(x, yy); double k2 = f(x + h / 2.0, yy + h * k1 / 2.0); double k3 = f(x + h / 2.0, yy + h * k2 / 2.0); double k4 = f(x + h, yy + h * k3); yy += h * (k1 + 2.0 * k2 + 2.0 * k3 + k4) / 6.0; /* 加权平均 */ x += h; y[i + 1] = yy; } }k1到k4是区间内四个不同位置和斜率的采样,加权系数1:2:2:1除以 6 是经典公式。步长h的选择直接决定精度和稳定性,对于解变化剧烈的方程,h太大不仅精度差,还可能输出完全失真的振荡结果。y数组由调用方分配,长度必须是n+1,这个约定代码里没有做检查,传小了就是越界写,属于最危险的一类 bug。
4. 跑通与自证:用解析解做回归,确认结果可信
4.1 编译环境准备与第一批算例
拿到这套源码之后,我建议不要先去读代码,先让它跑起来,心里有个“结果大概长什么样”的底。环境上只需要一个 C 编译器,Linux 下自带的gcc就够,Windows 下用 MinGW 或者 WSL 里的 gcc 都行。检查编译器:
gcc --version # 期望看到 gcc (GCC) 9.x 以上,C99 支持没问题然后找一个最简单的算例,比如积分文件里自带的那段,直接编译运行:
gcc -O2 -Wall -std=c99 -o simpson simpson.c -lm ./simpson如果输出是一堆数字,说明工具链没问题。如果编译报undefined reference to 'sin',检查-lm是不是漏了或者位置不对;如果报'for' loop initial declarations are only allowed in C99 mode,说明-std=c99没加上。这两个是最高频的编译错误,几乎每个刚拿到这套代码的人都会撞上一次。
4.2 用解析解构造回归测试:误差随步长下降的验证
代码能跑不等于算得对。数值计算代码最大的风险是“看起来正常,其实精度丢光了”,所以一定要拿有解析解的问题来对照。我的做法是给每个模块挑一个能手算的问题,比如积分模块用 $\int_0^1 e^x,dx = e-1 \approx 1.71828$,然后写一个小驱动程序,把不同网格下的结果和误差一起打出来:
/* 用解析解做回归:观察误差是否按理论阶数下降 */ #include <stdio.h> #include <math.h> double f(double x) { return exp(x); } double simpson(double (*g)(double), double a, double b, int n); int main(void) { double exact = exp(1.0) - 1.0; /* 精确值 e-1 */ int ns[] = {2, 4, 8, 16, 32}; int i; for (i = 0; i < 5; i++) { double approx = simpson(f, 0.0, 1.0, ns[i]); double err = fabs(approx - exact); printf("n=%3d approx=%.10f error=%.3e\n", ns[i], approx, err); } return 0; }编译的时候把simpson.c里的main注释掉,或者把函数抽到单独文件,然后把两个.o一起链接。运行结果应该是误差每加密一次下降大约 16 倍——辛普森公式的误差阶是 $O(h^4)$,n翻倍时h减半,误差缩小 $2^4=16$ 倍。如果实测只有 4 倍,说明代码实现的其实是梯形公式,或者权重写错了;如果误差根本不降,那基本可以断定是公式逻辑出了问题。
这个“误差阶验证”是我认为最值得抄走的一个习惯。它比单看某一次结果是否接近精确值有用得多,因为它验证的是算法本身的收敛行为,而不是某一次的运气。
同样的办法可以套到别的模块:RK4 用 $y'=y,,y(0)=1$ 解出 $e^x$,观察全局误差是否按 $h^4$ 下降;牛顿法用 $x^2-2=0$ 观察收敛是否大致每步误差平方级缩小。每个模块花十几分钟做一遍,心里对这套代码的可靠性就有数了。
4.3 输出与误差打印的工程化处理
原始代码的printf大多写得很随意,%f默认六位小数,看误差的时候完全不够用。我一般会统一改成科学计数法加重足够的有效位:
/* 误差打印统一用 %e,保证小误差也能看清量级 */ printf("%-8s %-18s %-12s\n", "n", "approx", "error"); printf("%-8d %-18.10f %-12.3e\n", n, approx, err);%-18.10f表示左对齐、总宽 18、保留 10 位小数;%-12.3e是科学计数法保留 3 位小数,这个精度足够看出误差是 $10^{-6}$ 还是 $10^{-12}$。用固定宽度对齐之后,多行输出可以直接目测趋势,比一团乱糟糟的数字强太多。这类改动很小,但对调试效率的提升很明显,属于典型的低投入高回报。
5. 常见问题排查:数值计算代码的五类翻车现场
5.1 主元为零与除零崩溃
现象:高斯消去跑某些矩阵时结果出现inf、nan,或者输出一个明显荒谬的巨大数值,程序本身不报错,正常退出。
原因:消元过程中对角线元素恰好为 0 或极其接近 0,a[i][k] / a[k][k]这一步被放大成天文数字,后续所有计算都被污染。原始的顺序消元版本没有主元选择,遇到这种矩阵必然出问题。
解决:换成列主元版本,每步消元前先找列内绝对值最大的行交换上来;同时在函数入口和每次除法前加一个阈值判断,fabs(pivot) < 1e-12直接返回错误码。阈值不要用== 0.0判断,浮点数几乎不会精确等于零。
5.2 迭代不收敛却被当成发散
现象:牛顿法或者迭代法跑了几十次还没出结果,或者返回的解和初值差不多,调用方以为算法本身不好用。
原因:多半是初值选得不对。牛顿法只在根附近有二次收敛性,初值离根很远时可能来回震荡甚至飞出定义域。另一个常见原因是收敛判据写错了,用|f(x)| < tol判断时,如果函数在根附近很平,残差早就很小了但解还差得远。
解决:先用二分法在区间上定位一个粗略的根,把它当牛顿法的初值,这个组合我一直在用,稳定性好很多。收敛判据同时检查步长和残差两个条件,任一满足且另一个不发散才算收敛。最大迭代次数一定要设,maxit取 50 到 100 都行,超了就返回失败,不要让它无限跑。
5.3 浮点数比较与步长的坑
现象:两个理论上相等的浮点数用==比较结果是假;或者把积分步长设得特别小,精度反而不升反降。
原因:二进制浮点无法精确表示大多数十进制小数,累加过程还会引入舍入误差。步长取得过小时,舍入误差在总误差中的占比上升,超过截断误差成为主导,出现“越算越不准”的反直觉现象。
解决:比较浮点数一律用fabs(a - b) < eps的形式,eps根据量级取,一般1e-9到1e-12。步长选择上,不要迷信“越小越好”,做一次步长减半的收敛性测试,看误差是否按理论阶数下降,一旦不再下降就说明已经触到舍入误差的地板,继续减小没有意义。
5.4 数组维度传参与越界写
现象:程序在某个特定输入下崩溃,或者更糟——不崩溃但结果莫名其妙,换个编译器又正常了。
原因:定长二维数组传参时第二维长度是编译期绑定的,实际传入的矩阵如果小于N不会有问题,但如果函数内部按N去遍历就会读越界。另一类常见错误是 RK4 的y数组长度传成n而不是n+1,循环最后一次写y[n]就踩到了数组尾部之外。
解决:所有函数内部循环边界严格用参数n,不要用宏N;对于输出数组,在函数注释里写清楚“调用方需保证长度不少于 n+1”,并在测试时用-fsanitize=address编一遍跑跑看,越界写基本都会被当场抓住。这个编译选项是我现在的标配,加了之后能省掉大量“查到最后发现是内存问题”的时间。
6. 二次开发:把教学代码改成能进产品模块的三个动作
这套代码直接拿去用是不行的,但改动量没想象中大。我的做法是三步走。第一步,把返回值统一成错误码加输出参数的形式,所有可能失败的函数(消去、迭代、求根)不再返回裸的double,改成int返回状态、结果通过指针带回,调用方必须检查,这是从“能算”到“可靠”的分界线。第二步,把定长数组换掉。矩阵维度改成运行时传入,内部用一维连续内存加宏A(i,j) a[(i)*n+(j)]模拟二维寻址,既保留了a[i][j]的写法,又摆脱了编译期上限。第三步,写回归测试。每个算法至少两个算例,一个正常规模、一个边界规模,把 4.2 节那套误差阶验证固化进测试脚本,改完代码跑一遍,误差阶掉下来就说明改坏了。
| 改造项 | 改造前 | 改造后 | 验证方法 |
|---|---|---|---|
| 错误处理 | 返回裸值,异常静默 | 返回 int 错误码,必检 | 构造奇异矩阵,确认能报错 |
| 矩阵规模 | #define N 20定死 | 运行时传n,一维连续内存 | 用 n=2 和 n=200 各跑一次 |
| 精度保证 | 无测试 | 误差阶回归脚本 | 步长减半,误差应降 16 倍 |
有个细节容易忽略:一维连续内存版本里,A(i,j)宏的展开如果用(i)*n+(j),i、j最好加括号,写成((i)*(n)+(j)),否则遇到A(i, j+1)这种表达式,运算符优先级会让结果错位。这个坑我踩过一次,明明算法没问题,结果就是不对,查了大半天才定位到宏展开。
从那以后我每次动这套数值代码,都强制先跑一遍误差阶回归再提交,哪怕只改了一个打印格式。希望帮到你。
本文还有配套的精品资源,点击获取