0x20 高精度算法
0x21 前言
高精度算法普遍使用的思路是 模拟竖式计算 的过程。两数计算时会分别倒序存入两个数组 (为了方便最高位的进位) ,一边进位一边计算。加减法的时间复杂度为 O ( n ) O(n) O(n) ,乘法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),除法的时间复杂度为 O ( n ) O(n) O(n)(高精度除以低精度)或 O ( n 3 ) O(n^3) O(n3)(高精度除以高精度)。高精度计算主要分为:
-
逆序存储
-
计算
-
最高位进位
-
去除前导零
各运算具体思路如下。
0x22 加法
0x22-1 加法(整数)
加法就是模拟竖式计算的过程,举一个例子,123+237=250,将123和127存进数组后,应该是这样的:
下标 | [1] | [2] | [3] | [4] |
---|---|---|---|---|
a | 3 | 2 | 1 | 0 |
b | 7 | 3 | 2 | 0 |
c | 0 | 6 | 3 | 0 |
3+7=10,需要进位,c[1]保留0,c[2]++ ;2+3+1=6,不用进位,直接放进c[2];1+2=3,不用进位,直接放进c[3]。最后将c数组倒序存入字符串ans,返回ans并输出。实现代码如下(注意,ans必须使用string类型,不然使用运算符会报错)。
string add(string a1,string b1) {
//预备需要使用的变量及数组
int lena=a1.size(),lenb=b1.size(),i,x=0;
int a[MAXN],b[MAXN],c[MAXN];
//倒序存入数组
for (i=0;i<lena;i++) a[lena-i]=a1[i]-'0';
for (i=0;i<lenb;i++) b[lenb-i]=b1[i]-'0';
//计算加法并进位
i=1;
while (i<=lena||i<=lenb) {
c[i]=a[i]+b[i]+x,x=c[i]/10;
c[i]%=10,i++;
}
//特判最高位是否需要进位
c[i]+=x;
//去除最高位前导零
if (c[i]==0) i--;
//转化为string字符串,方便下一步
string ans;
for (int j=1;j<=i;j++) ans+=c[i-j+1]+'0';
return ans;
}
0x21-2 加法(小数)
总体来说,与整数加法相似,但有以下几个难点:
- 小数后需补整数
- 十分位向个位进位需要特判
- 如果小数部分全为0,则删除小数点
解决这些问题,小数加法就解决了。
bool pd(string s) {
int len=s.size()-1;
while (len--)
if (s[len]=='.') return true;
return false;
}
int find_pit(string s,int len) {
int pit=0;
for (int i=len-1;i>=0;i--)
if (s[i]=='.') {
pit=len-i-1;
break;
}
return pit;
}
void turn_num(string s,int a[],int len) {
for (int i=len-1;i>=0;i--)
if (s[i]!='.') a[++a[0]]=(s[i]-'0');
}
string add(string a1,string b1) {
int a[MAXN]={},b[MAXN]={},c[MAXN]={},i,x=0;
int lena=a1.size(),lenb=b1.size();
int pit_a=0,pit_b=0,pit_c=0;
pit_a=find_pit(a1,lena);
pit_b=find_pit(b1,lenb);
pit_c=max(pit_a,pit_b);
b[0]=pit_c-pit_b,a[0]=pit_c-pit_a;
turn_num(a1,a,lena);
turn_num(b1,b,lenb);
i=1;
while (i<=a[0]||i<=b[0]) {
c[i]=(a[i]+b[i]+x),x=c[i]/10;
c[i]%=10,i++;
}
(x==0)? i-- : c[i]=x;
string ans;
for (int j=i;j>0;j--) {
if (j==pit_c) {
if (j==i) ans+='0';
ans+='.';
}
ans+=(c[j]+'0');
}
if (pd(ans)) while (ans[i]=='0') ans.erase(i--,1);
if (ans[i]=='.') ans.erase(i--,1);
return ans;
}
0x22 减法
减法与加法相似,但需要考虑 a<b 的情况,于是可以做一个特判:当 a<b 时,交换a,b的值,ans储存一个"-",表示结果为负数。代码如下所示(注意,a1和b1必须使用string类型,否则不能直接使用swap( )函数)。
string sub(string a1,string b1) {
//预备需要使用的变量及数组
int lena=a1.size(),lenb=b1.size(),i;
int a[MAXN],b[MAXN],c[MAXN];
string ans;
//特判是否为负数
if (lena<lenb || (lena==lenb && s1<s2)) swap(a1,b1),swap(lena,lenb),ans+="-";
//倒序存入数组
for (i=0;i<lena;i++) a[lena-i]=a1[i]-'0';
for (i=0;i<lenb;i++) b[lenb-i]=b1[i]-'0';
//计算加法并进位
i=1;
while (i<=lena||i<=lenb) {
if (a[i]<b[i]) a[i+1]--,a[i]+=10;
c[i]=a[i]-b[i];
i++;
}
//去除最高位前导零
while (c[i]==0 && i>1) i--;
//转化为string字符串,方便下一步
for (int j=1;j<=i;j++)
ans+=c[i-j+1]+'0';
return ans;
}
0x23 乘法
乘法的竖式涉及到错位相加,所以需要两层循环,分别用b的每个数位乘a的每个数位,举个例子:
a
[
3
]
a
[
2
]
a
[
1
]
∗
b
[
2
]
b
[
1
]
———————————
c
1
[
3
]
c
1
[
2
]
c
1
[
3
]
c
2
[
3
]
c
2
[
2
]
c
2
[
1
]
———————————
c
[
4
]
c
[
3
]
c
[
2
]
c
[
1
]
\begin{align} a[3]\quad a[2]\quad a[1]\\ *\qquad \qquad b[2]\quad b[1]\\ ———————————\\ c1[3]\quad c1[2]\quad c1[3]\\ c2[3]\quad c2[2]\quad c2[1]\qquad \quad \\ ———————————\\ c[4]\qquad c[3]\qquad c[2]\qquad c[1] \end{align}
a[3]a[2]a[1]∗b[2]b[1]———————————c1[3]c1[2]c1[3]c2[3]c2[2]c2[1]———————————c[4]c[3]c[2]c[1]
这是一个典型的多位数乘多位数的竖式。它的计算顺序大概是这样的:
-
计算a[i]*b[j];
-
将c[i+j-1]加上结果;
-
计算c[i+j-1]进的位数;
-
将c[i+j]加上c[i+j-1]进的位数。
所以,计算乘法的代码就呼之欲出了:
string mul(string a1,string b1) {
//同上
int lena=a1.size(),lenb=b1.size(),i,x;
int a[MAXN],b[MAXN],c[100*MAXN];
for (i=0;i<lena;i++) a[lena-i]=a1[i]-'0';
for (i=0;i<lenb;i++) b[lenb-i]=b1[i]-'0';
//计算乘法,i代表a的第i位,j代表b的第j位
for (i=1;i<=lena;i++) {
x=0;
for (int j=1;j<=lenb;j++) {
//计算每一位相乘的值
c[i+j-1]+=a[i]*b[j]+x;
//计算进位
x=c[i+j-1]/10;
//计算c[i+j-1]进位后剩下的部分
c[i+j-1]%=10;
}
//最高位加上进位
c[i+lenb]+=x;
}
i=lena+lenb;
//去前导零
while (c[i]==0 && i>1) i--;
//数组转string
string ans;
for (int j=i;j>0;j--)
ans+=char(c[j]+48);
return ans;
}
0x24 除法
(由于高精除高精的算法时间复杂度过高,所以添上了高精除低精)
0x24-1 高精度除以高精度
高精除以低精实际上是用减法去模拟除法,举个例子,114514÷11,计算过程如下:
首先,给除数11扩大10倍直到等于被除数的位数,接着开始减除数:
114514
−
110000
=
4514
114514-110000=4514
114514−110000=4514
由于只能减一个,所以商1。接着除数缩小10倍,继续减法,但被除数小于除数,商0。继续缩小,减法求商:
4514
−
1100
=
3414
3414
−
1100
=
2314
2314
−
1100
=
1214
1214
−
1100
=
114
\begin{align} 4514-1100=3414\\ 3414-1100=2314\\ 2314-1100=1214\\ 1214-1100=114 \end{align}
4514−1100=34143414−1100=23142314−1100=12141214−1100=114
商4余114,除数缩小,继续求商:
114
−
110
=
4
114-110=4
114−110=4
商1余4。这时,我们发现除数再去一个0还是大于余数,只能商一个0。那么,我们得出结果114514÷11=10410。思路已有,代码开始:
bool flag=false;
int lena,lenb,a[MAXN],b[MAXN],ans[MAXN];
bool pd() {
//判断被除数是否大于除数
string a1,b1;
for (int i=lena;i>=1;i--) a1+=a[i]+'0';
for (int i=lenb;i>=1;i--) b1+=b[i]+'0';
if (lena>lenb ||(lena==lenb && a1>=b1)) return true;
else return false;
}
void sub() {
//做减法
for (int i=1;i<=lena && i<=lenb;i++) {
if (a[i]<b[i]) a[i+1]--,a[i]+=10;
a[i]=a[i]-b[i];
}
while (a[lena]==0 && lena>1) lena--;
}
void wy(int f) {
//除数添0或去0
if (f==1) {
for (int i=lenb;i>=0;i--)
b[i+1]=b[i];
lenb++;
} else {
for (int i=2;i<=lenb;i++)
b[i-1]=b[i];
lenb--;
}
}
void div_high(string a1,string b1) {
//高精除高精的主体
lena=a1.size(),lenb=b1.size();
for (int i=0;i<lena;i++) a[lena-i]=a1[i]-'0';
for (int i=0;i<lenb;i++) b[lenb-i]=b1[i]-'0';
int tmp_lenb=lenb;
while (lena-lenb>0) wy(1);
while (lenb>=tmp_lenb) {
//num记录上
int num=0;
while (pd()) sub(),num++;
//去前导零
if (num>0) flag=true;
//商的存放
if (flag) ans[++ans[0]]=num;
wy(0);
}
//也可以在后面将商转为string类型
}
0x24-2 高精度除以低精度
高精除以低精就是模拟竖式除法的过程,过程如下:
-
计算上一位的余数除以除数的值(向下取整);
-
将余数保留至下一位。
但这样就会有个问题,有一堆前导零在前面站位,所以,就需要去前导零。代码如下:
string div_low(string a1,int b) {
long long lena=a1.size(),x=0;
int a[1010],c[1010];
for (int i=0;i<lena;i++) a[lena-i]=a1[i]-'0';
//除法主体
for (int i=lena;i>=1;i--) {
x=10*x+a[i];
c[i]=x/b;
x%=b;
}
//去前导零
while (c[lena]==0 && lena>1) lena--;
string ans;
for (int i=lena;i>=1;i--) ans+=c[i]+'0';
return ans;
}
0x25 模运算
根据上面的思路,最后余下来的数就是余数。所以,返回一个x即可(高精除高精就返回计算完商后的被除数)。
long long mod_low(string a1,int b) {
long long lena=a1.size(),x=0;
int a[1010],c[1010];
for (int i=0;i<lena;i++) a[lena-i]=a1[i]-'0';
for (int i=lena;i>=1;i--) {
x=10*x+a[i];
c[i]=x/b;
x%=b;
}
return x;
}
0x26 总结
高精度算法的思路比较简单明了,主要在于代码的细节程度,稍有不注意,就爆零了。
重庆计算机协会提醒您:高精须注意,爆零两行泪。