算法:高精度的加减乘除 <- o.0 d

91 阅读13分钟

高精度算法存在的意义

万物存在总归是有意义的,有需求催生了它的存在,那么不妨让我们来看看它为什么存在,而它究竟有什么用。

大家都明白我们经常用的数据类型如int、double、long long等能存储的数据大小都是有限的,比如int类型占用4个字节的长度,其能存储的数字范围是-2^31~2^31-1,约为21个亿。但是计算机在存储和处理数据时只会用到在这些规定长度之内的数据吗?肯定不是的,还有更大的更小的数据等待被处理,这时候就能用到高精度算法了。 67ED92D97FC3BEE13FEBE8BC90949971.jpg

高精度加法

例子

举个栗子.jpg 举一个栗子:有这么两个数:1111111111111111111111111111111111111111111111111111 1111111111111111111111111111111111111111111111111111,想要求他们的和,该如何去做呢? 如果用普通的方法的话,自然就是定义两个long long类型的变量求和,最后答案会是:

屏幕截图 2024-10-30 162907.png 在进行输出时,软件会提示“整数常量的值超出了其类型允许的范围”,答案自然是错的。这个时候就要用到高精度加法了。

高精度加法思路

对于高精度加法的原理,我们其实早就学过了,就是二年级学的竖式加减法

屏幕截图 2024-10-30 163726.png 原理就是这样,从个位数加到最高位数,满10就进1,最后得出结果。(在这里是随便举的例子,这么小的数用int就可以直接实现加减了)

实现步骤

既然我们要存储的数据超出了数据类型存储的范围,那么我们就需要转换一下思路,将其保存为字符串数组的形式

1.估计计算数字的长度和个数,创立相应的字符串数组,并利用scanf输入。例如

char s1[100010];
char s2[100010];
scanf("%s", s1);
scanf("%s", s2);

2.想要在数组内实现加减法,就要将字符串数组内的数据类型变换为int或其他一些类型,使得相应的数字进行加减进位。 因此创立两个int类型的数组。

int a[100010];
int b[100010];

3.随后将s1,s2内的数据转化为int类型的数据并存入a 和b数组。这个时候我们就需要用到循环了,那么要循环多少次呢?这就要利用strlen()函数获取s1和s2数组的长度了。

int la, lb;
	la = strlen(s1);
	lb = strlen(s2);
        for (int i = 0; i < la; i++)
		a[la - i - 1] = s1[i] - '0';//从a[la-1]赋值到a[0],个人习惯,如果想从a[1]开始赋值也可以。
	for (int i = 0; i < lb; i++)
		b[lb - i - 1] = s2[i] - '0';//利用数组数据减去'0'即可以获得数字,ASCII码原理。

有心的朋友们可能发现了,在s1,s2的数组中数据是正常顺序排放的,到了a,b数组成倒序了。例如:

屏幕截图 2024-10-30 165425.png 这样做的原因是方便我们从个位开始进行加减法,从而更好地进行进位。

4.此时我们就需要进行加减法并存储结果了,创立一个c数组,用来存储答案,其长度lc我们暂时运用a和b中长度最大的那一方的长度。

int c[10010];
int lc=max(a,b);

之后进行运算:

for (int i = 0; i < lc; i++) {
		c[i] += a[i] + b[i];
		c[i + 1] = c[i] / 10;
		c[i] = c[i] % 10;
	}
	if (c[lc] != 0)
		lc++;
	for (int i = lc - 1; i >= 0; i--) {
		printf("%d", c[i]);//输出答案
	}

屏幕截图 2024-10-30 170313.png

关于循环的实现

for (int i = 0; i < lc; i++) {
		c[i] += a[i] + b[i];//a[i]+b[i]获取到结果
		c[i + 1] = c[i] / 10;//利用c[i]/10来判断和执行是否满十进一
		c[i] = c[i] % 10;//最后c[i]=c[i]/10,来使得此位的结果保证在0~9以内
	}
	

由于c[]数组定义在函数外,所以所有值默认为0,只在进位时会改变。比如上面的例子中,a[0]+b[0]=10,

执行到第一句时:c[0]=c[0]+a[0]+b[0],也就是c[0]=0+4+6=10;

第二句:c[1]=1;

第三句:c[0]=10%10=0;

就此,c[0]=0,c[1]=1;

之后c[1]进入循环:c[1]=c[1]+a[1]+b[1]=1+5+4=10。。。。。 就此类推,我们正确地得到了每一个值。

关于lc长度的问题

为什么我们要取a,b中长度大的作为c的长度呢?

849907FD83842BBAA50BC023E5C9CFD5.jpg 其实你自己可以去计算机随便输入几个数来看看,两个数的和,其长度要么和加数中位数多的一样,要么比它多一位,比如100+90=190,和是3位数,100+900=1000,和是四位数。所以先取了加数其中较大的一位作为其长度。最后当我们循环完毕得到c时还有个if条件的判断

if (c[lc] != 0)
		lc++;

这一段内容就是在判断加和后有没有进位,如果加和后有进位,就像100+900=1000,变为了四位数,这个时候就需要使得lc++,不然1000的1录入不进去,最后循环得出来的答案也就错了。

