☰
高精度算法详解:C++实现大数四则运算与性能优化
2026/9/30 9:22:12 网站建设 项目流程

先问一个看起来很基础的问题:在C++里,怎么精确计算2的1000次方?如果你直接写pow(2, 1000),得到的浮点数早就丢了精度;如果硬用long long存,编译期就能看到溢出的苗头。答案只有一条路:上高精度算法,也就是用数组或字符串把超长整数拆成一位一位地存,再手动模拟加减乘除。

我之前做项目时也遇到类似场景,要统计一个持续累加的计数,数值轻松突破几十位,long long完全兜不住,用double又全是误差,最后只能老老实实自己实现一套基于数组的高精度运算。写完之后我发现自己顺手搭起了一个可以复用的基础组件,后面做阶乘、幂运算、递推数列处理全都靠它。这篇就把高精度算法的完整思路、C++实现细节、性能优化和踩过的坑一次讲清楚,适合刚接触算法的同学,也适合想快速获得一套可落地代码的从业者。

1. 为什么要突破整型上限:先搞懂天花板在哪

1.1 C++整型的容量一览

很多新手不理解为什么需要高精度,觉得long long已经"足够大"了。那我们先把C++整型的容量盘一遍:

类型位数最大值数量级
int32位2,147,483,647约2.1×10^9
unsigned int32位4,294,967,295约4.3×10^9
long long64位9,223,372,036,854,775,807约9.2×10^18
unsigned long long64位18,446,744,073,709,551,615约1.8×10^19
__int128(GCC扩展)128位约3.4×10^38约3.4×10^38

看起来数字挺大,但一碰到真正的数学问题就露馅了。我随便写段代码感受一下溢出:

#include <bits/stdc++.h> using namespace std; int main() { long long x = 1; for (int i = 1; i <= 25; i++) { x *= i; cout << i << "! = " << x << '\n'; } return 0; }

运行到21的时候,输出已经变成负值了。21!约等于5.1×10^19,而long long上限是9.2×10^18,直接爆掉。如果题目要求算100!甚至1000!,自带整型根本无解。100!约等于9.33×10^157,位数超过157位,即便用__int128也只能存下它零头的零头。

1.2 哪些场景逼着你必须用高精度

我总结了几类最常见的需求场景,基本上只要遇到其中之一,你就要立刻想到高精度:

  • 大数阶乘:n超过20就会溢出long long,20!约2.43×10^18刚好还能塞下,21!直接爆炸。
  • 大数幂运算:比如2^1000、3^500这类题目,在基础算法练习里出现频率极高。
  • 递推数列:斐波那契数列第92项是7540113804746346429,还没超long long,但第93项就已经越界了。卡特兰数、Hanoi塔步数等也都类似。
  • 密码学相关的底层运算:RSA、大素数判定这种场景,动不动就出现1024位、2048位的大整数,必须依赖任意精度运算。
  • 业务统计中的精确累计:我做项目时遇到过需要精确记录一个总数,位数到了几十位的情况。任何浮点方案都是错的,只能高精度硬扛。

还有一个很实际的原因:在各种算法题和面试题中,高精度经常作为"辅助工具"出现。你真正要考的是贪心、DP或分治,但计算过程需要先解决大数存储。高精度这部分基础不牢,主算法写得再漂亮,也可能因为一个溢出导致全盘皆输。

2. 核心设计思路:用数组模拟竖式运算

高精度算法的本质,就是把"数字"从CPU原生寄存器里解放出来,改用内存里的容器逐位保存,再模仿小学竖式的方式完成运算。这个思路一点也不神秘。

2.1 存储选型:vector 还是 string

我见过有人直接用string存高精度数,觉得输入输出方便。实际做运算的时候,字符和数字的来回转换非常别扭,做加法和进位操作更是绕手。我的建议是统一用vector ,每一位存一个0~9的数字。

理由很简单:vector支持随机访问和动态扩容,做进位处理时直接修改对应下标位置的元素,代码直观且效率高。string在底层也是连续存储,但它的元素类型是char,算术运算时还得考虑字符编码偏移,徒增心智负担。vector 一眼就能看出"这是一个数字数组",语义比string清晰太多。

