这么多年写代码,有个问题几乎每个开发都会撞上:好好的数字,算着算着就“不对了”。写C++的,int一乘就溢出,结果变成负数;写Python的,0.1加0.2算出来是0.30000000000000004;写Java金融系统的,double算金额,对不上账,被财务追着跑。这些问题的根子都在同一个地方——计算机内置的数字类型,要么位数不够,要么精度不够。于是就有了“高精度加减乘除算法”这门手艺,一套专门处理超大整数、超高精度小数的运算方案。
这篇文章把高精度算法的原理、代码实现和实战避坑一次性讲透。你会看到大数加法怎么用“竖式计算”的思路落地,乘法为什么要把数组下标错开,除法保留小数位该怎么处理,还会对比C++手写高精度、Python内置大整数和Java BigDecimal三种主流方案的使用场景与坑点。适合刷算法题的在校生、写业务系统时被金额精度折磨的后端开发,以及想彻底搞懂数字精度问题来龙去脉的编程爱好者。
1. 高精度加减乘除到底解决什么问题
1.1 先看几个“翻车现场”
先说说我最早踩的坑。大学的时候写一个计算阶乘的小程序,算到 20! 的时候输出变成了负数,当时完全懵了,检查半天逻辑也没问题。后来才知道int占32位,最大值是2147483647,而20!已经到2.43e18,早就溢出到天上去了。
类似的“翻车现场”还有这些:
- Python里
0.1 + 0.2,期望是0.3,实际得到0.30000000000000004。这不是语言bug,而是浮点数用二进制存储,0.1和0.2的二进制表示是无限循环小数,存储时就截断了。 - Java里
System.out.println(0.1 * 3)输出0.30000000000000004,double类型无法精确表达十进制小数。 - 金融系统里用double算钱,一单差几分钱,一万单就是几百块差额,审计一查一个准。
- 计算 100 的阶乘,普通整型连影都看不到,100! 有158位十进制数,远超任何内置整型的范围。
这些问题的共同特征就是:数字的位数超出了CPU原生数据类型的表示范围,或者小数部分无法被二进制精确编码。高精度算法就是在这个背景下出现的——用数组或字符串来模拟数字,用代码来模拟手工竖式运算过程,从而突破内置类型的位数和精度限制。
1.2 高精度算法适用哪些场景
高精度算法的应用范围比你想象的大得多,至少包括这些场景:
第一个是算法竞赛和面试题。像“大数加法”“大数乘法”“计算N的阶乘”这类题目几乎是必考题,用来考察数组操作、进位处理和边界条件的掌握程度。
第二个是金融与计费系统。金额计算要求绝对精确,Java后端几乎清一色用BigDecimal,Python用decimal模块,C++则需要封装高精度小数类。
第三个是密码学与科学计算。RSA加密里的大整数运算,动辄1024位、2048位,普通整型完全无法承载。圆周率计算到几百万位,也依赖高精度运算实现。
第四个是编程语言底层实现。Python 3的int类型本质上就是高精度整数,理论上可以无限大,只是内存约束。Java的BigInteger、BigDecimal同样如此。
核心结论是:只要涉及“数字超过内置类型上限”或者“小数不能被二进制精确表示”的运算场景,都绕不开高精度算法。
1.3 内置类型与高精度方案的能力边界对比
| 方案 | 整数上限 | 小数精度能力 | 典型用途 |
|---|---|---|---|
| C++ int | 约21.47亿 | 不支持小数 | 小范围整数计算 |
| C++ long long | 约922京(9e18) | 不支持小数 | 中等范围整数计算 |
| C++ double | 约1.8e308 | 约15-17位有效数字 | 科学计算、图形学 |
| Python int | 无限(受内存限制) | 不支持小数 | 任意大整数运算 |
| Java BigInteger | 无限(受内存限制) | 不支持小数 | 大整数加密、哈希 |
| Java BigDecimal | 无限(受内存限制) | 任意精度,可配置舍入 | 金融、计费、税务 |
| Python decimal | 无限(受内存限制) | 任意精度,可配置舍入 | 金融业务、精确统计 |
| C++手写高精度 | 随数组长度扩展 | 自行实现 | 算法题、竞赛、底层库 |
从这个表里能看出一个事实:不同语言对高精度运算的支持程度差异极大。Python用户大部分时候不用手写大数算法,因为内置int就能处理;Java后端直接用BigDecimal;但C++没有标准库级别的高精度数字类型,所以算法比赛里还得自己写。接下来分语言逐一拆解。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高精度加法的核心思路与实现细节
2.1 竖式运算的思想迁移
高精度算法的灵魂就是“竖式运算”,把小学数学的竖式过程搬到代码里。手算 1234 + 5678 的时候,我们会从个位开始逐位相加,满10进1;代码里干的事一模一样,只是数字的每一位被拆出来,存进数组或者字符串。
先说存储方式。一个数字比如 123456789,在内存里最好倒着存(个位在数组下标0,十位在下标1),这样进位只需要向后数组追加,而不用频繁地整体移动元素。举个例子:
- 数字 987654321 存入
vector<int> a后,a[0]=1, a[1]=2, a[2]=3, ... a[8]=9 - 两个数相加时,
a[i] + b[i]直接对应同一位
倒序存储是几乎所有高精度实现的共同选择,原因很简单:进位操作发生在最高位时,只需要 push_back(1) 就行,如果正序存储,要在数组开头插入元素,那复杂度就是O(n),性能差距明显。
2.2 C++实现大数加大数
用C++的vector实现,代码如下:
cpp复制#include <iostream>
#include <vector>
#include <string>
using namespace std;
// 将字符串数字转为倒序存储的vector
vector<int> strToVec(const string& s) {
vector<int> res;
for (int i = s.size() - 1; i >= 0; i--) {
res.push_back(s[i] - '0');
}
return res;
}
// 高精度加法:a + b
vector<int> add(vector<int>& a, vector<int>& b) {
vector<int> res;
int carry = 0; // 进位
int n = max(a.size(), b.size());
for (int i = 0; i < n; i++) {
int sum = carry;
if (i < a.size()) sum += a[i];
if (i < b.size()) sum += b[i];
res.push_back(sum % 10);
carry = sum / 10;
}
if (carry > 0) res.push_back(carry); // 处理最高位进位
return res;
}
int main() {
string s1 = "12345678901234567890";
string s2 = "98765432109876543210";
vector<int> a = strToVec(s1);
vector<int> b = strToVec(s2);
vector<int> sum = add(a, b);
for (int i = sum.size() - 1; i >= 0; i--) {
cout << sum[i];
}
cout << endl;
return 0;
}
这段代码的关键点在于循环里 sum = carry + a[i] + b[i],然后 res[i] = sum % 10 是当前位的最终值,carry = sum / 10 是向下一位的进位。由于两部分可能长度不同,下标越界时按0处理。
注意:
if (carry > 0) res.push_back(carry)这一步不能漏。比如 999 + 1 = 1000,循环只处理3位,最后一位进位会产生新的最高位,漏掉这一步结果就成000了。
2.3 大数加大数的时间复杂度与优化空间
高精度加法的时间复杂度是O(n),n是位数。理论上线性时间已经是最优,但实际工程中还有优化空间。
比较典型的优化是“压位存储”。如果vector的每个元素只存0~9,那一个int浪费了31个bit。把每个元素存0~9999(四位),数组长度直接缩短为原来的四分之一,循环次数减少了,内存也省了。压位后加法的进位判断就不是“满10进1”,而是“满10000进1”。代码如下:
cpp复制vector<int> addCompressed(vector<int>& a, vector<int>& b) {
vector<int> res;
int carry = 0;
int n = max(a.size(), b.size());
for (int i = 0; i < n; i++) {
int sum = carry;
if (i < a.size()) sum += a[i];
if (i < b.size()) sum += b[i];
res.push_back(sum % 10000);
carry = sum / 10000;
}
if (carry > 0) res.push_back(carry);
return res;
}
压位存储唯一的麻烦是输出的时候高位不足四位的要补前导零。比如一个元素存的是7,但它在高位区域,实际上代表的是0007,如果直接输出就错了。这个细节在算法竞赛里特别容易踩,输出前需要手动处理。
3. 高精度减法和乘法的实现难点
3.1 减法:借位与符号处理是最大陷阱
减法比加法难的地方有两个:一个是大数减小数时结果为负数,另一个是借位处理。
先看一个不处理符号的版本。核心思路是模拟竖式减法:从低位到高位逐位相减,如果当前位不够减,就向高位借1当10,同时高位的数字减1。
cpp复制// 前提:a >= b,否则结果不正确
vector<int> sub(vector<int>& a, vector<int>& b) {
vector<int> res;
int borrow = 0; // 借位
for (int i = 0; i < a.size(); i++) {
int diff = a[i] - borrow;
if (i < b.size()) diff -= b[i];
if (diff < 0) {
diff += 10;
borrow = 1;
} else {
borrow = 0;
}
res.push_back(diff);
}
// 去掉前导0,例如 100-99=001,需要变成1
while (res.size() > 1 && res.back() == 0) {
res.pop_back();
}
return res;
}
这个实现有个隐蔽的问题:如果a小于b,那结果就错了。所以封装减法时,第一步必须先比较两个数的大小,用绝对值大的减去绝对值小的,然后根据实际情况在前面加负号。比较大小也简单,先比位数,位数相同再从高位往低位逐位比。
实际项目中我习惯把“比较大小”“加法”“减法”封装成一组函数,内部自己处理符号。比如 calculate(a, b, op) 这个统一入口,遇到减法时先判断a和b大小,如果a<b就交换再做减,输出时加上负号。这个设计能避免调用方在业务逻辑里反复判断符号,减少出错概率。
3.2 乘法:双重循环与错位相加
乘法是高精度运算里最容易写错的一个。核心原理还是竖式,手算 123 × 45 时,我们把 123 乘 5,再乘 40,然后错位相加。代码里的实现就是双重循环:
cpp复制vector<int> mul(vector<int>& a, vector<int>& b) {
vector<int> res(a.size() + b.size(), 0);
for (int i = 0; i < a.size(); i++) {
long long carry = 0;
for (int j = 0; j < b.size(); j++) {
long long cur = res[i + j] + (long long)a[i] * b[j] + carry;
res[i + j] = cur % 10;
carry = cur / 10;
}
res[i + b.size()] += carry;
}
// 去掉前导0
while (res.size() > 1 && res.back() == 0) {
res.pop_back();
}
return res;
}
关键点在于 res[i + j] 这个下标。a的第i位实际上代表 a[i] * 10^i,b的第j位代表 b[j] * 10^j,两者相乘的结果落在 10^(i+j) 这一位上,所以下标要做两次加法,体现“错位相加”的核心思想。
注意:代码里用
long long接收中间结果。如果直接用int,两个三位数相乘的结果可能超过int上限,造成溢出。这一点在压位存储的时候尤其明显,比如存四位数的数组元素相乘,结果可能达到 9999×9999≈1e8,必须用更大类型接住。
3.3 乘法的时间复杂度与压位优化
普通双层循环乘法的时间复杂度是O(n²),n是数字的位数。这个复杂度在位数上万时变得不可接受。进制优化同样适用,四位数一压,数组长度缩短四倍,乘法的复杂度就从O(n²)降到O(n²/16),实际体验差距非常大。
如果位数来到百万级别,比如计算圆周率的百万位,就得用更高级的算法了:Karatsuba乘法可以到O(n^1.585),快速傅里叶变换(FFT)可以到O(n log n log log n)。但这类优化实现复杂度陡增,竞赛和业务开发中,O(n²)加压位处理已经覆盖绝大多数场景。
4. 高精度除法:最难啃的硬骨头
4.1 大数整除大数:试商法的实现
除法是四则运算里最复杂的。加法是线性扫描,乘法是双重循环,而除法的核心是“试商”——每次猜测一位商数字,然后乘回去验证,大了就减小。
一个比较实用的实现思路是“减法模拟除法”:被除数不断减去除数,统计减了多少次,商就是多少。但这样效率太低,例如 1000000000 ÷ 1,要减十亿次,直接超时。
更好的方法是“逐位试商”。把被除数从高位往低位逐位取下来,形成一个当前值cur,每次计算 cur / b 得到商的一位数字,cur % b 作为余数传给下一位。这个过程用文字描述有点绕,直接看代码:
cpp复制// 这里的a是正序存储的字符串数字(高位在前),b是一般整数
string divide(string a, int b, int& remainder) {
string quotient;
int cur = 0;
for (char c : a) {
cur = cur * 10 + (c - '0');
quotient.push_back('0' + cur / b);
cur = cur % b;
}
remainder = cur;
// 去掉前导0:商的第一位如果是0,要删掉
int pos = quotient.find_first_not_of('0');
if (pos == string::npos) return "0";
return quotient.substr(pos);
}
这里被除数用字符串正序存储,商也是一位一位生成的,最后统一去掉前导零。但这个方法只支持“大数除以普通整数”,如果除数也是大数,实现复杂度就上了一个档次。
4.2 高精度除以高精度的实现思路
大数除以大数,最常用的方法还是“模拟手算长除法”,但试商过程需要额外比较。具体步骤:
- 截取被除数的一段,使这段大于等于除数。
- 用二分法或者循环尝试,找出最大k,使得
k × divisor ≤ 当前段。 - 把
k作为商的一位数字,然后从当前段中减去k × divisor,得到余数。 - 从被除数后面取下一位数字,拼到余数末尾,继续第2步,直到被除数取完。
这个过程中涉及大数乘法、大数减法、大数比较三个子函数,任何一个出错,结果都会偏。代码量不短,工程实现时建议先单独测试乘法和减法,确保子函数正确再组装除法逻辑。
4.3 除法的精度控制:保留N位小数
实际业务中很少需要“整除余数”,更多是要求“保留两位小数”或者“保留八位小数”。比如 BigDecimal 的 divide 方法,必须指定 scale 和 RoundingMode,否则会抛 ArithmeticException: Non-terminating decimal expansion。
手写高精度保留小数的思路是:先把被除数补零,再按整数除法处理,最后在正确的位置点上小数点。以计算 10 ÷ 3 保留4位小数为例:
- 把10变成100000,除以3,得到33333,商的小数点在第4位之前,即3.3333
- 精确做法:被除数乘以10ⁿ,n是保留的小数位数,然后做整数除法,得到的商从右往左数n位放小数点
cpp复制// 计算 a / b,保留scale位小数,a和b都是字符串大数
string divideToScale(string a, string b, int scale) {
// 1. a 补 scale 个0,相当于乘以 10^scale
// 2. 执行大数除以大数的整数除法
// 3. 得到的结果从右往左数 scale 位插入小数点
// 4. 如果位数不足 scale,前面补0
}
再补充一点:舍入模式也需要考虑。四舍五入比较符合直觉,但银行计息可能用“四舍六入五成双”,也就是银行家舍入。BigDecimal的 RoundingMode.HALF_EVEN 就是这种模式,在等概率的边界上能让统计误差趋近于零。自己实现高精度小数运算时,舍入模式的取舍要提前定好,不然后期改逻辑会非常痛苦。
5. Python大整数与输入输出实战
5.1 Python内置int其实已经是高精度
接触Python三年后回头看,Python int 的内部实现说穿了就是C语言写好的高精度整数。Python 3里int没有位数上限,10**1000 可以瞬间算出来,这正是因为它底层就是用数组存储每一位,自动管理进位和借位。
所以用Python刷算法题,遇到“大数加法”这类题,直接 int(a) + int(b) 就完事了,根本不需要手写数组模拟。真正让你头大的反而是输入输出。
5.2 Python输入输出的常见坑
先看一串代码:
python复制a = input()
b = input()
print(int(a) + int(b))
这段代码在在线评测系统里能过,但在实际脚本里却可能踩坑。最常见的问题有三个:
第一个是行尾换行符和空格。input() 默认会去掉行尾的换行符,但是如果在同一行输入 123 456,一个input()只能拿到 '123',第二个数你得用 split() 拆。正确写法是:
python复制a, b = map(int, input().split())
第二个是EOFError。脚本从文件或管道读入时,可能遇到没有数据可读的情况,input() 直接抛异常。稳妥的做法是循环读取直到EOF:
python复制import sys
for line in sys.stdin:
if not line.strip():
continue
# 处理每一行数据
第三个是字符串转数字时的异常。输入里可能混有空格、换行、空串,直接 int() 转换会ValueError。所以在线上环境处理数据,我习惯先清洗再转换,或者用 try-except 包住转换逻辑。
5.3 Python高精度小数的正确打开方式
Python内置的float是双精度浮点数,该丢精度还是会丢。想要精确的十进制小数运算,必须用 decimal 模块。看个对比:
python复制from decimal import Decimal, getcontext
# 错误示范
print(0.1 + 0.2)
# 0.30000000000000004
# 正确姿势
print(Decimal('0.1') + Decimal('0.2'))
# 0.3
# 调整精度,默认28位有效数字
getcontext().prec = 50
print(Decimal(1) / Decimal(7))
# 0.14285714285714285714285714285714285714285714285714
用Decimal时有几个细节值得注意:构造参数要传字符串,Decimal(0.1) 会先把float的近似值转进去,反而失去了精确性;除法时容易遇到无限循环小数,建议手动设置精度上下文或者使用quantize指定保留位数。
6. Java BigDecimal实战与金融场景避坑指南
6.1 BigDecimal和BigInteger的基本玩法
Java后端处理高精度计算,BigDecimal几乎是唯一解。它支持任意精度的有符号十进制数,既适合大整数运算,也适合精确保留小数。基本使用方式如下:
java复制import java.math.BigDecimal;
import java.math.RoundingMode;
BigDecimal a = new BigDecimal("123.45");
BigDecimal b = new BigDecimal("67.89");
// 加法
BigDecimal sum = a.add(b);
// 减法
BigDecimal diff = a.subtract(b);
// 乘法
BigDecimal product = a.multiply(b);
// 除法必须指定精度和舍入模式,否则可能异常
BigDecimal quotient = a.divide(b, 4, RoundingMode.[HAL](https://taotoken.net/?utm_source=general)F_UP);
这里必须反复强调:BigDecimal的除法一定要传scale和RoundingMode。如果不传,遇到像1除以3这样的无限小数,直接抛ArithmeticException。我在生产环境里见过不传参调用divide导致接口500,排查半天才找到原因。
6.2 构造BigDecimal的三种方式对比
BigDecimal有三种常见构造方式,效果完全不同:
new BigDecimal(0.1):不推荐。传入的double是近似值,BigDecimal存的是这个近似值的精确十进制展开,结果是"0.1000000000000000055511151231257827021181583404541015625"new BigDecimal("0.1"):推荐。字符串原样解析,结果是精确的"0.1"BigDecimal.valueOf(0.1):推荐。内部先调Double.toString,再转BigDecimal,结果也是"0.1"
我在代码审查时看到 new BigDecimal(0.1) 这种写法都会打回。一旦踩了这个坑,后面所有运算全都会带着一个极小的误差,金额统计时差异就暴露了。所有浮点数转BigDecimal,必须先转成字符串。
6.3 equals与compareTo的深刻教训
BigDecimal的 equals 和 compareTo 行为不一致,这个坑坑过无数人。equals 要求数值和精度都相等,new BigDecimal("1.0").equals(new BigDecimal("1.00")) 返回false;而compareTo只比较数值大小,上面两个compareTo返回0。
业务中判断金额是否相等应该用 compareTo,而不是 equals。比如查账系统里,一笔记为“10元”,另一笔记为“10.00元”,按数值是一样大的,但equals直接判为不相等,导致对账失败。这种问题非常隐秘,运行很久才会被用户发现。
6.4 BigDecimal性能与线程安全
BigDecimal是不可变对象,线程安全,但是每次运算都会创建新对象,性能比原生double慢几十倍。如果循环里做了上万次BigDecimal运算,耗时可能从毫秒级涨到秒级。优化手段有两个:
- 大量累加时,先转成long或int(金额按“分”存储),最后再转BigDecimal。比如金额单位是“元”,数据库存“分”,那直接用long做累加,最后
BigDecimal.valueOf(total, 2)转换。 - 需要频繁计算的场景,考虑用缓存、批量聚合等方式减少BigDecimal的调用次数。
我在一个计费系统里做过优化,把原本每个订单都做三次BigDecimal运算改成批量按天聚合后再运算,耗时直接从平均80ms降到8ms,效果非常显著。
7. 典型实战案例:从手写算法到业务落地
7.1 案例一:计算1000的阶乘
这是算法领域最经典的高精度练习。用Python一行代码能搞定:
python复制import math
print(math.factorial(1000))
但用C++手写就考验功底了。思路是先初始化结果数组为1,然后从2循环到1000,每次做“大数乘以普通整数”的乘法。这个乘法要比大数乘大数简单,因为外层只有一层循环。
cpp复制vector<int> factorial(int n) {
vector<int> res(1, 1);
for (int num = 2; num <= n; num++) {
int carry = 0;
for (int i = 0; i < res.size(); i++) {
int cur = res[i] * num + carry;
res[i] = cur % 10;
carry = cur / 10;
}
while (carry > 0) {
res.push_back(carry % 10);
carry /= 10;
}
}
return res;
}
注意第二层循环里 res[i] * num 可能很大,num最大到1000,res[i]是9,乘积9000,加上carry还在int范围内,但如果阶乘的n更大,比如算100000的阶乘,num本身是五位数,中间结果可能溢出,需要用long long。这里有个经验:高精度乘法里,只要两个数都不算小,中间变量一律用long long,不出事比省内存重要。
7.2 案例二:金融系统金额精确计算
业务场景是电商订单金额拆分。订单总价100元,三个商品分别占比例1/3、1/3、1/3,怎样把总价拆分到三个订单行,且三行之和严格等于100元?
如果直接用double计算每个金额 100 × 1/3 ≈ 33.333...,三个加起来是100.000...0001,偏差就出现了。正确做法是用BigDecimal配合精确舍入:
java复制BigDecimal total = new BigDecimal("100");
BigDecimal oneThird = new BigDecimal("100").divide(new BigDecimal("3"), 2, RoundingMode.DOWN);
BigDecimal twoThirds = new BigDecimal("100").subtract(oneThird);
// 比如拆成两行:一行33.33,一行66.67,加起来正好100
这里的关键是“舍入时机”不是每个比例乘完就舍,而是让最后一个金额等于总额减去前面所有金额,把舍入误差兜底到最后一行。这个技巧叫“尾差处理”,做财务系统时是必备技能。
7.3 案例三:高精度计算π值作为验证
想知道自己手写的高精度乘法对不对,可以算圆周率来验证。最简单的基于莱布尼茨公式:π = 4 × (1 - 1/3 + 1/5 - 1/7 + ...),每一项都涉及高精度除法。运行后和已知的π值比对,对上了说明乘法除法实现都正确。
我第一次实现FFT乘法时就是用π的百万位来验证的,当时跑了两天两夜,最后几十万位的数字和网上公布的π对上了,那种兴奋感到现在还记得。如果你手写高精度实现,建议也找这种能自检的题目来验证正确性,比自己瞎测试强很多。
8. 常见问题与排查技巧实录
8.1 高精度算法问题速查表
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 结果前面多了一堆0 | 前导零未处理 | 检查去前导零的while循环是否执行 |
| 加法结果少了最高位 | 最高位进位未push_back | 检查carry是否为0 |
| 减法结果全为0 | 借位逻辑写反 | 逐位打印中间diff和borrow |
| 乘法结果位数不对 | 结果数组长度初始化错误 | 检查res初始化大小是否为a.size()+b.size() |
| 除法商第一位多0 | 试商逻辑重复 | 检查循环边界,是否在开头多算了一次 |
| BigDecimal除法抛异常 | 未指定精度和舍入模式 | 给divide传scale和RoundingMode |
| BigDecimal equals结果不对 | 数值相同但精度不同 | 用compareTo代替equals |
| Python float计算结果有尾差 | 二进制无法精确表示十进制小数 | 改用Decimal模块 |
8.2 调试高精度代码的实用技巧
调试高精度算法有一个特别好用的方法:拿Python内置大整数当标杆。比如你写了C++的大数乘法,随便生成两个大数,用Python算一遍结果,再和自己C++程序的结果对比。两边不一致就从低位逐位打印,看是从哪一位开始分叉的,能快速定位问题。
另一个方法是“小规模测试加边界测试”。先用个位数测:9+1、999+1、1000-1,这类边界情况最容易暴露进位借位问题。然后再测两个长度差异大的数,比如一位数加一百位数,再看下标越界是否处理正确。
8.3 生产环境踩坑记录
我曾经接过一个排查任务,某报表系统偶尔出现金额少一分钱的情况。代码看了一遍,BigDecimal用法没问题,舍入模式也指定了,最后发现bug出在数据库字段类型上:金额字段用的是 DECIMAL(10,2),但程序里BigDecimal保留了4位小数,JDBC驱动直接截断而不是四舍五入,导致数据写入时丢了尾部两位。
这个坑提醒我:高精度计算不只是代码层的责任,数据库、接口协议、JSON序列化每一层都可能有精度损失。JSON传输时如果用float类型,解析后同样会丢精度,正确的做法是金额字段统一用字符串传输,数据库统一用DECIMAL,代码层统一用BigDecimal——全链路一致才能保证不差一分钱。
9. 高精度算法性能优化的一些心得
9.1 压位、压位、还是压位
如果你的高精度代码在算法竞赛里超时,压位是最先需要考虑的优化。以乘法为例,不压位的数组长度是n,压四位后变成n/4,双层循环的累加次数减少到原来的1/16,这是一个数量级的提升。
但压位有个前提:中间结果的类型要足够大。四个十进制位相乘,最大是9999×9999≈1e8,多个相加可能超过int上限,所以中间变量用long long是必须的。压位数不是越大越好,超过int能承载的范围反而要拆分,一般4位是最佳平衡点。
9.2 除法优化:二分试商
大数除以大数时,每次试商从9循环到0,最多试10次;但如果用二分法在0~9之间找最大可行值,只需要约4次比较。虽然10次和4次差距不大,但除法过程会进行大量大数乘法、减法,每少一次大数运算都能实打实省时间。
位数上涨后,可以考虑牛顿迭代法求倒数再乘,这在浮点高精度计算库中很常见,但实现复杂度很高,普通场景不推荐硬上。
9.3 大数乘法再进阶:Karatsuba与FFT
当乘法位数上万,O(n²) 的复杂度就扛不住了。Karatsuba算法把两个n位数拆成高位和低位,用三次乘法替代四次乘法,递归下去,复杂度降到O(n^1.585)。FFT则把卷积运算变成点值运算,复杂度可以到O(n log n)。这两类算法我建议先掌握原理,真的需要时再参照开源库实现,不要自己从零硬写,容易出边界bug。
Python的 gmpy2 库底层已经封装了这些优化算法,Java的BigInteger内部也有Karatsuba优化,日常业务完全够用。手写高精度主要还是在算法学习和竞赛场景,追求一步到位的极致优化不如先保证正确性。
最后一件事:始终保留一位“参考答案”
写这套高精度算法,我自己的体会是:正确性比性能更依赖验证手段。因为高精度运算的输入输出都很大,人工根本判断不了对错,必须有一个可信的参考答案来对照。Python内置int、Java BigDecimal、甚至Windows计算器,都可以充当这个“裁判”。每次改完代码,拿它跑上一堆随机用例,错误自然现形。
如果你刚开始接触高精度算法,建议按这个顺序来:先把加法、减法写对,再加乘法,最后啃除法。每一步都找几道算法题练手,比如大数A+B、大数A×B、计算阶乘。把基础打牢了,后续无论是用BigDecimal做金融结算,还是自己实现底层的大数库,都能做到心里有数。踩过数字溢出的坑,才知道每一步都值得认真对待。
