高精度运算
本节概要
高精度运算涉及高精度加法,高精度减法,高精度乘法,高精度除低精度,高精度除高精度 5类。以下的讲解中,不考虑负数的情况;除法运算中,我们规定除数小于被除数;规定高精度数字的位数不超过200。
- 本节内容
- 高精度数字的输入和存储方法
- 高精度加法
- 高精度减法
- 高精度乘法
- 高精度除低精度
- 高精度除高精度
- 总结
1. 高精度数字的输入和存储方法
输入
单个高精度数字可能长达200位,可以通过两种方式读入:1. 单字符读取、判断并存储,直到遇到“\n”(换行符),可以使用scanf("%c", &c)
,或c=getchar()
这种方法麻烦;2. 将数字作为字符串读入,存入字符数组中,这种方法最简单、高效:scanf("%s", temp);
存储
字符到数字:使用字符数组存储容易,但计算比较麻烦,因数字在字符数组中是以字符0~9的形式存在(ASCII码范围:48~57),将所有数位以数字0~9的形式转存在一个int数组中会方便运算。N=='N'-'0'
可以实现字符N到数字N的转换。
反序存放:使用竖式做加法运算时,规则是最低位和最低位对齐,在int数组中如果将数字正序存放,对应计算位的下标就不一样,不便于计算,因此我们将数字反序存放在数组中,就可以使得低位和低位一一对应(下标相同),方便运算,如12345+9987:
//array index 1 2 3 4 5 6 7 8 9
5 4 3 2 1
+ 7 8 9 9
-------------
2 3 3 2 2
//jw 1 1 1 1
//array index是数组下标,jw代表进位
存储位数: 上面的例子并没有使用数组的0号单元,这主要是为了方便运算的缘故,例如n位数字,我们从1循环至n即可,这样思路更加清晰。而留下的0号单元刚好可以用来存储数组的长度,长度Array[0]=strlen(temp);
代码示例
void getNum(int num[]){ //数组初始化和高精度数字构建
char temp[500];
scanf("%s", temp); //数字作为字符串读入
memset(num, 0, sizeof(int)*500);//将数组的所有字节置0
num[0]=strlen(temp); //将位数存于num[0]中
for(int i=1; i<=num[0]; i++) //逆序将每一位存储于num中
num[i]=temp[num[0]-i]-'0';
return;
}
//使用方法
int main(){
int num1[500];
int num2[500];
getNum(num1);
getNum(num2);
......
return 0;
}
上面的代码中用到了
memset()
,该函数位于cstring头文件中,用于内存块的赋值,在例子中起到了初始化数组的作用
参数1是内存块的起始地址, num是数组的名字,即起始地址
参数2是初始化值,范围是0~255,因memset是按字节为单位进行的
参数3是初始化的字节数,sizeof(int)计算出int的字节数,乘以数组大小500
这里若是没有用
memset
进行内存块初始化会导致运算出错:如果参与计算的数字A和数字B位数不同,高位相加时,小数字的高位空间的值具有不确定性,如31415+12:
//array index 1 2 3 4 5 6 7 8 9 10 ...
A 5 1 4 1 3
B 2 1 ? ? ?
------------------------
C 7 2 ? ? ?
//jw 0 0 ? ? ?
可见,数组在使用前全部置0可以避免这个问题,你也可以用循环来解决,当然使用memset更加方便。
2. 高精度加法
思路
按位相加:从两个数A、B的低位开始按位相加,一边加一边进位,C[i]=A[i]+B[i]+jw
,进位是上一次相加产生的。
代码示例
void jiafa(int A[], int B[], int C[]){ //C=A+B
int i, jw; //i从1循环至max(A[0], B[0])
for(i=1, jw=0; i<=A[0] || i<=B[0]; i++){
C[i]=A[i]+B[i]+jw; //按位相加
jw=C[i]/10; //求进位
C[i]%=10; //与10求余
}
C[i]=jw; //存放最后一个进位
C[0]=C[i] ? i : i-1; //确定C的位数
return;
}
//使用方法
int main(){
int num1[500];
int num2[500];
int result[500];
getNum(num1);
getNum(num2);
memset(result, 0, sizeof(result)); //在当前函数中才可以如此计算大小
jiafa(num1, num2, result);
printNum(result); //后面提供函数代码
return 0;
}
3. 高精度减法
思路
大小判断:相减之前要判断两个数A、B的大小关系,如果A>B,则计算A-B,否则计算B-A,需要编程实现大小判断。
按位相减:从两个数A、B的低位开始按位相减,如果L[i]<R[i]
,就需要向高位借位,因为我们已经确定了左数字L大于右数字R,一定能借到。 if(L[i]<R[i]){ C[i]=L[i]+10-R[i]; L[i+1]--;}
确定有效位:按位减完后,高位有可能变为0,如1000-990后等于10,这时就要重新计算确定结果的有效位数C[0]
是多少。可以从位置max(L[0], R[0]
开始倒推,直至遇到非0值停止,这时的位置就是有效位数。
代码示例
bool compare(int L[], int R[]){ //实现两个高精度数字的大小比较
if(L[0]>R[0]) return true; //位数高的大
if(L[0]<R[0]) return false; //位数低的小
int i;
for(i=L[0]; i>=1; i--){ //位数相同逐位比较
if(L[i]>R[i]) return true; //数字大的大
if(L[i]<R[i]) return false; //数字小的小
}
return true; //能执行到这里说明相等,返回true
}
void jianfa(int L[], int R[], int C[]){ //C=L-R
for(int i=1; i<=L[0]; i++)
C[i]=L[i]; //把L的所有数位复制到C
C[0]=L[0]; //C的位数暂等于L的位数
int i;
for(i=1; i<=R[0]; i++){ //右值有多少位,就减多少次
if(C[i]<R[i]){
C[i+1]--; //向高位借1
C[i]+=10; //当前位+10
}
C[i]-=B[i]; //按位减
}
if(i<C[0]) return; //计算有效位数:L[0]-R[0]>=2
else{ //计算有效位数:L[0]-R[0]==1 or 0
while(C[i]==0 && i>=1) i--;
A[0]=(i==0) ? 1 : i; //例如10000-9999
}
return;
}
//使用方法
int main(){
int num1[500];
int num2[500];
int result[500];
getNum(num1);
getNum(num2);
memset(result, 0, sizeof(result));
if(compare(num1, num2)) jianfa(num1, num2, result);
else jianfa(num2, num1, result);
printNum(result);
return 0;
}
4. 高精度乘法
思路
类似加法,可以用竖式求乘法,同样也有进位,对每一位进行乘法运算时,必须进行错位相加:如856×25,见如下规律:
//array index 1 2 3 4 5 6 7 8 9 ...
A 6 5 8
B 5 2
----------------------------------
0 8 2 4
2 1 7 1
----------------------------------
0 0 4 1 2
//array index 1 2 3 4 5 6 7 8 9 ...
A a1 a2 a3 a4
B b1 b2
----------------------------------
C c1 c2 c3 c4
c2 c3 c4 c5
//本例中未写出进位
综上分析,乘法要进行B[0]
轮,每轮要进行A[0]
次乘法和进位运算,这就找到了循环的规律。结合错位相加,可知C[i+j-1]=C[i+j-1]+A[i]*B[i]+jw
,进位jw=c[i+j-1]/10
有效位数:计算完成后,需要确定结果的有效位数,使用和减法类似的方法从可能的最大位数往后推,直到遇到非0的数位,当前位置就是有效位数。对于M位的数字A和N位的数字B向乘,最大位数为M+N
。
代码示例
void chengfa(int A[], int B[], int C[]){ //C=A*B
int i,j;
for(i=1; i<=B[0]; i++){ //B[0]轮乘法
int jw; //进位
for(j=1, jw=0; j<=A[0]; j++){ //每轮按位乘A[0]次
C[i+j-1]=C[i+j-1]+A[j]*B[i]+jw; //错位相加
jw=C[i+j-1]/10; //求进位
C[i+j-1]%=10; //进位后,与10求余
}
C[i+A[0]]=jw; //存储最后一个进位
}
int len=A[0]+B[0]; //最大可能位数
while(C[len]==0 && len>1) len--; //确定有效位数
C[0]=len;
return;
}
//使用方法
int main(){
...
chengfa(num1, num2, result);
printNum(result);
return 0;
}
5. 高精度除低精度
思路
高精度初低精度采用竖式思想,由于除数是低精度,最后的商可能是高精度,余数一定为低精度。
商有效位数:M位的数字A除以N位的数字B,商的位数最大为M-N+1
,因为B是低精度数字,计算长度N比较麻烦,简单起见,也可以认为商的最大位数为M
, 从最大数位往后推,直到遇到非0的数位,当前位置就是有效位数。
以下是一个模拟计算过程的例子,113056/23:
//
被除数数位 当前位数字 过程被除数 过程除结果
i=6 1 1/23 商:0
i=5 1 11/23 商:0
i=4 3 113/23 商:4,余:21
i=3 0 210/23 商:9,余:3
i=2 5 35/23 商:1,余:12
i=1 6 126/23 商:5,余:11
//计算结果 商:4915, 余数:11
代码示例
void chuDi(int A[], int B, int C[], int &yushu){ //A/B=C 余数:yushu
int i, t; //t为过程被除数
for(i=A[0], t=0; i>=1; i--){ //从被除数高位循环到低位
t=t*10+A[i]; //更新t
C[i]=t/B; //计算C[i]
t%=B; //更新t
}
yushu=t; //计算完后t就是余数
i=A[0]; //计算C有效位数
while(C[i]==0 && i>1) i--;
C[0]=i;
return;
}
//使用方法
int main(){
int num1[500];
int num2;
int shang[500];
int yushu;
getNum(num1);
scanf("%d", num2);
chuDi(num1, num2, shang, yushu);
printNum(shang);
count<<" "<<yushu;
return 0;
}
6. 高精度除高精度
思路
高精度除高精度是这几种运算中最难的一种。仍然可以模仿竖式的方式进行,过程有点儿麻烦。另一种办法是利用减法:被除数A-除数B,看看能减多少个,直到A<B
,这时剪去的个数就是商,剩下的A就是余数。如果利用前面编写好的高精度减法jianfa()
和compare()
的话,实现起来岂不是很容易?可惜不是这样,假设被除数A的位数M与除数B位数N之差M-N
很大,这个范围有可能从0~200,按照最大的情况200考虑,这意味着商值也是高精度数字,而你要减10^200次才能减完,这是一个天文数字。
所以,明白否?解决这个问题也很简单,我们不一个一个减,而是按照最大可能的数量级减,例如:12345/45:
//商的最大位数i=M-N+1,即4,设计一个 临时减数,减数后面补齐i-1个0,再进行减法
i=4 12345 < 45000 可以减0个 shang[4]=0 减后A:12345
i=3 12345 < 4500 可以减2个 shang[3]=2 减后A:3345
i=2 3345 < 450 可以减7个 shang[2]=7 减后A:195
i=1 195 < 45 可以减4个 shang[1]=4 减后A:15
//因shang[4]=0,故商的有效位数shang[0]--,为3
//结果 商为274,余数15
看明白了吗,这样的计算过程,仅仅减了2+7+4=13
次,而非274
次,可见一个一个减是绝对不靠谱的。下面提供了代码,注意:减法函数jianfa()
被修改了,使得直接把结果存入A中,而不需要存在另一个数组C中,这是为了简化后面运算的缘故。
代码示例
void jianfa(int A[], int B[]){ //修改了的减法,使得直接在A数组上减
int i; //不再需要存在数组C,简化后面的运算
for(i=1; i<=B[0]; i++){
if(A[i]<B[i]){
A[i+1]--; //向高位借1
A[i]+=10; //当前位+10
}
A[i]-=B[i]; //按位减
}
if(i<A[0]) return; //计算有效位数
else{
while(A[i]==0 && i>=1) i--;
A[0]=(i==0)? 1 : i;
}
return;
}
//shang=A/B, yushu为余数
bool chuGao(int A[], int B[], int shang[], int yushu[]){
memset(shang, 0, sizeof(int)*300); //初始化shang
memset(yushu, 0, sizeof(int)*300); //初始化yushu
shang[0]=A[0]-B[0]+1; //商的最大数位
for(int i=shang[0]; i>=1; i--){ //从高位到低位开始计算商
int jianshu[300]; //构造要减的减数
memset(jianshu, 0, sizeof(jianshu));
//这个函数下面有解释
memcpy(jianshu+i, B+1, sizeof(int)*B[0]);
jianshu[0]=B[0]+i-1; //确定减数的位数
while(compare(A, jianshu)){ //通过循环减
jian(A, jianshu);
shang[i]++; //减去一个商的对应为+1
}
}
if(shang[shang[0]]==0) shang[0]--; //判断商的最高位是否有效
memcpy(yushu, A, sizeof(int)*300); //A就是余数,把它完全复制给yushu
}
//使用方法
int main(){
int num1[300]; //数组A存储第1个数字信息
int num2[300]; //数组B存储第2个数字信息
int shang[300];
int yushu[300];
init(num1);
init(num2);
chuGao(num1, num2, shang, yushu);
print(shang);
printf(" ");
print(yushu);
return 0;
}
上面的代码中用到了memcpy(),该函数位于cstring头文件中,用于内存块之间的复制
参数1是destination,即复制的目的地,为一地址
参数2是source,即复制数据的来源,为一地址
参数3是要复制的字节数,sizeof(int)计算出int的字节数,乘以相应的元素个数N例程中的
memcpy(jianshu+i, B+1, sizeof(int)*B[0])
是从B数组的1号位置开始(跳过B[0]),复制B[0]
个数位,目的地为jianshu
数组的第jianshu+1+i-1
个位置,+1
是为了跳过jianshu[0]
,+i-1
是把这几个位置空成0,以构造最大数量级的减数。晕了么有?别怕,其实可以把代码写的长一些,但是更简单一些的,这里是偷懒了;-)
7. 总结
用途
高精度运算在信息学奥赛计算中常常出现,因此必须掌握。例如计算大型斐波那契数列、计算高阶次幂、计算N!
等,如果不使用高精度进行计算,内存溢出是必然结果。
最后提供print()函数,十分简单,你应该可以自己写出来:-)
代码示例
void print(int X[]){ //输出高精度数字
for(int i=X[0]; i>=1; i--) printf("%d", X[i]);
return;
}