2.2 数字存放顺序:低位在前是唯一正解

这是新手最容易纠结的问题:高精度数在数组里到底应该高位在前,还是低位在前?

我的答案是:默认全部用低位在前,也就是数组下标0存个位,下标1存十位,以此类推。原因有两个:

  • 所有运算都必须从低位开始算,因为进位是从低位往高位传递的。低位在前可以直接用下标i访问对应位,进位只需要追加到数组尾部。
  • 数组尾部天然对应高位区域,加法或乘法产生的新进位直接push_back即可,不需要整体移动元素。

代价只有一个:打印输出时需要倒序遍历。通过一个printBigNum函数就能解决,相比运算的便利,这点成本完全可以忽略。

2.3 字符串与高精度数的互相转换

实际输入通常以字符串形式给出,因为输入本身就可能超过整型范围。转换的标准写法如下:

#include <bits/stdc++.h> using namespace std; // 字符串转高精度数:低位在前 vector<int> toBigNum(const string& s) { vector<int> a; for (int i = (int)s.size() - 1; i >= 0; i--) { a.push_back(s[i] - '0'); } return a; } // 打印高精度数:注意倒序输出 void printBigNum(const vector<int>& a) { for (int i = (int)a.size() - 1; i >= 0; i--) { cout << a[i]; } cout << '\n'; }

这段代码里要注意两点。第一,s[i] - '0'利用字符编码做转换,是字符运算的基本功。第二,输入字符串是高位在前,而内部存储是低位在前,所以倒着遍历并push_back,正好完成顺序翻转。printBigNum则是反向操作,把内部存储还原成日常书写顺序。

转换完成后,所有运算都在vector 上操作,结果也统一保持低位在前的格式,确保多个运算能无缝串联。

3. 四大基础运算的完整实现与核心细节

高精度算法的基础就是加减乘除四种运算。下面每个运算我都给出可以直接复用的C++实现,并重点拆解关键细节。

3.1 高精度加法:核心就是一个进位标志

加法就是小学竖式:从个位开始逐位相加,超过9就进1。唯一需要额外处理的是,最后一位相加后可能还会产生新的进位。

// 高精度加法 vector<int> addBig(const vector<int>& a, const vector<int>& b) { vector<int> c; int carry = 0; int n = max(a.size(), b.size()); for (int i = 0; i < n; i++) { int t = carry; if (i < (int)a.size()) t += a[i]; if (i < (int)b.size()) t += b[i]; c.push_back(t % 10); carry = t / 10; } if (carry) c.push_back(carry); return c; }

很多人写加法容易漏掉最后的if (carry)。举个例子,99 + 1:个位9+1=10,当前位0,进位1;十位9+0+1=10,当前位0,进位1;循环结束,carry还是1,如果不处理,结果就是"00",等于算出来个0。这是加法里最经典的低级错误,没有之一。

加法的时间复杂度是O(max(m,n)),两个数的位数取较大的那个。这个运算几乎不可能再优化,因为每一位都必须检查一遍。

3.2 高精度减法:借位和前导零是两大坑

减法比加法麻烦,因为涉及借位。我的实现要求调用方保证a >= b,所以实际使用前,需要先写一个比较函数:

// 比较两个高精度数:1表示a>b,0表示相等,-1表示a<b int cmpBig(const vector<int>& a, const vector<int>& b) { if (a.size() != b.size()) { return a.size() > b.size() ? 1 : -1; } for (int i = (int)a.size() - 1; i >= 0; i--) { if (a[i] != b[i]) return a[i] > b[i] ? 1 : -1; } return 0; } // 高精度减法,要求 a >= b vector<int> subBig(const vector<int>& a, const vector<int>& b) { vector<int> c; int borrow = 0; for (int i = 0; i < (int)a.size(); i++) { int t = a[i] - borrow; if (i < (int)b.size()) t -= b[i]; if (t < 0) { t += 10; borrow = 1; } else { borrow = 0; } c.push_back(t); } while (c.size() > 1 && c.back() == 0) c.pop_back(); return c; }

