尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
高精度减法全解析:从long溢出到C++大数减法实现
前阵子带新人重构一个内部结算模块业务方输入了两个20多位的订单编号模块直接用 long 相减一上线就出现了一堆负数。排查了很久最后发现问题不是业务逻辑而是类型上限——Java 的 long 最大值是 9223372036854775807超过 19 位就装不下了。这种场景光换类型不行必须上高精度减法。高精度减法简单讲就是让计算机像小学生列竖式那样把任意长度的整数拆成一个个数字存进数组逐位相减、处理借位最后拼回字符串。它是整个大数计算体系的地基后面做高精度加法、乘法、除法、取模都是在这套思路上扩展。这篇文章会从类型溢出的真实场景讲起把高精度减法的存储设计、借位原理、负数处理讲透再给出完整可运行的 C 实现最后分享我踩过的一些坑。适合刚接触大数运算的竞赛选手也适合工作中突然需要处理超长数值的后端开发。1. 高精度减法到底是什么为什么非学不可1.1 一次实战翻车Long类型爆掉之后先说那次翻车。结算模块里有个功能要对两张工单做差额校验订单编号是 22 位数字。当时写代码的人用了 long long本地测试一切正常但生产环境里有一批真实订单编号超过 19 位结果所有相减结果都是乱的而且很多变成了负数。我印象特别深排查时把数据打印出来看数字被截断成了一段奇怪的尾巴跟原始输入完全对不上。后来我把这 22 位编号存成字符串再对比计算逻辑结果完全正确。这件事给我两个教训第一看到大数不要条件反射用 int/long先数一下位数第二在无法确定输入范围时用字符串存储是最稳妥的方案。很多系统里所谓的“大数”其实不是数学意义上的大而是超出了编程语言原生整数类型的表达范围。原生整数类型的上限是硬约束。Java 的 long 是 64 位有符号最大 9223372036854775807C 的 unsigned long long 虽然更大上限也只有 18446744073709551615。一旦数字超过这个范围就只能靠自己的代码来处理。竞赛里常见的“两个上万位整数相乘”这类题目本质就是绕开类型限制用数组和循环把每一位的运算重新实现一遍。1.2 高精度减法的核心思路模拟人脑竖式高精度减法的核心思路就是把小学学过的竖式减法搬到程序里。人脑算 999 - 123 的时候会先把两个数右对齐个位对个位、十位对十位然后从个位开始逐位相减不够减就向高位借 1 当 10。程序里做的事一模一样差别只是纸笔换成了数组手写借位换成了 borrow 标志。具体分三步。第一步把输入的大数字符串拆成一位一位的数字存进数组第二步保证被减数不小于减数如果被减数更小就交换两者并记下符号为负第三步从最低位开始逐位相减用 borrow 变量记录借位情况。只要这三步想清楚代码怎么写都不会跑偏。这套思路最妙的地方在于复杂度很低。两个 n 位数相减时间复杂度是 O(n)空间复杂度也是 O(n)只需要扫描每一位一次不需要任何乘除法操作。所以即使数字有上万位只要内存放得下计算也很快。这也是为什么很多底层大数库在处理减法时能做到接近内存带宽级别的性能。1.3 高精度减法能解决什么实际问题高精度减法不是竞赛选手的专属玩具在真实世界里有相当多的应用场景。最直接的是编程竞赛。在很多算法题里大数运算是前置工具比如高精度乘法需要反复调用高精度加法高精度除法需要反复调用高精度比较和减法。虽然 LeetCode 上没有专门的高精度减法题但 415 字符串相加和 43 字符串相乘都是同一套思路理解了减法能帮你更轻松地举一反三。金融系统的精确计算也是大头。浮点数算钱会出大问题0.1 0.2 在 IEEE 754 标准下得到 0.30000000000000004这在银行是不能接受的。所以 Java 的 BigDecimal、Python 的 Decimal、数据库的 DECIMAL 类型底层都在用类似高精度运算的机制保证每一位的精确性。你平时调用的 BigDecimal.subtract内部就是一套大整数减法只是封装得比较隐蔽而已。密码学和区块链领域大整数更是无处不在。RSA 加密的密钥是几百位的整数区块链交易金额的最小单位在很多链上是 10 的 18 次方普通 int 根本不够用。这些系统做加、减、乘、除、模运算时底层全是高精度算法。理解高精度减法的原理再去看那些底层库的源码会轻松很多。2. 高精度减法的核心设计与存储方案2.1 输入输出形态为什么用字符串而不是整数输入为什么用字符串而不是直接读整数答案很简单真正需要高精度减法的场景是因为数字已经超过了原生整数类型的上限。这时候如果直接用 long 读入数字在读取瞬间就被截断了后面的计算全是错的。所以高精度运算的输入输出几乎都是字符串形态最多再加一个符号位。有些场景也会用字符数组或字节数组读入但本质上都一样把大数的每一位以字符或字节的形式保存下来在内存里重新组织成数字数组。这里有个容易被忽略的小细节——输入本身可能带前导零比如 000123 和 123 在逻辑上是相等的但如果不处理比较大小的时候会出问题。所以读入之后的第一步永远是去除前导零这个我在后面会专门展开讲。输出形态上也有一致性要求。既然输入是字符串输出也应该拼成字符串。减法做完之后结果可能带负号也可能因为恰好相等而变成 0或者因为两个数的位数差距较大而变成一个短得多的数。这些情况都要求你在拼接字符串时小心处理不能出现 0000 或者空串。2.2 数组方向的选择低位在前还是高位在前存储大数的数组有两个方向可以选择低位在前还是高位在前。高位在前很直观123 存成 [1, 2, 3]0 号位是最高位低位在前则反过来存成 [3, 2, 1]0 号位是个位。两种方式各有优劣。高位在前方便人理解打印调试的时候直接看数组就是原数字。但做加减法时进位和借位是从低位往高位走的如果数组高位在前处理完低位进位后可能在数组头部插入新元素这是 O(n) 的操作非常浪费性能。所以大多数实现库都选低位在前让数组下标天然对应从个位开始的位数。实际写代码时常见的做法是先用 string 存原始输入然后倒序遍历字符串把每个字符转成 int存进 vector 这样 0 号位就是最低位。后续做减法、加法、乘法都能顺着下标往高位走非常顺手。我第一次写高精度时没注意这一点用高位在前的数组从头处理结果每次进位都要整体后移一位代码又长又容易错。后来改成低位在前整个实现简洁了一大截。2.3 借位机制的完整推导借位是高精度减法的灵魂值得单独拿出来讲。竖式减法中如果当前位被减数小于减数就要向高位借 1在高位减 1当前位加 10。程序里把这个过程拆成两个变量diff 和 borrow。逐位处理的伪代码是这样先取出被减数的当前位 x减数的当前位 y如果减数已经用完y 就取 0然后计算 x - y - borrow。如果结果小于 0说明需要借位就把 diff 加 10同时把 borrow 置 1否则 borrow 置 0。这里有个关键点borrow 在每一轮循环里都要被重新赋值不能留着上一次的借位状态否则会叠加出一堆错位。为什么借位是加 10 而不是加别的因为每一位是十进制的一位从高位借来的 1 在当前位就是 10。如果你将来做二进制的大数减法借位就是加 2做十六进制就是加 16。理解了这一层你可以随意换成任意进制代码只需要改动一个常量。这个推导过程想明白之后高精度减法就没有任何悬念了。2.4 负数与结果符号的处理策略整数的减法天然涉及负数这是高精度减法和高精度加法最大的不同。最简单也最稳的策略就是先比较两个数绝对值的大小。如果被减数大于等于减数直接逐位相减如果被减数小于减数就交换两个数让大数减小数最后在结果前面加负号。这个策略的好处是整个逐位相减的逻辑永远只处理“大数减小数”这种情况不会出现借位借到最高位还不够的尴尬。代码实现的时候需要先写一个比较函数。比较前一定要先去除前导零然后先比位数位数相同再比字典序。很多新手会直接拿字符串运算符比大小比如 9 10 在字典序下是成立的但数值上 9 小于 10这就是经典错误。符号处理还有一个细节如果两个数相等结果应该是 0而不是 -0。这意味着即使你交换了两个数并标记了 negative在最终拼接结果时如果去掉前导零后结果是 0也要把负号去掉。这个分支不处理就会输出一个诡异的 -0在某些严格校验的系统里会直接报错。3. 完整代码实现与逐行拆解3.1 一个可以直接用的C实现直接上完整代码我加了必要的注释你复制到本地编译器就能跑。#include iostream #include string using namespace std; // 去除字符串前导零保证至少保留一位 string trimLeadingZeros(string s) { int pos 0; while (pos s.length() - 1 s[pos] 0) pos; return s.substr(pos); } // 比较两个非负大数字符串返回 a b bool compareBig(string a, string b) { a trimLeadingZeros(a); b trimLeadingZeros(b); if (a.length() ! b.length()) return a.length() b.length(); return a b; } // 高精度减法a 和 b 都是非负整数字符串 string bigSub(string a, string b) { a trimLeadingZeros(a); b trimLeadingZeros(b); bool negative false; if (!compareBig(a, b)) { swap(a, b); negative true; } string result; int borrow 0; int i a.length() - 1; int j b.length() - 1; while (i 0 || j 0) { int digitA (i 0) ? a[i] - 0 : 0; int digitB (j 0) ? b[j] - 0 : 0; int diff digitA - digitB - borrow; if (diff 0) { diff 10; borrow 1; } else { borrow 0; } result.push_back(0 diff); i--; j--; } // 此时 result 是低位在前的需要反转 string ans(result.rbegin(), result.rend()); ans trimLeadingZeros(ans); if (negative) ans - ans; return ans; } int main() { string a, b; cin a b; cout bigSub(a, b) endl; return 0; }这段代码你直接跑一下输入两个大数就能得到正确结果。我在很多场景下都用过这个模板只要输入不超过内存限制位数再多也没问题。3.2 辅助函数去除前导零与比较大小的设计trimLeadingZeros 这个函数我单独说一下。它做的事情很简单找到第一个非零字符的位置然后截取子串。特殊处理是如果字符串只有一个字符即使它是 0也不处理直接返回。这样能保证 0 不会被删成空串这是很容易忽略的边界。compareBig 的逻辑是先分别去掉前导零然后比较长度。长度长的数值一定大这个结论在非负整数里永远成立。长度相同的时候再用字符串的字典序比较因为此时位数一致字典序就等于数值大小。这个顺序不能反先比长度再比字典序缺一不可。如果你只比字典序遇到 9 和 10 就会出错如果你只比长度遇到相同位数的 123 和 321 也会出错。这两个辅助函数在高精度运算里几乎是通用的。后面做高精度加法时也需要去除前导零和比较大小做高精度除法时比较大小更是核心操作。把它们单独抽出来是为了让主流程更清晰也方便复用。3.3 其他语言的实现差异Python 内置了大整数理论上不需要手写。但了解这套实现仍然有价值。Python 的 int 不限制位数底层运算时其实就是类似的分段处理只是被封装掉了。如果你在代码里要强调“精确到每一位”的语义也可以自己实现这套算法把处理逻辑完整掌控在手里。Java 可以直接用 BigInteger 做减法用法是 a.subtract(b)。但很多面试和竞赛场景不允许用现成类而且 BigInteger 的性能比手写的数组版本要重一些。了解原理之后你才能在性能和代码复杂度之间做更好的权衡。Java 的 BigInteger 源码也值得一读它内部的 int[] mag 就是低位在前的实现跟我在上面写的思想是一样的。我见过不少开发者把 Python 的大整数当成“无限制”其实它也有底层分配和性能问题。大整数运算超过一定规模后Python 的性能下降很快而 C 的手写数组实现配合压位优化可以在同样数据量下快一个数量级。这也是为什么像 GMP 这种 C 语言大数库至今仍在密码学、科学计算里占据统治地位。3.4 边界用例与测试结果我在本地跑了四组典型用例覆盖了主要分支结果贴在这里。输入 a输入 b期望输出实测输出1000199999911000-999-999000010000000000000000000001999999999999999999999999999999999999999999第三组最容易翻车因为两个都是 0不处理前导零和正负号很容易输出空串或者 -0。第四组则是验证连续借位从最低位开始会一路借到最高位borrow 标志必须保持为 1 直到最高位处理完。这两组用例跑通了说明核心逻辑基本没问题。除了直接相减我还建议测一下带前导零的输入比如 0001000 减 1。这个用例能检验 compareBig 是否先去掉了前导零如果没去0001000 和 1 会被错误地比较导致符号判断错误。我在开发时习惯把这五组用例都写进单元测试每次改代码后跑一遍防止回归。4. 常见问题与实战排查技巧4.1 前导零处理是两头都要做的细节前导零的坑我踩过不止一次。读入阶段的 000123 和输出阶段的 00001 是两个不同方向的问题。读入时不处理compareBig 函数就会把 000123 和 123 当作不同的数导致比较结果错误输出时不处理结果会带一堆多余的 0。我见过有人只在读入阶段处理结果减完还是输出一堆 0也有人只在输出阶段处理结果输入一旦带前导零比较大小就出错。正确的做法是两头都要处理。输出阶段的前导零是减法特有的问题。当两个大数非常接近时结果可能只有很少的几位有效数字但数组高位留了很多 0。比如 10000 减 9999正确结果是 1中间过程会得到 00001不处理就会输出 00001。在一些严格校验的系统里这种值会被当作非法数据。所以我在 trimLeadingZeros 里特意保留至少一位就是为了避免结果为 0 时输出空串。调试前导零问题有个笨办法但很有效在关键地方打印中间变量。比如 bigSub 每轮循环后打印当前的 result以及反转后的完整字符串。这样你能快速定位是读入阶段没处理还是输出阶段没处理。经验之谈90% 的前导零 bug 都是因为只在一端做了处理。4.2 借位标志复位问题与隐蔽bug还原borrow 必须每轮循环都重新赋值这句话我用一个反例说明。假设代码里写的是 diff 小于 0 时 borrow 置 1但没有 else 分支把 borrow 清零相当于 borrow 一旦为 1 就永久保留。用这个错误逻辑算 1000 - 999个位0 - 9 - 0 -9diff 1borrow 1十位0 - 9 - 1 -10diff 0borrow 1百位0 - 9 - 1 -10diff 0borrow 1千位1 - 0 - 1 0最终结果是 0000去掉前导零变成 0但正确结果应该是 1。这个 bug 非常隐蔽因为在很多普通用例上它碰巧是对的只有在连续借位跨越多个数位时才出错。排查技巧很简单构造连续借位的用例比如 1000 - 1、100000 - 1这些用例会强制 borrow 从低位一路传到最高位。如果结果比预期小一点或者变成 0基本就是 borrow 没有正确复位。更稳妥的做法是在循环体开头打印每一轮的 x、y、borrow、diff逐行对拍很快就能看出问题。4.3 性能优化压位到底怎么压基础版本每一位只存一个数字空间和时间都不算最优。一个简单有效的优化是压位用一个 int 同时存多位十进制数字。比如压 4 位数字 123456789 会存成 [6789, 2345, 1]数组下标 0 对应最低的 4 位。相减时借位不再是加 10而是加 10000因为一组代表 4 位十进制数。举一个真实例子。123456789 - 99999999正确结果是 23456790。压 4 位后被减数是 [6789, 2345, 1]减数是 [9999, 9999, 0]。逐组相减组06789 - 9999 -3210借位加 10000得 6789 10000 - 9999 6790borrow 1组12345 - 9999 - 1 -7655借位加 10000得 2345borrow 1组21 - 0 - 1 0拼接时组2是最高位为 0 省略组1是 2345不需要补零因为它是最高有效组组0是 6790需要补到 4 位结果就是 23456790完全正确。压位能显著减少循环次数。10000 位的数字不压位要循环 10000 次压 4 位只需要 2500 次性能提升非常明显。竞赛中很多人压 4 位或 9 位。但要注意压 9 位时两个 9 位数字相减不会超过 int 范围最大差值的绝对值也不超过 999999999所以不会溢出。如果将来扩展到压位乘法中间结果可能超过 int 上限就需要用 long long 保存中间量。4.4 常见问题速查表我把这些年遇到的高精度减法问题整理成一张速查表方便你排查时直接对照。症状根因快速排查方法输出开头有多余的0输出阶段没去除前导零检查 trimLeadingZeros 是否在结果反转后调用结果一直是负数比较大小逻辑错误单独测试 compareBig 函数结果比预期小1借位标志未正确复位用 1000 - 999 连续借位用例测试0减0输出空串或-0前导零处理过度或负号未判断确保 trim 至少保留一位并在结果非负时去掉负号输入带前导零时结果不对比较前未去除前导零在 compareBig 开头调用 trimLeadingZeros相差一位的结果变0借位跨越多位时 borrow 未保留打印每轮 borrow逐位对拍这张表我在团队内部用过很多次新人写完高精度减法后直接对照表里的症状自查比自己瞎猜快得多。4.5 手
RELATED

