ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

高精度计算核心:大数加减乘除的实现与工程选型

高精度计算核心:大数加减乘除的实现与工程选型 做高精度计算这些年我发现一个很有意思的现象很多人一听到“高精度”三个字第一反应是浮点数多留几位小数或者疯狂用 double 撑着。但真正到了线上项目里把人逼疯的往往是整数运算——金额被乘到万分位、密钥做幂运算、坐标经过几轮变换int 和 long long 就已经溢出得面目全非了。所谓“高精度加减乘除”核心就是一套不依赖语言内置位数上限的大数运算方法任何长度、任意数量级的数字都能用代码拆开逐位处理。这套基本功作用在算法竞赛里是万金油在工程里则是金融结算、密码学、编译器、科学计量的兜底方案。毕竟高精度定位、高精度遥感这些词听着很前沿底层去掉滤波和几何变换之后跑得最凶的依然是一堆数字的加减乘除。这篇文章就把我自己的实现思路、代码模板和踩坑记录全部摊开讲忽略华丽包装直接从第一行代码说起。我会用 C 手写一套支持加减乘除的高精度整数再用 Julia 的 BigInt、BigFloat 做对比最后聊清楚什么时候必须自己撸轮子什么时候直接抄现成工具更聪明。1. 为什么“加减乘除”也需要高精度先分清需求再动手1.1 内置类型不够用的真实场景先给新手补个基础C 里 int 通常 4 字节范围大概从 -21 亿到 21 亿long long 通常 8 字节最大也只有 9223372036854775807。看起来很吓人对吧但很多领域的数字轻轻松松就突破这个上限。举三个我实际碰过的场景。场景一是金融结算。订单总额、折扣、税率、积分抵扣每一步都可能产生小数最后必须按“分”为单位四舍五入。如果直接在 double 里做加减乘除0.1 0.2 都会变成 0.30000000000000004。很多业务规则规定金额要保留到分、厘甚至更高精度用二进制浮点数保存就会累积出账目差异。正规做法是用十进制字符串或定点整数来表示这就是典型的高精度。场景二是密码学。RSA 密钥生成、大整数幂运算、椭圆曲线点运算动辄就是 1024 位、2048 位的二进制大数。long long 在这种量级面前连零头都装不下必须把数拆成数组一段一段算。我刚开始接触的时候心想“这不是发明轮子吗”后来才发现手写大数才能控制常数性能也更清楚每一步在算什么。场景三是坐标换算。高精度定位和高精度遥感这类应用里我经常看到经纬度、UTM 坐标被放大了十倍、百倍转成整数参与计算。你以为是浮点数精度不够结果排了半天查出来是两个 int 乘出负数导致坐标漂移。定位领域很多成熟的代码库宁愿用整数毫米作为单位就是为了让加减乘除可预期、可复现。所以高精度加减乘除不是在校验“算得对不对”而是在解决“根本算不出来”的问题。内置类型装不下就必须另寻出路。1.2 表示方案选型字符串、数组还是语言内置大数高精度问题的第一步不是写算法而是决定数字怎么存。我见过不少人上来就用 string 保存每个字符代表一位数字加减法写得确实很顺手但一旦乘法复杂度上来字符操作的开销会非常难受。我的习惯是用 vector 低位在前即数组下标 0 存个位下标 1 存十位依此类推。低位在前的理由很直接做加减法时进位只会往高位传播操作集中在数组尾部用 push_back 扩展自然且高效。输出结果时需要从后往前打印这个反转成本很小完全值得。数字的符号怎么办我习惯在结构体里单独放一个 bool neg表示是否为负数。绝对值运算都按无符号处理最后根据符号规则拼结果。这样加法器、减法器、乘法器都只处理正数逻辑简单得多不容易犯混乱。下面是基础结构#include bits/stdc.h using namespace std; struct BigInt { vectorint digits; // 低位在前 bool negative false; BigInt() : digits(1, 0) {} BigInt(long long x) { if (x 0) { digits.push_back(0); return; } if (x 0) { negative true; x -x; } while (x) { digits.push_back(x % 10); x / 10; } } };至于选择 base 是多少这是一步非常重要的取舍。最简单是一格存一位十进制数字也就是 BASE 10代码直观适合学习和调试。如果需要性能就做“压位”让一个元素存 10000 或 100000000 这样的一整段十进制数。压位会显著减少数组长度、减少循环层数、减少进位次数代价是输出和输入时要处理前导零。后面讲乘法的时候我会专门展开。选型对比可以总结成一张表方案优点缺点适合场景string 一字符一位直观输入输出方便乘除性能差占用内存高考试题、快速验证vector 一格一位逻辑清晰可读性好运算循环次数偏多教学、通用模板vector 压位万/亿进制性能好内存省前导零处理烦工程级大数库语言内置 BigInt/BigFloat零开发成本API 完善性能不可控定制困难原型、胶水脚本这里顺便强调一个很多人忽略的细节高精度数字在初始化时就要把前导零清干净。如果 digits 尾部也就是数字高位残留了一个 0在做比较、除法、输出时都会出各种诡异问题。我自己的习惯是写一个 trim 函数操作完统一调用。void trim() { while (digits.size() 1 digits.back() 0) digits.pop_back(); }这个函数是整套模板的地基后面所有运算结束后都会调用一遍目的是保证 digits 的最高位不为 0。注意数字 0 也要保留一位否则空 vector 会让后续所有判断都失控。2. 加法与减法先搞定数字的存放方式和进位借位2.1 加法的实现和细节加法实现的核心是逐位相加加进位最后别忘了把最高位的进位塞进去。很多人写加法时习惯先按较短数的长度算一轮再单独处理较长数剩下的位数代码会显得很长。我更推荐一次性分配最长的长度加一位的空间循环里通过下标判断当前位是否存在。这样代码统一也不容易漏掉边界。BigInt add(const BigInt a, const BigInt b) { if (a.negative ! b.negative) { // 异号相加退化成减法符号规则放到减法里处理 if (a.negative) return sub(b, BigInt(0) - a); // 简化写法下面会再补充 else return sub(a, BigInt(0) - b); } BigInt res; int n max(a.digits.size(), b.digits.size()) 1; res.digits.assign(n, 0); int carry 0; for (int i 0; i n; i) { int val carry; if (i (int)a.digits.size()) val a.digits[i]; if (i (int)b.digits.size()) val b.digits[i]; res.digits[i] val % 10; carry val / 10; } res.trim(); res.negative a.negative; // 同号符号不变 return res; }注意到这里把异号相加直接转成了减法。我在工程实现里一贯采用“加法只处理同号、减法只处理异号”的策略把符号判断放在最外层内部只做绝对值。这样函数职责清晰后续排查问题的时候不需要盯着符号规则猜半天。进位处理有个常用的“坑”循环次数如果设为 max(a.size(), b.size())那么两个位数相等的数相加时最高位的进位就会丢失比如 999 1。所以我是直接分配 max 1 的长度多出来那一位专门接进位。最后调 trim把不存在的最高位 0 去掉逻辑完美。2.2 减法比较、借位与负号处理减法比加法复杂的地方在于不够减时要借位而且要知道两个数谁更大。绝对值小于被减数要借位绝对值大于则结果需要反过来变成负号。在实现 sub_abs两个正数绝对值相减保证 a b时我从不提前判断长度而是像手算一样一个循环解决vectorint subAbs(const vectorint a, const vectorint b) { vectorint res(a.size(), 0); int borrow 0; for (int i 0; i (int)a.size(); i) { int cur a[i] - borrow; if (i (int)b.size()) cur - b[i]; if (cur 0) { cur 10; borrow 1; } else { borrow 0; } res[i] cur; } return res; }这个版本的巧妙之处在于循环结束时如果 borrow 仍为 1说明 a 其实小于 b但这在调用前已经被外部比较拦截掉所以这里不会发生。借位的本质是当前位不够减就向高位借一个“10”同时把借位标记传给下一位。高位的借位会继续向更高位传播整个过程和手写减法一致。外部完整减法这样做BigInt sub(const BigInt a, const BigInt b) { // 考虑符号统一成绝对值减法 if (a.negative ! b.negative) { // 异号负减正、正减负都变成加法 BigInt res add(a, b); return res; } int cmp absCompare(a, b); if (cmp 0) { BigInt res subAbs(b.digits, a.digits); res.negative !a.negative; return res; } BigInt res; res.digits subAbs(a.digits, b.digits); res.negative a.negative cmp ! 0; return res; }负号规则里最容易被忽略的一点两个负数相减比如 -5 - (-8)其实是 “-5 8”结果是正数。不少人在这一步直接抄惯例“负负得正”结果把 -5 8 算成了 -3。正确的思路永远是根据绝对值和符号统一而不是死记口诀。比较函数必须写对这是减法正确性的前提。我推荐先比长度长度相同再从高位往低位比。因为 digits 是低位在前高位在尾部所以要反向遍历int absCompare(const BigInt a, const BigInt b) { if (a.digits.size() ! b.digits.size()) return a.digits.size() b.digits.size() ? -1 : 1; for (int i (int)a.digits.size() - 1; i 0; --i) { if (a.digits[i] ! b.digits[i]) return a.digits[i] b.digits[i] ? -1 : 1; } return 0; }2.3 用调试经验检验加减法加减法写完后我建议立刻跑一批边界用例不要上来直接测大数。我最常用的 a、b 组合是这些999 1验证最高位进位。1000 - 1验证连续借位。1 - 1000验证结果为负且绝对值正确。0 - 0验证不会产生负号和空数组。123456789123456789 987654321987654321验证多轮进位。我自己踩过最惨的坑是在减法中忘记处理“结果恰好为 0”时的符号。比如 5 (-5) 通过加法转减法得到绝对值 0但如果 negative 被随手置为 true后续排名、排序、输出都会异常。所以我在 sub 中专门加了 cmp ! 0 这个条件结果为 0 时强制不带符号这个细节一定不能省。3. 乘法竖式算法的复杂度以及“压位”这个关键优化3.1 朴素竖式乘法直观但要知道复杂度乘法的原理就是小学学的竖式两层循环让被乘数的第 i 位和乘数的第 j 位相乘结果累加到第 ij 位。如果 a 的位数是 nb 的位数是 m朴素算法时间复杂度是 O(n*m)空间 O(nm)。对于几百位的数这个复杂度完全够用几千位就开始明显变慢。BigInt mul(const BigInt a, const BigInt b) { vectorint res(a.digits.size() b.digits.size(), 0); for (int i 0; i (int)a.digits.size(); i) { int carry 0; for (int j 0; j (int)b.digits.size(); j) { int cur res[i j] a.digits[i] * b.digits[j] carry; res[i j] cur % 10; carry cur / 10; } int pos i (int)b.digits.size(); while (carry) { int cur res[pos] carry; res[pos] cur % 10; carry cur / 10; pos; } } BigInt result; result.digits move(res); result.trim(); result.negative a.negative ! b.negative; return result; }这个版本的内层循环用 carry 记录同一 i 行产生的进位并把进位继续累加到后续更高位。外层循环结束后还要把余下的 carry 喂进去。看起来稍有点绕但比每层都直接修改 res 更稳不会出现 res[ij] 超过一位的情况。乘法符号规则比加减法简单负号个数为奇数则结果为负偶数结果为正。C 的 bool 直接异或即可。3.2 压位优化为什么万进制能快这么多一位一格十进制虽然好懂但性能堪忧。以 1000 位乘 1000 位为例朴素算法需要 100 万次乘法每次边乘边处理 10 以内的小整数循环开销非常大。如果每个 vector 元素存 4 位十进制数数组长度缩到 250运算量变成 6 万多带来的提速不只是 4 倍而是接近一个数量级。这里的关键是选择“万进制”而不是“十万进制”。因为两个万进制数字相乘的最大值是 9999 * 9999 99980001在 32 位 int 里完全放得下。如果用十万一格99999 * 99999 ≈ 1e10就超过 int 上限必须改用 long long反而增加内存和类型转换开销。万进制是一个计算精度和性能非常平衡的选择。我推荐用 BASE 10000数组每个元素存储 0 到 9999 之间的数。需要说明的两点数组低位在前输出时除最高位外其余元素必须补 0 到 4 位否则 1234 1 看起来会变成 “12340” 这种错位效果。进位时每满 10000 就向更高位进 1。乘法循环里的模和除法都基于 BASE。const int BASE 10000; BigInt mulBase(const BigInt a, const BigInt b) { vectorint res(a.digits.size() b.digits.size(), 0); for (int i 0; i (int)a.digits.size(); i) { int carry 0; for (int j 0; j (int)b.digits.size(); j) { long long cur res[i j] (long long)a.digits[i] * b.digits[j] carry; res[i j] cur % BASE; carry cur / BASE; } int pos i (int)b.digits.size(); while (carry) { int cur res[pos] carry; res[pos] cur % BASE; carry cur / BASE; pos; } } BigInt result; result.digits move(res); result.trim(); result.negative a.negative ! b.negative; return result; }输出时单独写一个函数防止补零逻辑污染核心运算string toString(const BigInt x) { string s; if (x.negative) s -; s to_string((int)x.digits.back()); for (int i (int)x.digits.size() - 2; i 0; --i) { string part to_string(x.digits[i]); while ((int)part.size() 4) part 0 part; s part; } return s; }压位虽然增加了补零代码但对乘法的收益极大。很多框架里的“亿进制”也是类似思想只是把 4 位换成 8 位同时所有计算改用 long long 兜底。这完全没问题只是我的习惯是从 BASE10000 入手先保证逻辑不出错再谈更快。3.3 从 O(n*m) 到 O(nlogn)什么时候考虑 FFT再往后说一句进阶内容当两个数字各有几万位时竖式乘法的平方复杂度会让人失去耐心。此时可以用 FFT快速傅里叶变换把多项式乘法转成点值乘法把复杂度降到 O(n log n)。具体实现里往往还要用数论变换避免浮点误差。但我一直不建议普通项目一上来就上 FFT。FFT 的常数非常大代码量也大位数没有超过几千时它甚至不如优化的压位竖式快。我的判断标准很简单如果只是几千位以内的计算优化后的竖式已经很好如果要做几万位以上的大数科学计算直接换 Julia 的 BigInt 或专门的大数库比如 GMP可能比手搓 FFT 更划算。这涉及到工具选型而不是算法炫技。4. 除法最容易写崩的环节二分法与试商的取舍4.1 高精度除以低精度逐位取商除以一个普通 int 或 long long 的场景非常常见比如把金额除以份数、把秒数换算成天。写法是从最高位到最低位逐位处理每一步把之前的余数和当前位拼成新数再整除除数。BigInt divShort(const BigInt a, long long divisor, long long rem) { vectorint quotient(a.digits.size(), 0); long long cur 0; for (int i (int)a.digits.size() - 1; i 0; --i) { cur cur * 10 a.digits[i]; quotient[i] cur / divisor; cur % divisor; } BigInt q; q.digits move(quotient); q.trim(); q.negative a.negative ! (divisor 0); rem cur; if (a.negative) rem -rem; // 余数符号跟随被除数 return q; }这里有一个很容易被忽视的溢出问题cur 是前一轮的余数理论上 cur divisor但是如果 divisor 很大接近 long long 上限cur * 10 会直接溢出。最稳妥的办法是压缩位数为 4 位让每一步的 cur 都是一个较小的数或者用 __int128 把 cur * 10 的中间结果撑大。我最早写模板时漏掉这个点结果在某次金额分摊计算里出现了明显异常排查了很久才发现是乘法溢出而不是算法错误。4.2 高精度除以高精度二分答案的简单写法除数本身也超大时就不能用上面那种逐位除法了但可以先讲一个最容易理解、也最不容易出错的方案二分答案。因为除法的商 q 满足 q * b a (q 1) * b而且 q 的位数不会超过 a 的位数。于是可以令下界 l 0上界 r a在区间里二分查找最大的 q。每次取中点 mid算一下 mid * b 和 a 的关系如果乘出来小于等于 a说明 mid 可以作为答案继续右移否则左移。BigInt divBigByBinary(const BigInt a, const BigInt b, BigInt remainder) { BigInt low(0), high(a), ans(0); // 需要实现 high 和 low 的中间值可以用 divShort(mid, 2) while (absCompare(low, high) 0) { BigInt mid divShort(add(low, high), 2, remainder); BigInt product mul(mid, b); if (absCompare(product, a) 0) { ans mid; low add(mid, BigInt(1)); } else { high sub(mid, BigInt(1)); } } remainder sub(a, mul(ans, b)); return ans; }二分法在正确性上几乎没有坑但它慢每一次判断都要做一次高精度乘法而二分需要迭代约位数乘以 log 10 的次数。比如处理一万位数时这个方案的耗时完全不可接受所以我只在考试题和快速验证里用它。真正生产级的高精度库用的是试商法。4.3 试商法按位逼近的原理试商法的思路和手写除法完全一致从被除数的最高位开始每次取一位下来拼在“当前余数”后面然后尝试这一位最大能上几。假设当前余数是 rem被除数的下一位是 d那么拼出来的临时值是 cur rem * BASE d。接着从 9 到 1 逐个尝试 q找到满足 q * b cur 的最大 q然后让 rem cur - q * b。这个 q 就是商在这一位上的数字写入结果。注意这里每一步的 q 一定是个一位数所以测试代价很小。用 BASE10 的示意代码如下BigInt divBigByTrial(const BigInt a, const BigInt b, BigInt remainder) { BigInt rem(0); vectorint quotient; for (int i (int)a.digits.size() - 1; i 0; --i) { rem addOrAppendDigit(rem, a.digits[i]); // rem rem * 10 d int qDigit 0; for (int k 9; k 1; --k) { BigInt tmp mul(b, BigInt(k)); if (absCompare(tmp, rem) 0) { qDigit k; break; } } quotient.push_back(qDigit); if (qDigit ! 0) { BigInt tmp mul(b, BigInt(qDigit)); rem subAbs(rem.digits, tmp.digits); // 正常符号下 rem tmp } } reverse(quotient.begin(), quotient.end()); // 因为循环从高位到低位所以要反转成低位在前 BigInt q; q.digits quotient; q.trim(); remainder rem; return q; }这句“从 9 到 1 逐个尝试”看起来笨实际却很高效因为最多试 9 次。如果商的每一位都需要大范围查找才需要考虑更高级的估商法。不过对绝大多数应用来说试商法已经足够。关于符号商的符号遵循“同号得正异号得负”余数的符号和被除数保持一致。这个规则实现时别硬背关键是先完成绝对值除法最后再套一层符号。我自己在这里翻过车有个功能需要算“负数的绝对值余数”我直接把余数取负然后发现后续比较逻辑又写了一套负数分支越搞越复杂。后来统一改为内部全部用绝对值计算只在最外层处理符号代码瞬间清爽很多。4.4 求余数和判断整除的坑求余数有两种常见做法一是直接实现 mod 函数循环里不断乘 BASE 加 digit 再取模效率很高二是把除法完整算完再用 a - q*b 得到余数。后者简单但会多做一遍乘法和减法性能差一些。在工程里我更建议写一个专门的 mod 函数因为很多业务只需要余数不需要商。例如校验和、循环冗余、键盘分发这类场景浪费一次高精度除法有点没必要。实现如下long long modShort(const BigInt a, long long m) { long long r 0; for (int i (int)a.digits.size() - 1; i 0; --i) { r (r * 10 a.digits[i]) % m; } return r; }这个函数同样存在 r * 10 溢出问题所以 m 较大时建议内部用 __int128或者把 a 转成压位数组再处理。判断整除最稳的方式是求余后判断余数是否为 0不要用“两个数相除结果是否整数”这种浮点判断。另一个细节是除数为 0。任何除法函数的第一步都应该判断 b 是否为 0一旦为 0 就抛出异常而不是继续算否则会出现除以零后得到一个乱七八糟的大数非常难排查。早期我写模板时觉得调用方肯定不会传 0后来某次上游配置错乱传进来一个默认值 0我整整查了两天才定位到是除数为 0 触发了未定义行为。5. 从 C 手写实现到 Julia 的 BigInt 和 BigFloat哪些坑可以不必重复踩5.1 Julia 高精度体验如果你不想自己写大数模板只想快速得到正确结果Julia 是一门高精度支持非常流畅的语言。它原生支持 BigInt 和 BigFloat加减乘除直接重载运算符不需要任何额外配置。举个例子a big123456789123456789123456789 b BigInt(10)^50 3 println(a b) println(a * b) println(divrem(a*b, b1))只需要在字符串字面量前加上 big或者显式调用 BigInt系统就会把后续运算提升到大整数。BigFloat 的处理也很方便设置精度之后再做除法可以看到比 double 稳定得多的结果setprecision(80) do x BigFloat(1) / BigFloat(3) println(x) end这里的 setprecision 是 Julia 里控制 BigFloat 有效位数的标准做法。我个人体验是Julia 的大数实现非常省心特别适合做科学计算、数学建模、快速验证你不用关心进位借位这些底层事。5.2 什么时候用现成大数什么时候必须自己写很多读者可能会问既然 Julia 甚至 Python、Java 都自带高精度为什么还要把 C 手写模板研究得这么深我的看法分两层。第一层如果你是做业务系统、做数据分析完全没必要重复造轮子。Python 的 int 原生支持任意精度Java 有 BigDecimalJulia 有 BigInt现成方案足够稳定开发效率高。第二层如果你是在做算法竞赛、嵌入式设备、编译器、密码学、图形图像底层库或者对性能和可控性有硬性要求自己写一套模板能让你在关键路径上完全掌控每一步运算也方便针对特定场景做压位、做常数优化、做内存复用。我自己在嵌入式设备上写过一个传感器融合的小模块芯片没有原生的任意精度库也不能跑 GMP只能用定点整数模拟高精度。那时我才意识到手写高精度加减乘除不只是应付面试更是一种底层思维训练。你理解了进位、借位、补零、符号这些细节遇到任何“数字比类型大”的问题都能下意识判断该用哪种表示法。Julia 和 C 的对比是这样的Julia 适合把复杂数学验证跑通、把算法原型做出来C 适合把原型变成稳定高效的线上代码。很多项目里我习惯先用 Julia 验证一套公式再照着它的行为在 C 里手写一遍两边结果对齐准确性就有保证了。这个过程比纯看文档效率高得多。6. 从高精度整数到小数和定点数测量和定位场景的真实落地6.1 高精度浮点数不一定适合所有小数问题聊完了整数很多业务场景其实需要的是小数比如金额 19.99、ADC 采样电压 3.3V、电流霍尔传感器输出的一个带小数标定值。这时候有两种路线。第一种是使用 BigFloat 这样的二进制高精度浮点。它能表示很大的范围也能支持任意精度但它本质还是二进制存储10 进制小数在二进制里经常是无限循环小数打印时仍然可能看到意料之外的尾数。比如 BigFloat(1) / BigFloat(10) 内部并不是精确的 0.1只是精度足够高时误差会被压到非常小。第二种是干脆用整数扩倍表示也就是定点数。以分为单位后金额 19.99 直接存成 1999以毫伏为单位后 3.3V 存成 3300。所有的加减乘除都走整数逻辑最后输出的时候统一除以扩倍系数。这种做法的好处是完美避开二进制浮点误差单位放大后还大幅提升了整数类型的利用率。高精度定位里很多坐标存储也是这个思路经纬度放大到 1e7 或 1e9 存成整数既减少了坐标漂移也能让加减乘除的结果完全可预测。6.2 定点数在高精度测量中的实际用法以霍尔电流传感器为例一个常见的数据链路是传感器输出模拟电压ADC 采样成数字值然后乘一个比例系数换算成实际电流。如果这个比例系数是 1.23而 ADC 原始值是 4567硬用 double 乘也不算错但经过多次校准、滤波、温度补偿后微小误差会被不断放大。我的做法是在固件里把所有系数放大一千倍、一万倍后存入整数表。计算时原始整数 4567系数 1.23 变成 12300乘积 4567 * 12300 56174100最后除以 10000得到 5617对应 5.617A整个过程没有任何浮点参与数值每一步都是确定的整数。如果中间还有加减法比如减去零点偏移同样全部用放大后的整数做。这个思路本质上是把高精度小数问题降级成高精度整数问题所以前面讲的所有大数模板可以直接复用。我也建议一轮运算里不要来回切换倍率。统一倍率是定点数最容易犯的错误有人一边用 1000 放大、一边用 1e7 放大中间又没有把单位换算写清楚结果数据莫名其妙大出几个数量级。最安全的做法是定义成常量比如const long long SCALE 1000;所有常数都用同一个 SCALE 乘进去需要换算时再单独写函数。6.3 一套高精度小工具的推荐设计如果让我重新设计一套高精度小工具不会一上来就追求 FFT 和十万位数性能而是会先保证正确性和可维护性。推荐按四层来搭第一层是基础存储层直接封装 vector 和符号提供初始化、trim、比较函数。第二层是算术运算层先实现 add、subAbs、mul、divShort 这些基础操作再加 divBigByTrial 做通用除法。第三层是输出层统一做十进制字符串输出和补零逻辑。第四层是应用层针对定点数、金额、坐标这类场景封装扩倍系数。每一层之间不要有循环依赖比如输出层不应该反过来调用乘法逻辑。我最开始的模板全部写在同一个文件里后来加了定点数封装才发现拆开的好处底层算法跑不跑得对可以直接用 toString 快速检查业务逻辑完全不用卷入位运算细节。调试方面我的习惯是对拍。用 Python 的任意精度整数作为标准答案随机生成几百组大数算完 C 结果后输出字符串比对。这个流程每次都能帮我抓到一些平时想不到的边界问题比如负数减负数、余数符号、补零长度。与其靠人肉看样例不如交给随机数据和现成标准库去碾压。高精度加减乘除这四件事做齐不难做对也不难难的是在性能预算下持续保持正确。把符号、进位、借位、补零、比较这些细节一个个钉死后面任何数量级和精度的需求都会变得有章可循。
返回列表