减法必须处理前导零。比如1000 - 999,结果应该是1,但直接算会得到"0001"。虽然打印出来一个数是"0001"数学上没错,但在链式运算中,后面再做乘法或比较,前导零会带来额外干扰。所以每次运算结束后统一去掉高位多余的0,至少保留一位,这个习惯必须在所有运算里保持一致。

比较函数有个小细节:先比长度,长度不同直接判定大小;长度相同再从高到低逐位比较。这个函数不只是减法用,后面做除法试商和排序取大时都会反复用到。

3.3 高精度乘法:两层循环累加再把进位一次搞定

乘法实现思路是:先不急着进位,把所有位置上的乘积累加到一个结果数组里,最后统一扫描一遍处理进位。第i位与第j位相乘,结果存放在c[i+j]上。为什么是i+j?因为一个数的第i位(数值10^i)乘以另一个数的第j位(10^j),乘积落在10^(i+j),正好是结果数组的第i+j位。

// 高精度乘法 vector<int> mulBig(const vector<int>& a, const vector<int>& b) { vector<int> c(a.size() + b.size(), 0); for (int i = 0; i < (int)a.size(); i++) { for (int j = 0; j < (int)b.size(); j++) { c[i + j] += a[i] * b[j]; } } int carry = 0; for (int i = 0; i < (int)c.size(); i++) { int t = c[i] + carry; c[i] = t % 10; carry = t / 10; } while (c.size() > 1 && c.back() == 0) c.pop_back(); return c; }

这里有个性能优化点:把"边乘边进位"改成"先累加再统一进位"。如果一边乘一边进位,内层循环每次都要处理取模和除法,而且在累加过程中进位还会干扰后续的乘法结果。先累加后进位,内层只有一个加法操作,计算量小很多,代码也清晰。

乘法的时间复杂度是O(m×n)。这是基础版,处理上万位的数字时会明显变慢,此时一般会上Karatsuba分治乘法,复杂度能降到约O(n^1.585),或者直接走FFT快速乘法。这个作为进阶内容,后面再展开。

3.4 高精度除法:必须从高位入手

加减乘都是从低位算起,唯独除法要反着来,从高位开始。因为除法本质上是模拟"试商"过程,要看当前余数拼接下一位之后,能除以除数几次。

先给出最常用的版本:高精度数除以一个普通整型。这个操作在阶乘、进制转换、取模运算里出现频率极高:

// 高精度除以低精度,余数通过引用返回 vector<int> divSmall(const vector<int>& a, int b, int& rem) { vector<int> c; int r = 0; for (int i = (int)a.size() - 1; i >= 0; i--) { r = r * 10 + a[i]; c.push_back(r / b); r %= b; } reverse(c.begin(), c.end()); while (c.size() > 1 && c.back() == 0) c.pop_back(); rem = r; return c; }

这段代码的思路就是竖式除法:从最高位开始,当前被除数是上一次的余数乘以10再加上本位数字,整除以b得到商的当前位,余数传给下一位。

reverse那行要特别注意。因为我们从高位遍历,得到的商序列是高位在前的,而存储约定是低位在前,所以需要翻转。很多人第一次写这里容易漏掉,结果商的顺序完全颠倒。

至于高精度除以高精度,核心思路是"试商减":先比较被除数与除数,能减就累加商并做一次减法。朴素写法复杂度O(n×m),代码量翻倍。实际做题中,高精度除以高精度更多出现在"大数取模"场景,那些需求通常可以用循环减法简化实现。

4. 从写对到写快:实操细节与优化心得

代码能跑通只是第一步。下面这些实操层面的细节,能帮你把代码从"能跑"提高到"跑得又快又稳"。

4.1 乘法的进位策略:边乘边进 vs 统一进位

我前面提到乘法有两种写法,展开说一下优劣。边乘边进的形式长这样:

for (int i = 0; i < (int)a.size(); i++) { int carry = 0; for (int j = 0; j < (int)b.size(); j++) { int t = c[i + j] + carry + a[i] * b[j]; c[i + j] = t % 10; carry = t / 10; } c[i + b.size()] += carry; }