源码

#include <bits/stdc++.h>  //高精度加法
using namespace std;
char s1[100010];
char s2[100010];
int a[100010];
int b[100010];
int c[100010];

int main() {

	scanf("%s", s1);
	scanf("%s", s2);
	int la, lb;
	la = strlen(s1);
	lb = strlen(s2);
	for (int i = 0; i < la; i++)
		a[la - i - 1] = s1[i] - '0';
	for (int i = 0; i < lb; i++)
		b[lb - i - 1] = s2[i] - '0';
	int lc = max(la, lb);
	for (int i = 0; i < lc; i++) {
		c[i] += a[i] + b[i];
		c[i + 1] = c[i] / 10;
		c[i] = c[i] % 10;
	}
	if (c[lc] != 0)
		lc++;
	for (int i = lc - 1; i >= 0; i--) {
		printf("%d", c[i]);
	}



}

高精度减法

接下来我们来看一看高精度减法,其原理和加法差不多。 先看这组例子:s1[]={3,7,5,5,4},s2[]={3,7,2,1,5}。

屏幕截图 2024-11-11 144136.png

在进行高精度减法时,我们只需要将两组数字按照加法的运算过程一样放入两个int数组然后进行相减即可。 以下为详细实现过程:

#include <bits/stdc++.h> //高精度减法
using namespace std;
    char s1[100010];
    char s2[100010];
    char s3[100010];
    int a[100010];
    int b[100010];
    int c[100010];
    int flag;

int main(){

    scanf("%s",s1);
    scanf("%s",s2);
    int la,lb;
    la=strlen(s1);
    lb=strlen(s2);
    for(int i=0;i<la;i++)
    a[la-i-1]=s1[i]-'0';
    for(int i=0;i<lb;i++)
    b[lb-i-1]=s2[i]-'0';
    int lc=max(la,lb);
    for(int i=0;i<lc;i++)  //这里才是重点,即借位相减。其他与加法别无二致。
    {   if(a[i]<b[i]){
           a[i+1]--;
           a[i]=a[i]+10;
        }
        c[i]=a[i]-b[i];

    }
    for(int i=lc-1;i>=0;i--){
        printf("%d",c[i]);
    }


}

相信你能够看懂for循环里的内容,这样的话,高精度减法你就掌握了90%了!

当然这只是基础的实现方法。在实际使用时会出现各种各样的问题,下面我们来完善一下吧!

相减后出现“0”问题

拿上面的例子来说,

屏幕截图 2024-11-11 144231.png 在我们进行运算结束后并不能正确的得到结果,如果用上面的代码进行运算输出,会得到“00339”,这是因为数组后面的数相减为0而我们没有去掉的问题。此时就需要用循环去掉多余的0。

while(c[lc-1]==0&&lc>1) lc--;//保证lc>1是为了保证有结果,对应的是得到的答案为0的情况,如果
//去除lc>1,那么当两数相减为0时,输出就会出错。

小数减去大数

在减法运算中,我们需要注意是大数减去小数还是小数减去大数。这里与加法运算不同,我们需要加一个判断的函数,使得大的数亦或是绝对值较大的数在前,减去小的数。

bool compare(char s1[], char s2[]) {   //如果s1的数>=s2的数,则为true,否则为false,并使得两数换位。
    int u = strlen(s1);
    int v = strlen(s2);

    if (u > v) return true;      //首先判断数组长度,数组长度长的数字肯定大(前提是两个数都为正整数)
    if (u < v) return false;


    for (int i = 0; i < u; i++) {       //两数长度相等时进行下一步判断,从高位往低位逐位比较。
        if (s1[i] > s2[i]) return true;
        if (s1[i] < s2[i]) return false;
    }


    return true;  //走到这一步说明两数相等
}

减法只需要注意这两个地方就可以了。

源码

#include <bits/stdc++.h> //高精度减法
using namespace std;
    char s1[100010];
    char s2[100010];
    char s3[100010];
    int a[100010];
    int b[100010];
    int c[100010];
    int flag;
bool compare(char s1[], char s2[]) {
    int u = strlen(s1);
    int v = strlen(s2);

    if (u > v) return true;
    if (u < v) return false;


    for (int i = 0; i < u; i++) {
        if (s1[i] > s2[i]) return true;
        if (s1[i] < s2[i]) return false;
    }


    return true;
}

int main(){

    scanf("%s",s1);
    scanf("%s",s2);
    int la,lb;

    if(!compare(s1,s2)){
        flag=1;         //利用flag决定是否输出结果要加负号,
        strcpy(s3,s1); 
        strcpy(s1,s2);
        strcpy(s2,s3);   //利用strcpy函数交换数组内容
    }
    la=strlen(s1);
    lb=strlen(s2);
    for(int i=0;i<la;i++)
    a[la-i-1]=s1[i]-'0';
    for(int i=0;i<lb;i++)
    b[lb-i-1]=s2[i]-'0';
    int lc=max(la,lb);
    for(int i=0;i<lc;i++)
    {   if(a[i]<b[i]){
           a[i+1]--;
           a[i]=a[i]+10;
        }
        c[i]=a[i]-b[i];

    }
    while(c[lc-1]==0&&lc>1) lc--;
    if(flag==1) printf("-");  //flag决定是否加负号
    for(int i=lc-1;i>=0;i--){
        printf("%d",c[i]);   //从最高位逐位输出
    }


}