相关推荐

Telegraf 指标采集快速上手:5 分钟跑通第一条监控数据流

Telegraf 指标采集快速上手:5 分钟跑通第一条监控数据流

Telegraf 指标采集快速上手:5 分钟跑通第一条监控数据流 【免费下载链接】telegraf Agent for collecting, processing, aggregating, and writing metrics, logs, and other arbitrary data. 项目地址: https://gitcode.com/GitHub_Trending/te/telegraf 凌…

📅 2026/9/9 18:52:49
深入解析BACnet WritePropertyService:报文、优先级与调试

深入解析BACnet WritePropertyService:报文、优先级与调试

干楼宇自控和BACnet集成这一行的,几乎每天都在和各种服务打交道。要说最常调用的服务,WritePropertyService绝对排在前三——它是设备控制、参数下发、场景切换这些操作的底层通道。简单说,你要远程改一个模拟量输出、把某个风机启停位翻转一…

📅 2026/9/9 18:52:49
AI编程搭子实战:用OpenClaw开发Android五子棋全流程

AI编程搭子实战:用OpenClaw开发Android五子棋全流程

最近在整理手头的AI辅助开发流程,正好朋友让我用OpenClaw做一个五子棋练手项目。花了一个周末把简易版跑通,整体体验还挺顺。这篇就记录一下整个过程中我是怎么拆需求、怎么写代码、怎么让OpenClaw给我当编程搭子的,给同样想用AI工具做Androi…

