有一次我在给学生讲大数加法,讲到一半突然意识到一个问题:我们天天用的int、long、double,其实从来没有告诉你“数字装不下了怎么办”。你写个2^100试试,long long直接给你一个莫名其妙的负数;你在Java里跑一句0.1+0.2,得出的也不是0.3而是0.30000000000000004。这些现象背后指向同一个话题——高精度加减乘除算法。
这篇文章我想把这块内容讲透。不管是算法竞赛里那些动辄几百位的大整数运算,还是业务系统里用BigDecimal做金额计算,或者是Python里处理输入输出时的精度陷阱,本质上都在跟“机器原生数字类型精度不够”这件事较劲。我会从底层原理讲起,把加减乘除四种运算的手写实现、工程实践、常见坑一次性说清楚,适合刚接触高精度算法的学生,也适合被金额计算精度问题折磨过的开发人员。
1. 高精度加减乘除,到底在解决什么问题
1.1 原生数字类型的精度边界
先看一组硬数据,这是所有编程语言共通的底层限制。
32位int能表示的最大整数是2147483647,也就是2^31-1;64位long long最大是9223372036854775807,也就是2^63-1。看起来挺大,但天文数字、组合数、大数幂运算随便一算就爆了。比如计算2^100,结果是1267650600228229401496703205376,这个数一共有31位,long long根本存不下,溢出后结果会变成一个无意义的负数。
浮点数更麻烦。double虽然能表示很大范围的数量级,但它只有53位二进制有效数字,折算成十进制大概是15到16位有效数字。也就是说,超过16位的整数部分,double根本不能保证精确表示。而且浮点数在设计上就是“近似值”,0.1在二进制里是一个无限循环小数,所以0.1+0.2会出现0.30000000000000004这种结果。
这就是高精度算法的核心价值:用数组或字符串存储数字的每一位,把四则运算拆成“手工竖式”的逐位计算,从而支持任意长度的数字精确运算。代价是速度比原生运算慢得多,但换来了精度上的上限突破。
1.2 哪些场景里躲不开高精度计算
我在实际工作中遇到的高精度场景,基本可以归成四类。
算法竞赛最典型。程序设计竞赛里经常出现n=1000的斐波那契数列、大数阶乘、大数幂运算,这些题的输出动不动就是几百位数字,不用高精度算法根本没法通过。除了C++选手要手写高精度,其实Java选手可以用BigInteger,Python选手直接依赖原生大整数,但原理是一样的,理解底层实现才能写出高效代码。
金融系统是最接地气的场景。任何涉及金额的计算都不能用浮点数,否则会出现分币差错。业务系统里一般会用BigDecimal或者decimal类型,加减乘除的精度和舍入规则必须严格把控,否则对不上账。
数据处理场景也很多。比如一个文件里有多行超长数字,需要做累加或者求平均值,因为数值太大可能超出数据库数值类型的范围,就需要在程序里用高精度方式处理。
还有一类是底层实现,比如加密算法、大整数分解、科学计算里的超长小数,这些领域的核心库内部就是用高精度算法实现任意精度运算的,只不过对性能要求极高,通常会用压位甚至FFT加速。
1.3 高精度运算的通用思路
不管是加法、减法还是乘法、除法,底层套路完全一致:用字符串读入,每位转成整数存进数组,再模拟手工计算过程。
这里有一个非常重要的约定,我会在后面反复用到:数组下标从0开始存个位,下标1存十位,下标2存百位,也就是“低位在前”。这样设计的原因很简单,加法进位或乘法增加位数时,结果可能比原来更长,数字往高位扩展只需要在数组末尾追加元素,非常方便。
理解了存储方式,剩下的事情就是用代码复刻你在小学二年级学过的竖式。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高精度加法和减法:竖式思想的代码化
2.1 字符串到数组:先解决存储顺序
先写一个固定套路。读入一个大数,通常是字符串,然后转换成倒序的int数组。
以Python为例,最直观的转换方式是:
python复制s = input().strip()
a = [int(ch) for ch in s[::-1]]
C++也类似:
cpp复制string s;
cin >> s;
vector<int> a(s.size());
for (int i = 0; i < a.size(); i++) {
a[i] = s[a.size() - 1 - i] - '0';
}
这里需要注意一个细节:读入的字符串如果前面有前导零,比如输入“000123”,转换后数组里就会有连续几个零。虽然在运算结果上没有问题,但会影响长度判断。我一般会在读入后先做一次清洗,去掉前导零,后面会少很多麻烦。
2.2 高精度加法实现与进位处理
两个大数相加,核心逻辑就是逐位相加并处理进位。模拟一下竖式:从个位开始,对应位相加,加上上一位的进位,如果结果大于等于10,就保留个位数并产生进位1。
C++实现大致是这样:
cpp复制vector<int> add(vector<int>& a, vector<int>& b) {
vector<int> c;
int carry = 0;
int len = max(a.size(), b.size());
for (int i = 0; i < len; i++) {
int sum = carry;
if (i < a.size()) sum += a[i];
if (i < b.size()) sum += b[i];
c.push_back(sum % 10);
carry = sum / 10;
}
if (carry) c.push_back(carry);
return c;
}
两个容易忽略的点值得专门说。
第一,短的数字高位视为0,所以循环长度取两者最大值,然后在循环内部判断下标是否越界。千万不要只遍历短的数组长度,否则高位直接丢失。
第二,循环结束后必须单独检查一次carry。比如999加1,按位算完,最后还有一个进位1,如果不单独处理,结果就会少一位。
这两条我见过无数人在比赛里翻车,属于高精度加法的经典坑。
2.3 高精度减法实现与借位处理
减法比加法多两个步骤:先比较大小确定正负号,再处理借位,最后去掉前导零。
先看大小比较。如果a的长度不等于b的长度,长的大;如果长度相等,从最高位(数组末尾)往低位逐位比较,找到第一个不相等的位,数值大者整个数更大。这里必须注意数组是倒序存的,所以最高位是数组末尾。
比较函数实现:
cpp复制// 返