高精度乘法

怎么说呢,这里没什么好讲的,和加法一样的思路。只需要注意一点就是结果长度取值的规律67ED92D97FC3BEE13FEBE8BC90949971.jpg

源码

#include <bits/stdc++.h>  //高精度乘法
using namespace std;
char s1[100010];
char s2[100010];
int a[100010];
int b[100010];
int c[100010];

int main() {

	scanf("%s", s1);
	scanf("%s", s2);
	int la, lb;
	la = strlen(s1);
	lb = strlen(s2);
	for (int i = 0; i < la; i++)
		a[la - i - 1] = s1[i] - '0';
	for (int i = 0; i < lb; i++)
		b[lb - i - 1] = s2[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];
			c[i + j + 1] += c[i + j] / 10;
			c[i + j] = c[i + j] % 10;
		}
	while (c[lc - 1] == 0 && lc > 1) //lc>1防止答案为0被误删的情况
		lc--;
	for (int i = lc - 1; i >= 0; i--) {
		printf("%d", c[i]);
	}


}

for循环解析

for (int i = 0; i < la; i++)
		for (int j = 0; j < lb; j++) {
			c[i + j] += a[i] * b[j];
			c[i + j + 1] += c[i + j] / 10;
			c[i + j] = c[i + j] % 10;
		}

利用双重循环进行竖式相乘(和正常竖式相乘过程一模一样),每相乘一次就将结果保存在c数组内,直到循环结束即得出答案。

例子

屏幕截图 2024-11-11 154000.png

屏幕截图 2024-11-11 154214.png

第1次循环后:(i=0,j=0 => i=0,j=1)

屏幕截图 2024-11-11 155620.png

第2次循环后:(i=1,j=0 => i=1,j=1)

屏幕截图 2024-11-11 155659.png

第3次循环后:(i=2,j=0 => i=2,j=1)

屏幕截图 2024-11-11 155717.png

高精度除法

OKOK!终于到我们的除法了,除法还是有点不太好理解的。让我们开始吧!

0B58D23FE80F9FC16EADAFDF0DC25218.jpg 看下面一个例子:

屏幕截图 2024-11-11 165230.png

我们都知道做除法需要被除数和除数之外,还有一个余下的值,因此我们在定义变量的时候就需要多出来一个数来表示余数。于是有:

#include <bits/stdc++.h>  //高精度除法
using namespace std;
char s1[100010];   //接受被除数
long long int a[100010];  //a[],c[]表示被除数和答案
long long int c[100010];
long long int la, lc;   //表示a与c的长度
long long int x, b;  // x表示余数

int main() {

	scanf("%s", s1);
	scanf("%lld", &b);
	la = strlen(s1);
	for (int i = 0; i < la; i++) //从a[0]开始赋值,与之前的都不一样,除法是从高位到低位逐渐进行
		a[i] = s1[i] - '0';
	
}

到此为止将s1的字符转换成了数字。 那么该如何进行操作呢?

先使得被除数/除数,也就是a[i]/b,得到答案c[i],以及余数x,x=a[i]%b,再进行下一步操作时就是 c[i]=(a[i]+x10)/b, x=(a[i]+x10)%b, 对应例子中就是得到了c[0]=1,x=2,再下一步是c[1]=5,x=1.... 所以这一段应该是这样:

for (int i = 0; i < la; i++) {  //i<la:结果的长度一定不会大于la.
		c[i] = (x * 10 + a[i]) / b; //最初x=0,不会影响第一次的循环
		x = (x * 10 + a[i]) % b;
	}

每一次循环都更新c[i]的值以及x的值,进行除法时都要将x*10,就像竖式的除法一模一样。

“0”的问题

例子:111/8,我们都明白这个例子在第一位的商为0,在代码中的实现为:1/8==0.125,计算结果为0.125,而我们用的是int类型,所以这个数值为0,c[0]=0,结果实际为c[]={0,1,3},x=7, 而我们输出时要特别注意,不要把0输出,这时候就需要用一个while循环来去掉0。

int lc=1; 
//与下面相匹配,使得lc代表c的长度,la代表a的长度,也可以令其为0,这样的话下面循环条件变为
//c[lc]==0 && lc<la-1  输出的循环条件 i变为 i=lc
while (c[lc - 1] == 0 && lc < la) //lc一定要小于la,最少保留一位数字进行输出
		lc++; //一直递增找到不为0的数字
	for (int i = lc - 1; i <= la - 1; ++i) {
		printf("%lld", c[i]); //从不为0的数字开始输出
	}

总结

这一段就是这样了,高精度不算太难,只要明白了二年级的竖式加减乘除就完全可以看懂,只需要注意一下关于“0”的细节,以及处置多余0的技巧就好了。如有错误,欢迎指出!