📅 2026/9/9 18:47:49
MORE NEWS

更多资讯

📰

SpringBoot宠物店管理系统实战:从数据库设计到部署答辩全攻略

每年到这个时间点,我总能在后台看到一堆类似的留言:“博主,有没有SpringBoot的管理系统源码?”“宠物店管理系统能不能出一期?”“毕设选题选了宠物店,但代码跑不起来怎么办?”其实这类基于Spri…

📰

Python机器学习筑基与实战:从环境搭建到完整项目

常有人问我:“Python机器学习到底该怎么学?”这大概是新手区被问得最多的问题,没有之一。我自己的感受是,市面上并不缺资料,缺的是能让人顺着主线走通的路径。很多人卡在“看完一堆教程,打开编辑器还是不知…

📰

AI不是互联网泡沫:从技术落地看真实价值

每隔一段时间,就会有一批人把“AI是互联网泡沫”这个话题翻出来炒一遍。尤其每次看到大模型公司又融了几十亿美金、估值高到离谱的新闻,评论区总有人说“这跟2000年的互联网泡沫有什么区别”。我做了十几年互联网产品,从移动互联网时代一直干…

📰

测试转AI训练师:数据质量与评测思维是关键跳板

近年来AI训练师这个岗位越来越热,各大招聘平台上挂出的需求量大,薪资也水涨船高。我身边不少做测试的朋友都动过心思,但又担心自己不是算法科班出身,投简历没底气,面试不知道聊什么。 我自己的经历是:做了…

📰

大模型直连PPTX:开源可编辑幻灯片生成工作流

1. 这不是PPT插件,而是一套“大模型直连PPT编辑器”的工作流你有没有试过让AI生成PPT?大多数人的体验是:先让ChatGPT写大纲,再复制粘贴到PowerPoint里手动排版;或者用某款在线工具生成一个PDF——好看但没法改字体、调…

📰

手把手教你汉化Docker Desktop v4.65:原理、步骤与避坑指南

Docker Desktop 的界面一直是英文这回事,从 v4.x 用到 v4.65 的老用户应该都习惯了。菜单栏里的 Containers、Images、Volumes 看了几年,靠肌肉记忆也能点对地方,但偶尔想找一个不常用的设置项,还是得停下来在英文菜单里翻半天。官…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