统一进位的写法中间不处理任何进位,最后一次性扫描。实测下来,位数超过1000位后,统一进位明显更快,因为内层循环少了一次取模运算。取模在CPU里是相对昂贵的操作,能省就省。

我自己的习惯是:基础实现直接用统一进位版本,只有处理超大数时才考虑引入分治乘法。注意统一进位版本里结果数组要预分配m+n位,否则最后的进位写入会越界。

4.2 前导零与边界:两个肌肉记忆

高精度代码里出现频率最高的bug,就是前导零没去掉。前面所有运算最后我都加了while循环去零,这绝不是多余,而是必须。

同时要记住保留最低一位。如果结果是0,也不能把数组清空,否则打印输出是空的。所以条件统一写成c.size() > 1才pop_back,保证至少留一个0位。

另外,这套代码的前提是输入全为正整数。如果要处理负数,需要额外维护符号位,或者用字符串层面处理负号,复杂度会明显上升。竞赛里绝大多数高精度题都不涉及负数,先把正向运算吃透,负数扩展放在后面再说。

4.3 万进制优化:同样的代码性能提升一个档次

当运算量上来之后,十进制逐位存储就太浪费了。把每个数组元素从0~9扩展到0~9999,也就是"万进制",可以让同样长度的vector容纳4倍的数据量,加减乘除的循环次数也相应降到四分之一。

万进制实现和十进制几乎完全一样,只是把取模和除法的基数从10改成10000。以乘法为例:

// 万进制乘法:每个数组元素存0~9999 vector<int> mulBig_1e4(const vector<int>& a, const vector<int>& b) { vector<int> c(a.size() + b.size(), 0); for (int i = 0; i < (int)a.size(); i++) { for (int j = 0; j < (int)b.size(); j++) { c[i + j] += a[i] * b[j]; } } int carry = 0; for (int i = 0; i < (int)c.size(); i++) { int t = c[i] + carry; c[i] = t % 10000; carry = t / 10000; } while (c.size() > 1 && c.back() == 0) c.pop_back(); return c; }

代价只有一个:打印时,除最高位外,其余每一位输出必须补足4位。比如万进制数组[1, 23],表示的是1×10000 + 23 = 10023。如果直接把23打印成"23",输出变成123,错得离谱。我第一次用万进制时就在这里被坑过,后来强制自己在输出函数里用printf("%04d")格式化中间位。

有人会继续往上做亿进制、甚至用unsigned long long做更大基数(比如每格存2^32),原理都一样,只是要把溢出边界算清楚。万进制是最稳妥的平衡点,9999×9999 = 99980001,远在int范围内,累加两个数也不会溢出int。

4.4 编译环境与实测结果

我平时直接g++一条命令编译:

g++ -O2 -std=c++17 bigint.cpp -o bigint

Windows环境用MinGW,或者VS Code里配好C++插件都行。但不管在哪个环境,编译时一定记得开-O2。高精度运算是循环密集型计算,开O2后的性能差距可以达到3到5倍。我自己实测计算10000!,开-O2大约0.3秒,不开要1.2秒,差距非常明显。

下面给一个完整的高精度阶乘测试程序,把前面零散的知识串起来:

#include <bits/stdc++.h> using namespace std; // 高精度数乘以低精度整数 vector<int> mulSmall(const vector<int>& a, int b) { vector<int> c; int carry = 0; for (int i = 0; i < (int)a.size() || carry; i++) { int t = carry; if (i < (int)a.size()) t += a[i] * b; c.push_back(t % 10); carry = t / 10; } return c; } // 计算 n! vector<int> factorial(int n) { vector<int> res; res.push_back(1); for (int i = 2; i <= n; i++) { res = mulSmall(res, i); } return res; } int main() { int n; while (cin >> n) { vector<int> res = factorial(n); cout << n << "! = "; printBigNum(res); // 倒序输出 } return 0; }

