【学习笔记】(一)高精度算法

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]
a3210
b7320
c0630

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]
这是一个典型的多位数乘多位数的竖式。它的计算顺序大概是这样的:

  1. 计算a[i]*b[j];

  2. 将c[i+j-1]加上结果;

  3. 计算c[i+j-1]进的位数;

  4. 将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 114514110000=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} 45141100=341434141100=231423141100=121412141100=114
商4余114,除数缩小,继续求商:
114 − 110 = 4 114-110=4 114110=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 高精度除以低精度

高精除以低精就是模拟竖式除法的过程,过程如下:

  1. 计算上一位的余数除以除数的值(向下取整);

  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 总结

高精度算法的思路比较简单明了,主要在于代码的细节程度,稍有不注意,就爆零了。

重庆计算机协会提醒您:高精须注意,爆零两行泪。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值