☰
C++实现高斯-约当消元法:从原理到数值稳定性实践
2026/10/6 9:04:13 网站建设 项目流程

最近写数值计算的代码,又把高斯-约当(Gauss-Jordan)消元法翻了出来。这个算法大概是线性代数课上最先学的求解线性方程组方法之一,也是我最早用 C++ 完整实现过的数值算法之一。它把“消元”这件事做到极致:目标只有一个,就是把增广矩阵的系数部分化成单位矩阵,然后方程组的解直接写在最后一列,连回代都省了。

听起来很简单,但真正写成能跑、能处理浮点误差、能正确区分无解和无穷解的 C++ 代码,里面还是有不少门道。这篇文章我会从算法原理开始,讲到 C++ 代码的关键设计、完整实现、数值稳定性问题,再到矩阵求逆的扩展,最后列出我实际开发中踩过的一些坑。适合正在学 C++ 和线性代数的同学,也适合需要在工程项目里快速计算中小规模方程组的开发者。

1. 高斯-约当消元法:用C++做一次完整消元

1.1 和高斯消元只差一步,但这一步很关键

先理清概念。传统的高斯消元(Gaussian Elimination)分两步走:第一步是把增广矩阵通过行变换化成“上三角矩阵”,也就是只保留主元下方的零,主元上方的数不管;第二步是从最后一行开始,把变量一个个回代上去,算出所有未知数的值。

高斯-约当消元法则走得更远。它在消元过程中不只消主元下方的元素,连主元上方的元素也一起消掉,并且把主元所在行整体缩放到主元系数为 1。全部处理完之后,系数矩阵会变成单位矩阵,方程组的解就直接出现在增广矩阵的最后一列,不需要任何回代过程。

用一个 3 元一次方程组来看会更加直观。假设有方程组:

x + 2y + 3z = 9 2x + 3y + z = 8 3x + y + 2z = 7

增广矩阵是:

[ 1 2 3 | 9 ] [ 2 3 1 | 8 ] [ 3 1 2 | 7 ]

高斯-约当消元法做完后,理论上会得到:

[ 1 0 0 | 0.6667 ] [ 0 1 0 | 1.6667 ] [ 0 0 1 | 1.6667 ]

所以答案直接就是 x ≈ 0.6667,y ≈ 1.6667,z ≈ 1.6667。

从计算过程来看,高斯-约当相比普通高斯消元只是多做了“消去上方元素”这一步,但这让算法的编程逻辑变得特别清晰:你不需要单独写一个回代函数,只需要一个统一的行变换循环。

1.2 为什么消元不会改变方程组的解

很多初学者第一次看到消元法时,心里都会犯嘀咕:把方程两边同时乘个系数,或者拿一个方程去减另一个方程,怎么就能保证解不变?

原因在于消元过程只用到了三种“初等行变换”,它们都是可逆操作:

  • 交换两行:本质上就是调整方程组的书写顺序,方程组本身没变。
  • 某一行乘以非零常数:相当于把某个方程两边同时乘以同一个数,等号仍然成立。
  • 某一行加上另一行的若干倍:相当于把一个方程的倍数加到另一个方程上,这个操作是可逆的,逆操作就是再减去同样的倍数。

这些操作都不会改变方程组的解集,因为它们只是对线性方程组做了等价变换。从几何上看,每个线性方程在空间中对应一个超平面,行变换相当于对超平面方程组进行重新组合、缩放和排列,但所有平面的交集——也就是方程组的解——完全不变。

这一点是高斯-约当消元法能成立的基石。你在 C++ 代码里做的一切交换、归一化、加减操作,本质上都是在矩阵的行空间里做等价变形,只要操作是这三种初等行变换,解就不可能被扭曲。

1.3 复杂度、适用边界和最常用的场景

高斯-约当消元法的时间复杂度是 O(n^3),空间复杂度是 O(n^2)。n 是未知数个数。道理也很简单:外层循环要处理 n 个主元列,对于每个主元列,你要扫描剩余 n 行找主元,然后对该行的 n+1 个元素做归一化,再对其他 n-1 行各做一次行加减,加起来就是 n 的三次方量级。

