位数再多,只管稳稳进位,终会把答案写满。
“高精度整数”指不受 C++ 原生整型 (int
/ long long
) 位宽限制,而用数组模拟任意位数的大整数。
64 位 long long
仅能保存到 9 × 10¹⁸。五级真题里随手就超出:
① 先谈“存储哲学”——把大整数拆成“块”并线性摆放
高精度实质上是把人类熟悉的十进制笔算过程,翻译成 C++ 的数组操作。任何一个千位、万位甚至百万位的十进制整数,都可以视作“若干个低位到高位连续排列的数字块”。高精度使用者的责任,就是决定“每个块存多少信息”、决定“块们的排列方向”,并确保 进位、借位、比较、截零 等操作在这样的布局下都能高效完成。存储方案因此成为高精度体系的基石:如果选得好,后续加减乘除皆可顺滑;若选得拙劣,则会在常数时间和代码复杂度上付出高昂代价。
② “一位一存”——最朴素但也最易懂的方案
最原始的做法是:把十进制的每一位(0‥9)直接存在 int
数组里。假设要存 31415926
,则定义 int a[8]
,把低位 6 放到 a[0]
(也可以存在 a[1]
),再依次存 2、9、5 … 直至最高位 3 存入 a[7]
。这种“小端倒序”布局有几大显见好处:
a[0]
开始循环,满 10 进 1,直观不易错。然而,“一位一存”也有天然瓶颈:循环次数与十进制位数完全等长。5000 位整数相加就得跑 5000 次循环,乘法则是平方级——两数都是 5000 位时,朴素乘法将运行 25 000 00 次乘-加-进位,极容易被 OJ 的 1 s 时限卡住。再者,同样 5000 位数字,数组占 5000×4 B≈20 KB,乘法的中间数组更大,对高速缓存不友好。
在实际使用者中,若题目已声明“输入位数 ≤ 2000”且只牵涉加法或减法,使用一位一存完全能 AC;若涉及乘法、尤其是阶乘、快速幂等 n² 量级循环,则需要考虑更高效的“块存储”。
③ 升级一步:十进制“块”存储,为什么是 10⁴ 或 10⁹?
所谓块存储,就是把若干位十进制数字打包进一个元素。32 位平台常用 BASE=10 000
(4 位),因为 9999×9999
不会溢出 32 位有符号整型。64 位平台可提到 BASE=1 000 000 000
(9 位);999 999 999×999 999 999≈10¹⁸
仍落在 64 位无符号范围,但若要存进位之和就需要 long long
或加上额外 carry
。对比单独一位,这样做的优势肉眼可见:
ceil(5000/9)=556
块;循环立减 9 倍。BASE=10⁹
的朴素乘法可轻松处理一万位 × 一万位于 0.5 s 内,而单位存储会超时。但块也不是越大越好。BASE=10¹⁰
会让一次相乘结果溢出 64 位,需要手写 128 位拆分或使用 __int128
。竞赛常用 10⁹
正是折中的“黄金”——既压缩了位数,又能在支持 __int128
的 GNU C++14 下安全地乘完后取低 64 位和进位 64 位。本篇后续给出的数组模板也选用 BASE=10⁹
。
④ “大端”还是“小端”?倒序到底好在何处?
我们常见和常做的,就是以小端基础,倒序适应我们的四则运算。
① 把正负逻辑从核心运算里剥离
加、减、乘、除的本质都发生在“非负绝对值”之间。如果每一步都掺杂正负判断,会让代码分支膨胀、进位逻辑混乱。
② 避免“负零”
若 0 仍保留 s = -1
,那么两个“零”比较会出现“‐0 < +0”的假象。统一约定:只要值为 0,就把符号改回 +1。
③ 简化比较
先看符号即可粗判大小(正数必然大于负数),只有符号相同才需比较绝对值。
计算 12345 + 789 图解如下:
倒序输出即 12345 + 789 = 13134。
具体题目:只考虑同号,输入两个位数为 的正整数,求它们的和。
下面我们用字符串输入,转数组进行高精度加法:
#include
using namespace std;
char a1[305], b1[305];
int a[305], b[305], c[305]; //加法最多进一位
int main(){
cin >> a1 >> b1;
int la = strlen(a1);
int lb = strlen(b1);
for(int i = la-1; i >= 0; i--) a[la-1-i] = a1[i] - '0';
for(int i = lb-1; i >= 0; i--) b[lb-1-i] = b1[i] - '0';
//字符串转数组倒序保存
int lc = max(la, lb) + 1;
for(int i = 0; i < lc; i++){ //枚举各位相加
c[i] += a[i]+b[i]; //相加,这里建议相加和进位分开写,更清楚不易错
if(c[i] >= 10){ //需要进位
c[i] -= 10;
c[i+1]++;
}
}
while(c[lc] == 0 && lc > 0) lc--; //去前导零
for(int i = lc; i >= 0; i--){ //倒序输出
cout << c[i];
}
return 0;
}
只考虑同号,输入两个位数为 的正整数,求它们的差,但不确定被减数和减数的大小。
下面我们用字符串输入,转数组进行高精度减法:
#include
using namespace std;
const int MAXN = 305
string a, b;
int na[MAXN], nb[MAXN], ans[MAXN];
bool flag;
int main(){
cin >> a >> b;
if((a < b && a.size() == b.size()) || a.size() < b.size()){
swap(a, b);
flag = true;
}
for(int i = a.size(); i > 0; i--) na[i] = a[a.size() - i] - '0';
for(int i = b.size(); i > 0; i--) nb[i] = b[b.size() - i] - '0';
//字符串转整数数组
int maxl = max(a.size(), b.size());
//找到两个数中的最大位
for(int i = 1; i <= maxl; i++){
if(na[i] < nb[i]){
na[i + 1] --;
na[i] += 10;
}
ans[i] = na[i] - nb[i];
}
while(ans[maxl] == 0) maxl--; //去前导零
if(flag == true) cout << "-"; //b>a时,a - b < 0 所以打上负号
for(int i = maxl; i > 0; i--) cout << ans[i];
if(maxl < 1) cout << "0";
return 0;
}
只考虑同号,输入两个位数为 的正整数,求它们的乘积。
下面我们用字符串输入,转数组进行高精度乘法:
#include
using namespace std;
char a1[1005], b1[1005];
int a[2005], b[2005], c[2005];
int main(){
cin >> a1 >> b1;
int la = strlen(a1);
int lb = strlen(b1);
int x = 0;
for(int i = la-1; i >= 0; i--) a[x++] = a1[i] - '0';
x=0;
for(int i = lb-1; i >= 0; i--) b[x++] = b1[i] - '0';
//转整数数组
int lc = la + lb;
for(int i = 0; i < la; i++){ //两数相乘
for(int j = 0; j < lb; j++){
c[i+j] += a[i]*b[j]; //思考,如果最低位保存在下标 1 会怎么样
}
}
for(int i = 0; i < lc; i++){ //进位
c[i+1] += c[i]/10;
c[i] = c[i]%10;
}
while(c[lc]==0 && lc > 0) lc--; //去前导零
for(int i = lc; i >= 0; i--) cout << c[i];
return 0;
}
给定一个位数 大正整数和一个小正整数 ,大整数除以小整数得到的商和余数分别是多少?
string s; int k; //输入大整数 s(字符串)和小整数 k
cin >> s >> k;
int l = s.size(), a = 0, t = 0;
for (int i = 0; i < l; i++){
a = a*10 + s[i] - '0'; //未除尽的数到下一位 *10
if (a >= k){ //能除则输出值 和 计算未除尽的数
cout << a/k;
a %= k;
t = 1;
}
else if (t) cout << 0; //出现过商才会出现0,防止有前导零
}
cout << " " << a << endl;
功能 |
代码思路 |
|
逐位 |
快速幂 |
大数乘法 × 指数二分 |
阶乘 |
循环 |
运算 |
时间复杂度 |
空间 |
加/减 |
O(n) |
O(n) |
乘 |
O(n²) |
O(n) |
÷ 小整 |
O(n) |
O(n) |
÷ 大整 |
O(n²) |
O(n) |
sgn=1
。trim()
每次运算后都要调用,否则比较和打印会错。a*b/gcd
” 时先 a/gcd
再乘,防止溢出;这条在高精同样适用。 知识点 |
真题位置 |
建议练习 |
高精加法核心循环 |
2023-12 题 11 选择填空 |
手写 5000 位两数求和 |
进位处理 |
2025-03 题 15 选择填空 |
随机生成 1000 位×1000 位乘法 |
算法复杂度判断 |
2024-09 题 14 单选 |
对比 n² vs Karatsuba 的性能 |
完整编程 |
历年五级编程题经常给出“大数阶乘 / 大数相加” |
独立实现加、乘、阶乘 |
struct Nod{ // 单向链表结点
int v; // 数据域
Nod* n; // 指针域 (next)
Nod(int x = 0, Nod* p = nullptr) : v(x),n(p){}
};
说明
v
与一条指向下一结点的指针 n
。hd
可指向首结点;若链表为空,hd == nullptr
。void prn(Nod* hd){
for(Nod* p = hd; p; p = p->n) cout << p->v <<" ";
cout << "\n";
}
过程:让游标 p
从头开始,依次沿 n
前进,直到遇到 nullptr
为止。
Nod* crt(const vector& a){ // 传入数组生成链表
Nod* hd = nullptr;
for(int x : a) hd = new Nod(x, hd); // 新结点指向原头
return hd; // 返回新头
}
头插的特点:总 O(1) 时间把新元素压到最前面;生成顺序与原数组相反。
void pus(Nod*& hd,int x){
if(!hd){
hd = new Nod(x);
return;
} // 空链
Nod* p = hd;
while(p->n) p = p->n; // 找尾
p->n = new Nod(x);
}
尾插需先遍历到尾结点,因此最坏 O(n)。若频繁尾插,可另设尾指针 tl
优化为 O(1)。
x
的结点Nod* fnd(Nod* hd, int x){
for(Nod* p = hd; p; p = p->n)
if(p->v == x) return p;
return nullptr;
}
返回:指向匹配结点的指针,若不存在返回 nullptr
。
x
的结点之后插入 y
bool ins(Nod* hd, int x, int y){
Nod* p = fnd(hd, x);
if(!p) return false; // 未找到
p->n = new Nod(y, p->n);
return true;
}
步骤
fnd
找目标结点 p
。q
,其 n
指向 p->n
,然后把 p->n
改为 q
。x
的结点bool era(Nod*& hd, int x){
if(!hd) return false;
if(hd->v == x){ // 删除头结点
Nod* t = hd;
hd = hd->n;
delete t;
return true;
}
for(Nod* p = hd; p->n; p = p->n)
if(p->n->v == x){
Nod* t = p->n;
p->n = t->n;
delete t;
return true;
}
return false; // 未找到
}
void clr(Nod*& hd){
while(hd){
Nod* t = hd;
hd = hd->n;
delete t;
}
}
next
走到 nullptr
。new
创建结点后务必在删除或销毁时 delete
,否则泄漏。struct Nod{
int v; // 数据
Nod* n; // next 指针
Nod(int x = 0, Nod* p = nullptr) : v(x),n(p){}
};
void addH(Nod*& h, int x){
h = new Nod(x, h); // 新结点指向旧头
}
bool delV(Nod*& h, int x){
if(!h) return 0;
if(h->v == x){
Nod* t = h;
h = h->n;
delete t;
return 1;
}
for(Nod* p = h; p->n; p = p->n)
if(p->n->v == x){
Nod* t = p->n;
p->n = t->n;
delete t;
return 1;
}
return 0;
}
void prt(Nod* h){
for(Nod* p = h; p; p = p->n) cout << p->v <<" ";
cout << "\n";
}
时空复杂度:头插 O(1)
;删除/查找最坏 O(n)
;空间 O(n)
(结点指针域额外占用)。
struct Dnd{
int v;
Dnd* l; // prev
Dnd* r; // next
Dnd(int x=0) : v(x), l(nullptr), r(nullptr){}
};
p
之后放 q
q->r = p->r;
q->l = p;
if(p->r) p->r->l = q;
p->r = q;
p
if(p->l) p->l->r = p->r;
if(p->r) p->r->l = p->l;
delete p;
因为有 prev
指针,删除 不必再找前驱,常数时间完成;代价是每结点多 1 个指针,耗内存加倍。
last->n
指回头结点。for(Nod* p=h; p; p=p->n) { ... if(p->n==h) break; }
循环链表省去判空:只要持有 last
指针,就能 O(1) 头插尾插;但遍历时务必用“再次到达起点”作为停止条件。
在线评测常限制 new/delete
或追求极限速度,故常用 平行数组 取代指针。
const int N = 1000005; // 最大结点数
int val[N]; // 数据域
int nxt[N]; // next 下标
int cur = 1; // 可用的下标指针 [0] 预留作“头结点”
void ini(){
nxt[0] = -1; // 头结点指向实际首元
cur = 1; // 下标 1 起做新结点
}
void add(int idx,int x){ // idx 是已有结点下标
val[cur] = x;
nxt[cur] = nxt[idx];
nxt[idx] = cur;
++cur;
}
void rem(int idx){ // idx 前驱
if(nxt[idx] == -1) return;
nxt[idx] = nxt[nxt[idx]];
}
void out(){
for(int i = nxt[0]; i != -1; i = nxt[i]) cout << val[i] <<" ";
cout << "\n";
}
add
/ rem
时间 O(1)
,空间 O(N)
,且无需垃圾回收。 形式 |
插入删除 |
顺序遍历 |
随机定位 |
内存开销 |
单链表 |
O(1/ n ) |
O(n) |
不支持 |
1 指针 |
双向链表 |
O(1) |
O(n) |
前驱 O(1) |
2 指针 |
循环链表 |
O(1) |
需哨兵 |
不常用 |
同单/双 |
数组模拟链表 |
O(1) |
O(n) |
不支持 |
2 数组 |
实战建议
new/delete
单链表最快写完。