简介:基于Java的数值分析、线性代数与机器学习算法设计源码包,面向科研人员、工程师以及有Java基础的算法学习者,将常用数学计算与机器学习基础模型整合为可直接调用的类与方法,适用于科学计算、工程建模、数据分析和教学实验等场景。包内共45个文件,包含36个Java源码、8个TXT说明与测试数据、1个PNG示意图,压缩包仅544KB,整体按numberAnalysis、linalg、ML等模块组织,层次清晰便于按需定位。内容覆盖数值积分、常微分方程求解、插值、方程求根和最优化算法,提供矩阵乘法、行列式计算、特征值分解等线性代数工具,并实现决策树、朴素贝叶斯、K近邻等基础学习模型;每个算法模块配有对应测试类和测试数据集,可快速验证结果、对比不同方法的表现。目前已有342人学习下载,既可直接嵌入工程项目,也可作为算法实现的研读范例。
1. 用 Java 写数值分析与机器学习:这不是自虐,是给算法包一层工程外壳
很多人一听「用 Java 实现数值分析和机器学习算法」,第一反应是「为什么不用 Python」。这个反应不奇怪,但真实的生产环境里,Java 系的技术栈承担着大量和数值计算相关的任务:交易系统的风险评估、电商推荐排序、工业仿真软件的底层计算模块。这些场景的共同特点是——算法不是终点,嵌入业务系统、承受高并发、稳定跑几年不崩才是终点。Python 适合模型探索,Java 适合把算法变成工程资产。这篇文章围绕「基于 Java 的数值分析、线性代数与机器学习算法设计源码」这个方向,讲清楚三件事:这些算法在 Java 里怎么落地、核心参数怎么调、以及哪些地方最容易翻车。适合的人群很明确:想用 Java 做算法模块开发的中级工程师、需要把 Python 原型改写成 Java 服务的后端开发者、以及正在看这类源码但觉得结构混乱的学习者。
2. 选型与边界:为什么这套源码值得用 Java 而不是 Python 重写
2.1 Java 在数值计算上的真实位置:不是替代 Python,是互补
先立一个判断:Java 做数值计算,从来不是「同样的事情用另一种语言再写一遍」,而是「把计算过程和业务生命周期绑在一起」。典型场景是风控引擎——模型推理和交易决策在同一个 JVM 进程内完成,省掉跨语言调用的开销和序列化成本。另一个高频场景是桌面工具和企业内部系统:客户现场没有 Python 环境,但一定有 JDK。这类源码的价值在于,它让你能在一个受控的运行时里完成从矩阵分解到模型训练的全流程。
Java 的优势体现在三个维度。第一是内存管理:JVM 的 GC 机制允许你用对象数组表达矩阵,而 C++ 那边得手动释放内存,容易在异常路径上漏掉。第二是工程生态:Maven 管理依赖、JUnit 写单测、JMH 做基准测试,这些在数值库的开发里能直接复用。第三是类型系统:泛型和接口能把「矩阵」和「向量」抽象成约束清晰的类型,比 Python 的动态类型更适合多人协作的代码库。
但边界也要说清楚。如果你在做深度学习模型的训练,Java 生态的 GPU 支持远不如 Python。这套源码的适用边界是:经典机器学习算法(回归、KMeans、KNN)、数值分析中的求根与插值、线性代数中的分解与特征值计算。再往上走,就该接 Python 服务或专用计算引擎了。
2.2 源码结构的常见组织方式:算法包怎么分才不烂
看这类源码,第一件事是摸清楚包结构。常见的做法是按「数学分支」划分顶层包,而不是按「业务场景」划分,因为算法是通用资产,业务会变,算法不会。
典型结构是这样的:
src/main/java ├── numeric/ # 数值分析:求根、插值、数值积分 ├── linear/ # 线性代数:矩阵、向量、分解 ├── ml/ # 机器学习:回归、分类、聚类 │ ├── regression/ │ ├── classification/ │ └── clustering/ ├── util/ # 公共工具:数据归一化、误差计算 └── demo/ # 可运行的示例入口我一般建议在linear包下先定义Matrix和Vector两个核心类,因为数值分析和机器学习算法都依赖它们。Matrix内部用double[][]存储,对外提供读取行列数、获取元素、矩阵乘法、转置、求逆这些基础操作。Vector则用double[],支持点积和范数计算。这两个类是整套源码的地基。
关于接口设计有一个容易走偏的点:不要把所有线性代数操作都塞进Matrix类,否则这个类会膨胀成「上帝类」。正确的是把Matrix.java只当成数据结构,把 LU 分解、QR 分解这些算法放独立的Decomposition类里,接收Matrix参数并返回结果。这样的设计让每个算法类只承担一个职责,测试时也不需要构造完整的矩阵对象。
3. 数值分析模块:从求根到插值,Java 实现中的精度控制与终止条件
3.1 二分法与牛顿法:迭代终止条件是最关键的参数
数值分析里最常用的是非线性方程求根,比如计算隐含波动率、求解现金流内部的收益率。二分法和牛顿法在 Java 里的实现都不复杂,但容易在终止条件上踩坑。
二分法的核心逻辑是不断缩小区间,直到区间长度小于容差。牛顿法则需要给出迭代初值和雅可比(或导数)的近似。先看二分法的实现:
public class BisectionSolver { /** * 二分法求根 * @param f 目标函数 * @param a 区间左端点 * @param b 区间右端点 * @param tol 容差,控制精度 * @param maxIter 最大迭代次数,防止死循环 */ public static double solve(UnaryOperator<Double> f, double a, double b, double tol, int maxIter) { double fa = f.apply(a); double fb = f.apply(b); if (fa * fb > 0) { throw new IllegalArgumentException("区间两端函数值必须异号"); } for (int i = 0; i < maxIter; i++) { double mid = (a + b) / 2.0; double fm = f.apply(mid); if (Math.abs(fm) < tol || (b - a) / 2.0 < tol) { return mid; } if (fa * fm < 0) { b = mid; fb = fm; } else { a = mid; fa = fm; } } return (a + b) / 2.0; } }这里有两个参数决定成败。tol是容差,建议不要设小于1e-12,因为double本身只有大约 15 位有效十进制位,设更小的容差没有实际意义,只会增加迭代次数。maxIter是保险丝,二分法最坏情况下的迭代次数是log2((b-a)/tol),我一般在这个理论值上再加 20% 作为安全余量。函数值判断Math.abs(fm) < tol是为了应对目标函数在根附近特别平缓的情况——此时区间长度可能还很大,但函数值已经接近零。
牛顿法的收敛速度远快于二分法,但有两个前提:初值靠近根、函数在根附近连续可导。代码里最关键的是迭代条件:
public static double solve(UnaryOperator<Double> f, UnaryOperator<Double> df, double x0, double tol, int maxIter) { double x = x0; for (int i = 0; i < maxIter; i++) { double fx = f.apply(x); double dfx = df.apply(x); if (Math.abs(dfx) < 1e-10) { throw new ArithmeticException("导数接近零,牛顿法不适用"); } double xNext = x - fx / dfx; if (Math.abs(xNext - x) < tol) { return xNext; } x = xNext; } throw new RuntimeException("超过最大迭代次数,未收敛"); }注意这里检查了dfx是否接近零——这是牛顿法最常见的炸点。如果初始点落在函数极值点附近,dfx很小,fx / dfx会给出一个巨大的步长,迭代直接飞到十万八千里外。经验做法是:在迭代过程中给步长加一个上限,比如步长不允许超过区间宽度的 10 倍,否则重置为二分法的一步。
3.2 插值与数值积分:边界条件决定结果可用性
插值在业务里的典型场景是补齐缺失的时间序列数据或构建价格曲线。拉格朗日插值在理论上有名,但实际代码里用得不多——高次多项式插值在端点附近会剧烈震荡,这就是著名的龙格现象。我一般推荐分段线性插值或三次样条插值。三次样条代码里最重要的是边界条件参数,常见的有「自然边界」(二阶导数为零)和「固定斜率边界」。多数情况下,自然边界就够了。
public class CubicSplineInterpolator { // x 和 y 是已知数据点,x 必须严格递增 public static double[] computeSecondDerivatives(double[] x, double[] y) { int n = x.length; double[] d = new double[n]; // 二阶导数值 double[] a = new double[n]; // 三对角矩阵系数 double[] b = new double[n]; double[] c = new double[n]; // 构造三对角方程组的系数矩阵 for (int i = 1; i < n - 1; i++) { double hPrev = x[i] - x[i - 1]; double hNext = x[i + 1] - x[i]; a[i] = hPrev; b[i] = 2.0 * (hPrev + hNext); c[i] = hNext; d[i] = 6.0 * ((y[i + 1] - y[i]) / hNext - (y[i] - y[i - 1]) / hPrev); } // 自然边界条件:端点二阶导数为零 d[0] = 0; d[n - 1] = 0; return tridiagonalSolve(a, b, c, d); } }数值积分则是另一套逻辑。辛普森法则在实际代码里比梯形法则常用得多,因为它在同样步长下精度更高。自适应辛普森积分是一个值得掌握的技巧:把区间不断二分,对比粗积分和细积分的差值,差值小于容差就返回——这样在函数平滑的区域自动用大步长,在抖动剧烈的区域自动加密,效率比均匀网格高很多。用 Java 实现时注意递归深度限制,一般用「栈 + 区间列表」模拟递归,防止StackOverflowError。
4. 线性代数核心:从矩阵分解到特征值计算的 Java 实现策略
4.1 为什么不建议手写矩阵求逆,以及 LU 分解的正确用法
线性代数模块是整个源码的地基。很多初写这类代码的人上来就要inverse()方法,这是一个误区。实际工程里求逆几乎都是打着「求逆」的旗号在解线性方程组,而解方程组应该用 LU 分解而不是显式求逆——显式求逆的计算量是 LU 分解的三倍,而且数值稳定性更差。你应该实现的底层操作是solve(A, b),内部调用 LU 分解,而不是inverse(A)再乘b。
public class LUDecomposition { private final Matrix lu; private final int[] pivot; private final int n; public LUDecomposition(Matrix m) { this.n = m.rows(); this.lu = m.copy(); this.pivot = new int[n]; for (int i = 0; i < n; i++) { pivot[i] = i; } for (int k = 0; k < n - 1; k++) { // 部分选主元:在第 k 列中找到绝对值最大的行 int maxRow = k; double maxVal = Math.abs(lu.get(k, k)); for (int i = k + 1; i < n; i++) { if (Math.abs(lu.get(i, k)) > maxVal) { maxVal = Math.abs(lu.get(i, k)); maxRow = i; } } if (maxVal < 1e-12) { throw new ArithmeticException("矩阵奇异或接近奇异"); } // 交换行并记录置换 swapRows(k, maxRow); pivot[k] = maxRow; // 计算乘子并消元 for (int i = k + 1; i < n; i++) { double factor = lu.get(i, k) / lu.get(k, k); lu.set(i, k, factor); for (int j = k + 1; j < n; j++) { lu.set(i, j, lu.get(i, j) - factor * lu.get(k, j)); } } } } public double[] solve(double[] b) { // 先应用置换,再回代 double[] y = new double[n]; // 前向替代 Ly = Pb for (int i = 0; i < n; i++) { double sum = b[pivot[i]]; for (int j = 0; j < i; j++) { sum -= lu.get(i, j) * y[j]; } y[i] = sum; } // 后向替代 Ux = y double[] x = new double[n]; for (int i = n - 1; i >= 0; i--) { double sum = y[i]; for (int j = i + 1; j < n; j++) { sum -= lu.get(i, j) * x[j]; } x[i] = sum / lu.get(i, i); } return x; } }这段代码里有几个必须注意的参数和设计细节。选主元阈值1e-12用于判断矩阵是否奇异,但这个值不是绝对的——如果你的矩阵元素本身在1e8量级,判断奇异性的阈值应该相应上调。更稳妥的方式是计算矩阵的「条件数」来判定,但这会增加成本。工程上的折衷是:先做 LU 分解,如果分解过程中发现主元太小,再抛出异常让上层决定是否用 SVD 兜底。回代部分我拆成了两步,Ly = b和Ux = y,虽然看起来多了循环,但逻辑清楚,调试时有明确的中间量可查。
在这个类里还藏着一个容易被忽略的性能问题:频繁调用get和set方法。如果矩阵类内部没有做边界检查,JVM 的 JIT 编译器能优化掉部分开销;但如果加了防御性边界检查,性能会显著下降。常见的折衷方案是:矩阵类直接暴露包私有数组访问方法,算法类在内部直接操作数组,仅在对外 API 层做检查。
4.2 幂迭代与 QR 算法:特征值计算在 Java 里的工程取舍
特征值计算是线性代数里最容易被「赶进度」略过的内容,但推荐系统里的 PageRank 类算法、主成分分析里的协方差矩阵分解,最终都落到特征值分解上。幂迭代法的实现极其简单,逻辑是「反复用矩阵乘以向量并归一化,向量会收敛到最大特征值对应的特征向量」。这个方法适合求最大特征值,但如果你需要全部特征值,就得用 QR 算法。
QR 算法的原理这里不展开了,重点说 Java 实现时的两个关键决策。第一是 Householder 变换把矩阵化为上海森伯格形(几乎三角矩阵)——这一步必须先做,否则直接迭代 QR 分解的收敛速度慢到无法使用。第二是移位策略:在迭代的每一步用对角元素的近似特征值做移位,可以大幅加速收敛。标准的 Java 实现里,这两个步骤大约占据了整个特征值类 70% 的代码量。如果你的源码里没有这两个预处理,基本可以判断这个特征值实现只适合教学演示,不适合生产。
在实际业务里,我遇到的「需要特征值但矩阵规模巨大」的场景,几乎都不会用完整的特征值分解,而是用奇异值分解 SVD。SVD 的 Java 实现比特征值分解更稳,因为它在任何矩阵上都可计算,数值稳定性更好。如果你在改造这套源码,建议优先把 SVD 类的优先级提到特征值类之前。
5. 机器学习算法落地:线性回归、KMeans 与 KNN 的 Java 实现细节
5.1 线性回归的两种路线:正规方程与梯度下降的选择依据
机器学习模块里,线性回归是最好的起点,因为它同时涉及数值分析和线性代数两个模块的知识。实现时有两种路线:正规方程法和梯度下降法。正规方程直接求解(X^T X)^{-1} X^T y,代码简单、结果确定;但它要求X^T X可逆,而且计算复杂度是O(n^3),其中n是特征数。当特征维度超过一万,正规方程就不太划算了。
梯度下降法则适用于高维度、大样本量场景。实现要点是特征归一化和学习率的选择:
public class GradientDescent { private double[] weights; private final double learningRate; private final int maxIterations; private final double tol; public GradientDescent(double learningRate, int maxIterations, double tol) { this.learningRate = learningRate; this.maxIterations = maxIterations; this.tol = tol; } /** * 批量梯度下降训练 * @param x 样本矩阵,已做归一化 * @param y 目标值 */ public void fit(double[][] x, double[] y) { int m = x.length; int n = x[0].length; weights = new double[n]; for (int iter = 0; iter < maxIterations; iter++) { double[] gradient = new double[n]; for (int i = 0; i < m; i++) { double prediction = predict(x[i]); double error = prediction - y[i]; for (int j = 0; j < n; j++) { gradient[j] += error * x[i][j]; } } for (int j = 0; j < n; j++) { gradient[j] /= m; weights[j] -= learningRate * gradient[j]; } if (computeLoss(x, y) < tol) { break; } } } }这段代码里最关键的参数是learningRate。初学者最常犯的错是把学习率设成0.01或0.001后期待通用——实际上学习率必须根据特征缩放后的数据分布来定。经验做法:先设0.1跑一次,观察 loss 是否震荡。震荡就把学习率除以 10,loss 下降太慢就乘以 3 或 5。另外注意tol是 loss 的阈值,不是梯度范数的阈值。loss 曲线在接近最优点时会变得平坦,用 loss 阈值做终止条件不容易过早停止。生产上我更推荐「梯度范数小于阈值」作为终止条件,因为 loss 的绝对值和样本量、特征维度相关,不好给通用值。
还有一个和数值分析模块衔接的点:梯度下降本质上是一个「优化问题」,和前面说的牛顿法同源。如果你在这套源码里看到有人用二阶导数(牛顿法)做线性回归的求解,那通常是为了加速收敛,但代价是需要计算 Hessian 矩阵的逆,这在特征维度高时得不偿失。
5.2 KMeans 聚类的三个翻车点:初始化、空簇、收敛判断
KMeans 的实现看起来是所有机器学习算法里最简单的,翻车点却一点也不少。第一个坑是初始中心点的选择。标准的随机初始化在数据分布不均匀时容易让某个初始中心落在离群点上,导致最终聚类结果方差剧烈波动。解决方案是用 KMeans++ 初始化:先随机选第一个中心,然后按「距离平方成正比」的概率选后续中心。这个改进只增加少量代码,聚类稳定性的提升却很明显。
第二个坑是空簇处理。某个簇在迭代中可能一个样本都分不到,此时如果直接保持中心点不变,这个中心点就「死」了,不会再吸引任何样本。常见的做法是:将空簇的中心重置为距离当前所有中心最远的样本点,或者重置为全局最远的点。
第三个坑是收敛判断。用「中心点不再移动」作为终止条件有一个隐患:在数据量大的时候,中心点的移动幅度很小但始终不为零,会导致迭代次数过多。我一般用「中心点移动的欧氏距离之和小于阈值」或者「达到最大迭代次数」这两个条件配合使用。
public class KMeans { private final int k; private final int maxIterations; private final double tol; public KMeans(int k, int maxIterations, double tol) { this.k = k; this.maxIterations = maxIterations; this.tol = tol; } public int[] fit(double[][] data) { int n = data.length; int dim = data[0].length; double[][] centers = initCentersKMeansPlusPlus(data); int[] assignments = new int[n]; for (int iter = 0; iter < maxIterations; iter++) { // E 步:分配样本到最近中心 for (int i = 0; i < n; i++) { assignments[i] = nearestCenter(data[i], centers); } // M 步:重新计算中心 double[][] newCenters = new double[k][dim]; int[] counts = new int[k]; for (int i = 0; i < n; i++) { int c = assignments[i]; counts[c]++; for (int j = 0; j < dim; j++) { newCenters[c][j] += data[i][j]; } } // 处理空簇 for (int c = 0; c < k; c++) { if (counts[c] == 0) { newCenters[c] = findFarthestPoint(data, centers); } else { for (int j = 0; j < dim; j++) { newCenters[c][j] /= counts[c]; } } } double movement = calculateMovement(centers, newCenters); centers = newCenters; if (movement < tol) { break; } } return assignments; } }5.3 KNN 不需要训练,但数据预处理和搜索结构决定了生死
KNN 是这套源码里最容易让新手产生「我能轻松写完」错觉的算法。它的核心计算发生在预测阶段,需要计算预测样本到所有训练样本的距离。如果训练集有十万条数据,一次预测就是十万次距离计算,这在实时系统里是致命的。
工程上的常见优化是 KD-Tree 或球树(Ball Tree)。如果你在改造这套源码,建议先实现 KD-Tree,把「找最近的 k 个邻居」的时间复杂度从 O(n) 降到 O(log n) 级别的平均情况。代价是 KD-Tree 在高维数据上性能退化严重——经验值是超过 20 维就考虑用暴力搜索加向量化。
距离度量的选择是另一个容易忽略的点。欧氏距离是默认值,但如果你是做文本向量或离散特征数据的分类,余弦相似度往往更合适。在 Java 里实现这两个距离度量时,建议把它们做成策略接口的实现类,而不是把 if-else 写死在 KNN 类内部。这样换度量时不需要改动主逻辑。
特征归一化对 KNN 的影响比线性回归更严重——因为 KNN 完全依赖距离,某个数值范围特别大的特征会直接压过其他特征对距离的贡献。如果不对特征做标准化,模型的效果基本看天。以我的经验,用 Z-score 标准化就足够,不需要每次都上 MinMaxScaler。
6. 避坑指南:这套源码常见的 5 个翻车点与排查思路
6.1 浮点误差累积导致的计算结果「看起来不对」
现象:矩阵乘法结果和手算结果在后几位小数不一致,或者特征值迭代几百次仍然不收敛。
原因:double类型本身存在舍入误差,在长循环里误差会累积。比如 KMeans 中心的更新在迭代 50 次以后,数值误差开始显现。
解决:不要追求「和理论值完全一致」,用容差比较代替恒等比较。在测试代码里写断言时用assertEquals(expected, actual, 1e-9),不要用assertEquals(expected, actual)。生产代码里,在循环内定期做数值裁剪或重归一化。
6.2 矩阵索引越界在分解算法里反复出现
现象:程序在lu.get(i, j)处抛出ArrayIndexOutOfBoundsException。
原因:手写矩阵分解时,循环边界写错。比如回代循环里j = i + 1应该到n结束,写成j < i + 1,中括号范围漏了最后一列。
解决:给矩阵类加索引校验不是好办法,性能损耗大。正确做法是:写针对分解算法的单元测试,用已知结果的 3x3 矩阵做测试用例,让错误在测试阶段暴露。我一般会在每个算法的测试代码里放「小规模已知答案」的用例,这是成本最低的防御手段。
6.3 梯度下降不收敛,loss 反而越来越大
现象:loss 曲线在前几次迭代下降后突然飙升,或者从一开始就持续增大。
原因:学习率过大。梯度下降在接近极值点时,如果步长太大,会跳过最优点,在两侧来回震荡,甚至发散。
解决:调低学习率,或使用自适应学习率的方法。如果不想引入复杂的优化器,可以先试学习率从 0.1 开始,每次除以 10,找到 loss 单调下降的最大学习率,再在这个值的 1/3 附近细调。另外检查特征是否做了归一化——没归一化时 loss 曲面是狭长的椭圆,梯度方向可能偏离最优点方向。
6.4 数据归一化「漏做」导致的结果异常
现象:KNN 的分类准确率比预期低很多,且不同特征的量纲差异明显时最严重。
原因:KNN 或梯度下降的代码里没有对输入数据做 Z-score 或 MinMax 归一化。数值范围大的特征在距离计算中主导了结果,模型实际上只用了一个特征在做预测。
解决:在数据入口处统一做预处理,不要在算法内部各自处理。比较好的做法是定义一个Preprocessor类,在训练阶段计算均值方差并保存,在预测阶段复用的同样参数。这比每个算法各自实现归一化要好——因为预测阶段如果重新计算均值和方差,结果就和训练阶段不一致了。
6.5 训练和预测阶段的数据处理不一致
现象:训练时准确率不错,但上线后预测结果完全不对。
原因:训练测试阶段和线上预测阶段走了不同的预处理代码路径。比如线下用 pandas 的describe看分布决定标准化,线上用另一种工具实现标准化,参数不同,结果自然就偏了。
解决:把预处理逻辑封装成和模型一起保存的对象。上线时加载模型的同时加载预处理参数,确保训练和推理走同一套变换。这个「模型包」的思维意识,比任何算法细节都重要。
7. 验证与进阶:用 JMH 做基准测试,把算法类和业务代码解耦
讲到最后一个技巧,我想说一个不太起眼但投入产出比很高的事:基准测试。看这类源码时,你可能会把注意力放在算法正确性上,但真正让这套源码在工程里站住脚的,是性能验证和结构设计。前者告诉你它能扛多少量,后者决定你改起来会不会骂人。
做基准测试,推荐用 JMH。不要自己写System.currentTimeMillis()包两圈就跑,JVM 的 JIT 预热会骗你。JMH 的@BenchmarkMode(Mode.AverageTime)配合@Warmup(iterations = 5, time = 1)能给出可信的数字。我在验证这套源码里的矩阵乘法时踩过这个坑:第一次手写计时测出 50 毫秒,用 JMH 正确测完是 3 毫秒——差距大到会让你误判性能瓶颈。
@Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Warmup(iterations = 3, time = 1) @Measurement(iterations = 5, time = 2) public void testMatrixMultiply(Blackhole blackhole) { Matrix result = matrixA.multiply(matrixB); blackhole.consume(result); }还有一个值得做的进阶改造:把算法模块做成独立的 Maven 模块,不依赖任何业务框架。这样做的价值是,你可以把这套算法源码直接放进任何 Java 项目——Spring Boot、纯 Servlet 应用、批处理框架,都能通过 Maven 依赖引入。这个设计本身需要一点克制力:算法类不写@Autowired、不接收 HTTP 请求对象、只处理纯数据。以我的经验和这个方向打交道时,最大的教训就是「算法代码混入业务代码后,想重用的成本接近重写」。如果你打算基于这套源码扩展自己的库,从第一天起就把业务边界划清楚。
最后说一个个人习惯:我会在项目里引入org.apache.commons.math3作为参照实现,某段代码写完了,拿它的结果跑一遍对比。不是让你直接用它替代自己写的代码——用第三方库做交叉验证,比自己检查计算过程高效得多,尤其在数值分析和线性代数模块。能写出「正确」的算法代码只是第一步,能验证自己的算法代码没有偏离数值计算的基本准则,才是真正的工程能力。希望帮到你。
本文还有配套的精品资源,点击获取