这个复杂度决定了它天然适合“中小规模稠密矩阵”。什么叫中小规模?我的经验是,在普通台式机上用 double 类型算几百阶以内的稠密线性方程组,高斯-约当消元法完全是够用的,速度很快。但一旦 n 上了几千,或者矩阵是稀疏的(大量零元素),再用它就很不划算了,这时候应该考虑迭代法(比如共轭梯度法、GMRES)或者专门的稀疏直接法。

实际应用里,高斯-约当消元法经常出现在这些场景:

  • 电路节点电压方程求解,节点数通常在几十到几百。
  • 最小二乘曲线拟合时求解正规方程。
  • 计算机图形学里求解单应矩阵、基础矩阵之类的参数。
  • 计算机视觉里的相机位姿估计。
  • 各种教学实验、算法原型开发。

我在写一些仿真脚本时,就经常直接内置一个小型线性方程求解器,避免引入额外的矩阵库依赖。高斯-约当在这种“临时工”角色里表现很好,因为它代码量小,行为直观,出了问题也好排查。

2. C++实现前的三个关键设计

2.1 用vector<vector >存矩阵,避免裸指针

实现之前,先得确定矩阵用什么样的数据结构存。我推荐直接用std::vector<std::vector<double>>,也就是二维动态数组。

为什么不推荐 C 风格的裸指针和new?原因很实际:裸指针需要手动管理内存,一旦中间逻辑出错就会内存泄漏,而且写起来又丑又容易越界。vector会自动管理内存,拷贝、移动语义也齐备,排查问题的时候还能配合调试器直接看每个元素的值。

用二维 vector 表示增广矩阵时,如果方程个数是 n,那么矩阵应该有 n 行 n+1 列。最后一列存的是等号右边的常数项 b,前 n 列存的是系数矩阵 A。

还有一个小细节要注意:传入函数时,如果希望保留原矩阵不变,可以选择传值,因为vector会完整拷贝一份;如果不在乎原矩阵被破坏,可以直接传引用并就地修改。我做数值算法时通常倾向于传值,因为处理过程中会频繁交换行,我不想把调用方的数据也改掉,否则后面做残差验证时还得重新构造原始矩阵,太麻烦。

2.2 部分选主元:宁可多写几行,不让精度翻车

这是高斯-约当消元法里最重要的一步,也是最容易被初学者忽略的一步。

先看一个极端例子。假设当前处理到第 k 列,主元位置上恰好是 0.0000000000000001,而下方某个元素是 1。如果不做任何处理,先归一化主元行,就要把所有元素除以 1e-16,结果变成 1e16 量级的数,再拿这一行去消其他行,必然会引入巨大的浮点误差。

更糟糕的情况是主元正好等于 0,直接导致除零,程序跑出NaN或者inf。

部分选主元的策略简单得惊人:在处理第 k 列时,从第 k 行往下扫描该列所有元素,找到绝对值最大的那一行,把它和当前第 k 行交换。这样每次都拿“最稳健的主元”来做归一化,尽量避免除以接近零的数,能在很大程度上抑制误差累积。

为什么不直接做全选主元?全选主元要在整个右下角子矩阵里找绝对值最大的元素,同时还需要记录列交换信息,因为交换列会改变未知数的排列顺序,处理起来复杂得多。实际上,对绝大多数实际遇到的稠密矩阵来说,部分选主元已经足够好。我在自研代码里也只用部分选主元,只有在做病态问题研究时才会考虑更强的策略。

2.3 用状态码表达求解结果,而不是只返回一个解向量

线性方程组不是永远都有唯一解的。它可能无解(增广矩阵矛盾行),也可能有无穷多解(自由变量存在)。所以函数返回值不能设计成一个简单的std::vector<double>,否则你没法区分“无解”和“所有解刚好都是零向量”这两种情况。

我习惯用枚举配合结构体来封装结果:

enum class LinearStatus { UniqueSolution, // 唯一解 NoSolution, // 无解 InfiniteSolutions // 无穷解 }; struct SolveResult { LinearStatus status; std::vector<double> x; };

函数返回一个SolveResult,调用方先判断status,再决定是否读取x。这样语义清晰,后续无论是写测试还是集成到更大的模块里,都很好用。

3. 完整C++代码和运行效果

3.1 可直接编译运行的完整源码

下面这段代码是我在实际项目中精简出来的教学版本,但功能是完整的:支持部分选主元、支持判断无解和无穷解、输出清晰。你把它复制到一个.cpp文件里,编译就能跑。

代码里我做了一些注释,方便你对照前面的原理看:

#include <iostream> #include <vector> #include <cmath> #include <iomanip> #include <stdexcept> const double EPS = 1e-9; enum class LinearStatus { UniqueSolution, NoSolution, InfiniteSolutions }; struct SolveResult { LinearStatus status; std::vector<double> x; }; SolveResult gaussJordan(std::vector<std::vector<double>> a) { int n = static_cast<int>(a.size()); if (n == 0) { return {LinearStatus::InfiniteSolutions, {}}; } int m = static_cast<int>(a[0].size()); if (m != n + 1) { throw std::invalid_argument("增广矩阵必须为 n 行 n+1 列"); } int rank = 0; // 统计有多少个有效主元 for (int col = 0; col < n; ++col) { // 1. 部分选主元:从第 rank 行开始,找当前列的绝对值最大元素 int pivot = rank; for (int i = rank + 1; i < n; ++i) { if (std::fabs(a[i][col]) > std::fabs(a[pivot][col])) { pivot = i; } } // 2. 如果主元位置已经接近0,说明这一列没有新的有效主元 if (std::fabs(a[pivot][col]) < EPS) { continue; } // 3. 交换当前行和主元行 std::swap(a[pivot], a[rank]); // 4. 归一化主元行:让主元变为1 double div = a[rank][col]; for (int j = 0; j < m; ++j) { a[rank][j] /= div; } // 5. 消除其他所有行的当前列 for (int i = 0; i < n; ++i) { if (i == rank) continue; double factor = a[i][col]; if (std::fabs(factor) < EPS) continue; for (int j = 0; j < m; ++j) { a[i][j] -= factor * a[rank][j]; } } rank++; } // 检查无解:某一行前 n 列全零,但最后一列非零 for (int i = 0; i < n; ++i) { bool zeroRow = true; for (int j = 0; j < n; ++j) { if (std::fabs(a[i][j]) > EPS) { zeroRow = false; break; } } if (zeroRow && std::fabs(a[i][n]) > EPS) { return {LinearStatus::NoSolution, {}}; } } // 有效主元个数小于 n,说明有自由变量,无穷多解 if (rank < n) { return {LinearStatus::InfiniteSolutions, {}}; } // 唯一解:把每行主元对应的解取出来 std::vector<double> x(n, 0.0); for (int i = 0; i < n; ++i) { int p = -1; for (int j = 0; j < n; ++j) { if (std::fabs(a[i][j]) > EPS) { p = j; break; } } if (p >= 0) { x[p] = a[i][n]; } } return {LinearStatus::UniqueSolution, x}; } void printResult(const SolveResult& result) { switch (result.status) { case LinearStatus::UniqueSolution: std::cout << "唯一解:" << std::endl; for (int i = 0; i < static_cast<int>(result.x.size()); ++i) { std::cout << "x" << i << " = " << result.x[i] << std::endl; } break; case LinearStatus::NoSolution: std::cout << "无解" << std::endl; break; case LinearStatus::InfiniteSolutions: std::cout << "无穷解" << std::endl; break; } } int main() { // x + 2y + 3z = 9 // 2x + 3y + z = 8 // 3x + y + 2z = 7 std::vector<std::vector<double>> mat = { {1, 2, 3, 9}, {2, 3, 1, 8}, {3, 1, 2, 7} }; auto result = gaussJordan(mat); printResult(result); return 0; }

这段代码的核心逻辑全在gaussJordan函数里。第一步到第五步的循环顺序不能乱:选主元 → 判空 → 交换 → 归一化 → 消元。尤其要注意,即使当前列的主元已经被判为接近0,循环也只是continue而不是break,因为后面的列可能还有有效主元,这样才能正确统计 rank。

3.2 编译、运行和VSCode环境配置

编译这份代码最简单的方式是使用支持 C++11 的编译器。在命令行里,进入源码所在目录后执行:

g++ -std=c++11 -O2 gauss_jordan.cpp -o gauss_jordan ./gauss_jordan

如果你用的是 VSCode 写 C++,配置其实不复杂。核心就三步:安装 C/C++ 扩展、安装 MinGW-w64 编译器、把编译器路径加入 PATH。我第一次在 Windows 上配 VSCode 时,最常踩的坑就是编译器装好了但 VSCode 检测不到,最后发现是 PATH 没配好,把g++所在的bin目录加到系统环境变量里才解决。

如果要在 VSCode 里直接编译运行,可以在项目根目录建.vscode/tasks.json,大致这样写:

{ "version": "2.0.0", "tasks": [ { "label": "build", "type": "process", "command": "g++", "args": [ "-std=c++11", "-O2", "gauss_jordan.cpp", "-o", "gauss_jordan" ], "group": { "kind": "build", "isDefault": true } } ] }

然后按Ctrl+Shift+B就能编译了。调试的launch.json也可以配置,但如果只是跑结果,其实没有它也能正常用。

3.3 换一组方程验证算法正确性

等你的程序跑完上面那个 3 元方程组,你会看到输出:

唯一解: x0 = 0.666667 x1 = 1.66667 x2 = 1.66667

怎么确认这个结果是对的?最简单的办法是把解代回原方程验证。比如把 x=0.6667,y=1.6667,z=1.6667 代入第一个方程:

0.6667 + 2*1.6667 + 3*1.6667 = 0.6667 + 3.3334 + 5.0001 = 9.0002

误差来自浮点数舍入,在可接受范围内。

然后我们再看两个特殊情形。第一个方程组:

x + 2y + 3z = 9 2x + 4y + 6z = 7 x + 0y + z = 2

第二个方程其实是2x + 4y + 6z = 7,但第一个方程两边同时乘 2 后得到2x + 4y + 6z = 18,不可能同时等于 7 和 18,所以这是无解情形,程序会输出“无解”。

第二个方程组:

x + 2y + 3z = 9 2x + 4y + 6z = 18 x + 0y + z = 2

第二个方程恰好是第一个方程的两倍,说明它没有提供新的约束信息,系统只有两个有效约束,三个未知数,所以是无穷解,程序会输出“无穷解”。

这三个测试用例放在一起,基本就把高斯-约当消元法的核心逻辑覆盖完整了,建议你跑通后自己再改改数,加深印象。

4. 数值稳定性:精度、病态矩阵和EPS怎么取

4.1 浮点误差在消元中怎么被放大

高斯-约当消元法看起来是个纯粹的代数过程,但一旦在计算机上用double实现,就必须面对浮点精度问题。double大约能表示 15 到 17 位有效十进制数字,这个精度对大多数场景够用,但在消元过程中,误差会被一步步放大。

举一个很容易复现的例子:假设某个主元是1e-16,而同一列其他元素是 1。部分选主元能帮你避坑,但如果矩阵本身的排列导致你“不得不”用一个非常小的数做除数,归一化后主元行里的元素就会变成1e16量级。接下来用这一行去消元,会和其他正常量级的数相加、相减,由于有效位数有限,原来的小数信息就被“吞噬”了,结果自然不准确。

更隐蔽的一种误差来自“大数减小数”。两个接近的数相减,结果的相对误差会被急剧放大,这在数值分析里叫做“灾难性抵消”。消元过程中反复做行加减,本质上就是在不断制造这种减法。

所以我的原则是:能选主元就选主元,在计算之前先精心重排行的顺序,把下面这些“坑”从源头堵住。这就是为什么很多数值算法都在强调主元策略——不完全是数学需要,更是浮点运算的现实需要。

4.2 病态矩阵:解对微小扰动极其敏感

有些方程组即使你用部分选主元,解出来的结果依然不靠谱。这时候要怀疑的不是算法,而是方程组本身的性质——它可能是个“病态矩阵”。

病态矩阵的定义可以从解对扰动的敏感性去理解。假设矩阵 A 稍微变化一点点,解 x 就会剧烈变化,这种矩阵就是病态的。典型例子是希尔伯特矩阵,它的第 i 行第 j 列元素是1.0 / (i + j + 1),比如 3 阶希尔伯特矩阵是:

[ 1 1/2 1/3 ] [ 1/2 1/3 1/4 ] [ 1/3 1/4 1/5 ]

希尔伯特矩阵的病态程度随着阶数 n 急剧上升。n 到 7、8 的时候,即使选主元,用 double 直接求出来的解也可能误差巨大。我在实验室里跑过一个 10 阶希尔伯特矩阵,求出的解和真实解差了十万八千里,检查了半天才发现不是算法实现有 bug,是矩阵本身就“不好惹”。

怎么早发现病态问题?工程上常用的是条件数(condition number),条件数越大,矩阵越病态。但精确计算条件数比较贵,更实用的做法是计算残差r = A*x - b的范数。如果残差很大,说明解不可信;如果残差很小但你还是觉得奇怪,那就需要对 A 施加一个微小扰动,观察解的变化,用这种方式粗略估计条件数的大小。

4.3 EPS:一个看似简单但非常敏感的常数

代码里我用了const double EPS = 1e-9;来判断元素是否接近零。这个值看起来随意,实际上很敏感。

如果 EPS 设得太大,比如1e-6,会把一些绝对值在1e-7但本该正常参与计算的合法主元误判成 0,从而错误地把一个可解方程组判成无解或无穷解。如果 EPS 设得太小,比如1e-18,又可能让一些实际上非常微小、已经完全不可靠的浮点噪声通过检查,导致后续出现除零或结果完全失控。

double的机器精度大约是2.2e-16,所以1e-9这个量级既高于机器精度,又不会过于激进,是个比较均衡的经验值。在很多开源数值库中,类似1e-12或1e-10也很常见。

代码里另一个细节是:在归一化主元行之前,消去其他行时我也用if (std::fabs(factor) < EPS) continue;跳过了那些系数已经接近0的行。这是为了节省运算,也为了避免在无意义的小数上引入额外噪声。

如果你想在工程中更稳妥一点,可以把绝对阈值改成相对阈值,也就是根据矩阵中最大元素的量级动态调整。比如先扫描整个矩阵找出maxAbs,然后用EPS * std::max(1.0, maxAbs)作为判断阈值。这样即使矩阵元素本身是1e8量级,你也不会因为用一个绝对化的1e-9把数字误判成 0。

5. 常见问题与避坑实录

5.1 主元接近0时,除零只是表面问题

我在初学 C++ 实现高斯-约当时,犯过一个很经典的错误:没有选主元,直接从左到右处理每一行。结果一个看起来很正常的 3 元方程组,解出来全是-nan。

排查到最后才发现,问题出在第二轮消元时,当前主元位置上的数恰好是 0。而我的代码里却直接执行了a[rank][col]当作除数,当然就炸了。

但如果你觉得“我只要加一个 if 判断主元不为0就能解决”,那就把问题想简单了。比除零更隐蔽的情况是“主元够小但又不是0”。比如主元是1e-14,这时候除法能执行,但会把其他项放大到1e14量级,后续的消元结果同样是灾难性的。即使加了部分选主元,也要记得在主元绝对值小于 EPS 时跳过该列,这是双重保险。

我从这次踩坑里得到的经验是:写数值算法时,不仅要考虑“数学上成立”,还要考虑“浮点上稳健”。一段代码如果只能跑在教科书那种精心挑选的矩阵上,那是玩具代码;能跑在随机矩阵、病态矩阵、接近奇异矩阵上,才算是真正能用的代码。

5.2 无解与无穷解:RREF之后的判断顺序

判断无解和无穷解,最怕的就是搞乱顺序。我的建议是:先判断无解,再判断无穷解。

原因在于,一旦某一行出现“左边全零、右边非零”,这个方程组就是彻底矛盾的无解系统。这种情况下,即使 rank 小于 n,也不能归类为无穷解,因为根本没解。

代码里我用了一个最简单的办法:在消元完成后,逐行扫描所有行,如果某一行前 n 个元素全部小于 EPS,而最后一列a[i][n]大于 EPS,直接返回无解。

如果这个检查通过了,再看 rank 是否等于 n。rank 小于 n 说明至少存在一个自由变量,系统有无限多组解。

还有一个很多初学者容易忽略的细节:如果存在自由变量,代码中最后那个“取出解向量”的循环是不能返回正确完整解的,因为它只能给出一个特解,而不是带参数的通用解。所以在实际项目里,如果遇到InfiniteSolutions,我一般会额外计算基础解系,把自由变量需要的参数向量也一并返回,方便上层逻辑使用。

5.3 Windows下C++编译环境的几个坑

虽然算法本身和平台无关,但我在 Windows 上编译运行这份代码时还是踩过几个环境相关的坑。

第一个是 Visual Studio 里的安全警告。直接使用数组或某些 CRT 函数时,VS 会报_CRT_SECURE_NO_WARNINGS之类的错误。解决方案是在项目属性里定义预处理器宏,或者文件开头加#define _CRT_SECURE_NO_WARNINGS。

第二个是 VSCode 配置 C/C++ 环境时,很多人下载了编译器但忘了设置 PATH,导致终端里输入g++找不到命令。解决办法是找到 MinGW-w64 安装目录下的bin文件夹,把它添加到系统环境变量 PATH 中,然后重新打开终端。添加完可以输入g++ --version验证。

第三个是版本选择问题。注意别下载到老的 MinGW 版本,我建议装 MinGW-w64 的较新版本,并且尽量选择支持 C++17 的版本。本文代码只需要 C++11,但现代 C++ 项目往往还会用到std::optional、结构化绑定等新特性,一步到位更省心。

还有个小提示:如果你在编译时遇到error: 'stod' is not a member of 'std'之类的错误,多半是编译器太老,试着加-std=c++11或升级编译器。

6. 从线性方程组到矩阵求逆和工程落地

6.1 高斯-约当求逆:改两个地方就行

高斯-约当消元法不仅用来解线性方程组,还能直接用来求矩阵的逆。思路特别直观:把要求逆的矩阵 A 和一个单位矩阵 I 拼成一个 n 行 2n 列的增广矩阵[A | I],然后对这个增广矩阵做高斯-约当消元。

如果消元后左边变成了单位矩阵,那么右边就是 A 的逆矩阵:

[A | I] -> 行变换-> [I | A^-1]

这是同一个算法换一个用法,代码改造起来只需要改两处。

第一处是把矩阵列数从n+1改成2n,右侧初始化为单位矩阵而不是方程组常数项。第二处是消元完成后,取右侧 n 列作为结果。如果消元过程中发现 rank 小于 n,说明 A 不可逆,直接返回错误状态。

逆矩阵在很多工程场景都有用,比如解线性方程时如果要对多组右端项 b 同时求解,先把逆矩阵算出来就能批量处理。不过要提醒一点:对病态矩阵,直接求逆再乘 b 往往不如直接用消元法稳定,所以如果只是解一次方程组,没必要先求逆。

6.2 行列式、稀疏矩阵和工业级数值库

同样的行变换思路也可以用来计算行列式。一个常见做法是用普通高斯消元把矩阵化成上三角,过程中记录行交换次数和行倍乘因子,最终行列式的值就是对角线乘积再乘上相应的符号系数。需要注意的是,高斯-约当消元法在归一化主元行时会对整行除以主元,这会改变行列式的倍数,所以如果想算行列式,最好使用不归一化的普通高斯消元。

在实际工业项目中,除非是教学或者原型开发,我一般不建议手写高斯-约当去处理大规模矩阵。大规模稠密矩阵可以用 LAPACK、Eigen 这类成熟库,它们对缓存、并行、精度都有精细优化;大规模稀疏矩阵则要用稀疏直接法或迭代法,存储格式又是另一套套路。

但这不等于手写这份代码没有价值。恰恰相反,理解高斯-约当消元法是理解这一切的起点。你只有亲手写过一遍,才能体会选主元、EPS、残差验证这些概念到底在解决什么问题,以后用第三方库时也知道它们背后在干什么。

6.3 一个很实用的调试技巧

数值代码调试起来不像普通代码那么直观,你不能只看返回值对不对,还要看中间过程。我的习惯是写一个printMatrix函数,在每一轮消元结束后把当前增广矩阵打出来。这样我可以人工“跟踪”一遍矩阵是怎么逐步变为单位矩阵的,对照理论步骤就能很快定位是哪一步出了问题。

另外一个验证正确性的实用方法是“反向构造测试”。先用一个整数解 x,乘上系数矩阵 A 计算出 b,然后拿这个 (A, b) 去跑求解器,看看能不能把原来的 x 还原出来。这样做的好处是你能精确知道真实解是什么,误差是多少,而不是只能拿一个模棱两可的结果猜测。

最后再分享一个我在实际调试中反复用到的习惯:不要只验证一组数据。随机生成几十组小规模整数方程组,再写一个双重循环计算残差,把所有失败案例自动收集起来。这样一次能跑出很多潜藏的问题,比自己手动敲数据高效得多。高斯-约当消元法的代码一旦通过了这种随机测试的考验,后面遇到大多实际问题时,基本都能放心使用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询