这个mulSmall是"高精度×低精度"的专用版本,在阶乘场景里效率远高于通用乘法,因为它省掉了嵌套循环。实际做题时,很多看起来"必须用高精度乘法"的题目,仔细拆解后都会退化成"高精度×低精度"。能识别出这一点,代码量能简化一大半。

5. 常见问题与排查技巧实录

这一部分都是实打实的经验。下面的问题我几乎每个都在实战中踩过,整理出来给你避坑。

5.1 典型错误速查表

症状根因解决办法
加法结果少了一位忘了处理最高位进位循环结束后检查carry,非零则push_back
减法或乘法结果前面有一串0没去前导零运算后统一循环去零,保留至少一位
打印结果多了或少了0存储顺序与输出顺序没对齐确认低位在前,打印时倒序
除法商完全颠倒没做reverse高位遍历产生的商要翻转回低位在前
万进制输出缺0中间位补零不到位除最高位外都用%04d格式化输出
乘法数组越界结果长度分配不足结果长度设为m+n位,防止进位越界

5.2 测试用例怎么设计

高精度代码调试,最忌一上来就测超大数。我的习惯是先跑一组极端边界用例,覆盖所有进位、借位、前导零路径:

  • 加法:999 + 1,验证最高位进位;0 + 0,验证空结果。
  • 减法:1000 - 999,验证连续借位和前导零;123456789 - 123456789,验证结果为0。
  • 乘法:999 × 999,验证多层进位;0 × 100,验证长度为1的结果。
  • 除法:100 / 3,验证余数和商的顺序;1 / 100,验证商为0的情况。

这组用例全过了,再上大数。我通常拿Python做参照物:用Python算一遍同样的大数,再用C++程序算一遍,对比结果是否一致。Python内置大整数,是验证C++高精度实现最方便的工具,没有之一。这个方法看着简单,却能帮你省掉大量排查时间。

5.3 高精度代码的性能瓶颈在哪

真正处理几万位的大数时,速度瓶颈会非常明显。我总结了几条实用经验:

  • 能用高精度×低精度,就绝不用高精度×高精度。阶乘、幂运算这类场景天然符合这个规律。
  • 优先上万进制,改动简单收益巨大。
  • 对几千位的数,乘法考虑Karatsuba;对几万位的数,就得上FFT了。
  • 输入输出用C风格的scanf/printf,或者关掉cin/cout同步(ios::sync_with_stdio(false))。高精度运算过程伴随大量IO,这个环节的耗时占比不小。

关于FFT乘法,提一句原理就够了:两个数组的卷积恰好就是乘法的累加过程,FFT能在O(n log n)时间内完成卷积,因此可以用来做超大数乘法。不过FFT涉及浮点误差和预处理,属于进阶玩法,初学者先把基础四则运算吃透更现实。

5.4 与其他现成方案的取舍

可能有人会问:C++里不是有Boost的cpp_int吗?Python的int不是也直接支持大数吗?这个现实问题确实值得说。

我的看法是:如果是写业务代码,能用现成库就用现成库,没必要重复造轮子。但如果是学习算法、应对竞赛,或者需要深度控制性能(嵌入式环境、内存受限场景都是典型例子),自己实现一遍高精度是必须的基本功。更重要的是,你理解了底层实现之后,再回头用cpp_int这类库时心里会特别有底,出了问题也知道大概卡在哪个环节。

写在最后

高精度算法这套东西,说难不难,说简单也不简单。它的每一行逻辑都是小学数学竖式的翻版,但真正把它写对、写快、写稳,需要你对进位、借位、前导零这些细节有近乎苛刻的敏感。我个人最大的体会是,这类算法题特别适合用来训练工程化思维:先设计存储结构,再实现核心操作,最后用边界用例做验证。这个流程和做大型软件工程的思路完全一致。

最后分享一个实用小技巧:我每次写完一个新的高精度运算函数,都会顺手放进一个统一的测试脚本里,用Python随机生成大数对,与C++结果自动对比。这个流程看着繁琐,但坚持下来,你的高精度代码会可靠到可以放心复用。希望这篇能帮你少走几条我走过的弯路。

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

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

立即咨询