《算法笔记》学习日记——5.6 大整数运算

目录

  • 5.6 大整数运算
    • 问题 A: a+b
    • 问题 B: N的阶乘
    • 问题 C: 浮点数加法
    • 问题 D: 进制转换
    • 问题 E: 大整数排序
    • 问题 F: 10进制 VS 2进制
    • 小结

5.6 大整数运算

Codeup Contest ID:100000593

问题 A: a+b

题目描述
实现一个加法器,使其能够输出a+b的值。
输入
输入包括两个数a和b,其中a和b的位数不超过1000位。
输出
可能有多组测试数据,对于每组数据,
输出a+b的值。
样例输入

6 8
2000000000 30000000000000000000

样例输出

14
30000000002000000000

思路
这题就是很典型的一个大整数的加法运算,和书上一样,定义大整数类型的结构体bign,定义一个字符串转bign类型的函数,再定义一个add函数,然后从低位开始计算。
因为这里最大要一千位,为了保险起见,建议把所有数组都开成1001,以免出现各种不必要的麻烦。
代码

#include
#include
#include
#include
#include
#include
using namespace std;
struct bign{
	int d[1001];
	int len;
	bign(){//构造函数 
		memset(d, 0, sizeof(d));
		len = 0;
	}
};
bign change(char str[]){
	bign a;
	a.len = strlen(str);
	for(int i=a.len-1, j=0;i>=0, j<a.len;i--, j++) a.d[j] = str[i]-'0';
	return a;
}
bign add(bign a, bign b){
	bign c;
	int carry = 0;
	for(int i=0;i<a.len||i<b.len;i++){
		int temp = a.d[i]+b.d[i]+carry;
		c.d[c.len++] = temp%10;
		carry = temp/10;
	}
	if(carry!=0) c.d[c.len++] = carry;
	return c;
}
char str1[1001], str2[1001];//这里要多1位,最后要存'\0' 
int main(){
	while(scanf("%s %s", str1, str2) != EOF){
		bign a = change(str1);
		bign b = change(str2);
		bign c = add(a, b);
		for(int i=c.len-1;i>=0;i--) printf("%d", c.d[i]);
		printf("\n");
		memset(str1, 0, sizeof(str1));
		memset(str2, 0, sizeof(str2));
	}
	return 0;
}

问题 B: N的阶乘

题目描述
输入一个正整数N,输出N的阶乘。
输入
正整数N(0<=N<=1000)
输出
输入可能包括多组数据,对于每一组输入数据,输出N的阶乘
样例输入

0
4
7

样例输出

1
24
5040

思路
首先这题肯定也是大整数类的题目,因为1000的阶乘计算器算出来是带10的几次方的,而题目的要求显然是全部输出。我的思路是先把大整数的乘法函数写出来,然后再用循环函数计算阶乘即可。
但是要注意,1000的阶乘很大很大,用win10的计算器算出来约等于4×102567,因此,大整数的数组d开1000位是不够的,至少2568位。
《算法笔记》学习日记——5.6 大整数运算_第1张图片
代码

#include
#include
#include
#include
#include
#include
using namespace std;
struct bign{
	int d[2568];
	int len;
	bign(){
		memset(d, 0, sizeof(d));
		len = 0;
	}
};
bign multi(bign a, int b){
	bign c;
	int carry = 0;
	for(int i=0;i<a.len;i++){
		int temp = a.d[i]*b+carry;
		c.d[c.len++] = temp%10;
		carry = temp/10;
	}
	while(carry!=0){
		c.d[c.len++] = carry%10;
		carry /= 10;
	}
	return c;
}
bign f(int n){
	bign a;
	a.len = 1;
	a.d[0] = 1;
	if(n==0) return a;
	else{
		for(int i=1;i<=n;i++) a = multi(a, i);
		return a;
	}
}
int main(){
	int n;
	while(scanf("%d", &n) != EOF){
		bign x = f(n);
		for(int i=x.len-1;i>=0;i--) printf("%d", x.d[i]);
		printf("\n");
	}
	return 0;
}

问题 C: 浮点数加法

题目描述
求2个浮点数相加的和
题目中输入输出中出现浮点数都有如下的形式:
P1P2…Pi.Q1Q2…Qj
对于整数部分,P1P2…Pi是一个非负整数
对于小数部分,Qj不等于0
输入
对于每组案例,第1行是测试数据的组数n,每组测试数据占2行,分别是两个加数。
每组测试数据之间有一个空行,每行数据不超过100个字符
输出
每组案例是n行,每组测试数据有一行输出是相应的和。
输出保证一定是一个小数部分不为0的浮点数
样例输入

2
3.756
90.564

4543.5435
43.25

样例输出

94.32
4586.7935

思路
本题的思路和大整数的加法一样,重点在于如何解决小数点对齐的问题。

我这里改进了一下原来的大整数类型的结构体定义,分为整数部分和小数部分,然后用lend和lenf分别记录整数部分的长度和小数部分的长度。因此,在change函数读入字符串的时候,遇到小数点要跳过(其他部分差不多,也是逆序存储),在add函数里要先分情况给小数部分补0,如果某个数的小数部分长度较小,则把它的小数部分末尾一直添0,直到其lenf和那个数的lenf一样。

另外在实现加法的时候,方法还是一样,但是这里是先从小数部分加起,并且小数部分加完之后,不用先处理进位carry的问题,因为小数部分的进位肯定是加到整数部分的,所以代码不用改,再进行一遍整数部分的加法,等把整数部分的加法也完成了之后,再处理进位的问题。

这题总的来说思路简单,实现复杂,我因为一些细节上的问题也卡了很久。

这里总结一下本题的坑点:①小数部分的0是否输出的问题,一开始我的输出的算法是遇到0则跳过,这样就造成了如果是0.605则会输出0.65的问题,于是需要从低位开始遍历,记录第一个非0数的位置tempi,然后再从高位一直输出到tempi的位置即可;②函数里定义的暂存字符数组tmp一定要定义完之后初始化,要写成char tmp[150]={0}的形式,而不是char tmp[150]就完事了,这里我卡了很久很久,直到后来单步调试的时候才发现输入数据的顺序会影响tmp数组里的内容,进而导致后面补0的处理结果出错。
代码

#include
#include
#include
#include
#include
#include
using namespace std;
struct bign{
	int d[150], f[150];//d是整数部分,f是小数部分 
	int lend, lenf;
	bign(){
		memset(d, 0, sizeof(d));
		memset(f, 0, sizeof(f));
		lend = lenf = 0;
	}
};
bign change(char str[]){
	bign a;
	int doti = 0;
	for(int i=strlen(str)-1;i>=0;i--){
		if(str[i]=='.'){
			doti = i;//记录小数点的位置 
			break;
		}
		else a.f[a.lenf++] = str[i]-'0';
	}
	for(int i=doti-1;i>=0;i--) a.d[a.lend++] = str[i]-'0';
	return a;
}
bign add(bign a, bign b){
	bign c;
	int carry = 0;
	if(a.lenf>b.lenf){
		char tmp[150]={0};
		int temp = a.lenf-b.lenf;//记录要补0的个数 
		for(int i=0;i<b.lenf;i++) tmp[i] = b.f[i]+'0';//把小数部分暂存在tmp里 
		b.lenf = 0;//暂时清空b小数部分的长度
		for(int j=0;j<temp;j++) b.f[b.lenf++] = 0;
		for(int i=0;i<strlen(tmp);i++) b.f[b.lenf++] = tmp[i]-'0'; 
	}
	else if(a.lenf<b.lenf){
		char tmp[150]={0};
		int temp = b.lenf-a.lenf;//记录要补0的个数 
		for(int i=0;i<a.lenf;i++) tmp[i] = a.f[i]+'0';//把小数部分暂存在tmp里 
		a.lenf = 0;//暂时清空a小数部分的长度
		for(int j=0;j<temp;j++) a.f[a.lenf++] = 0;
		for(int i=0;i<strlen(tmp);i++) a.f[a.lenf++] = tmp[i]-'0'; 
	}
	for(int i=0;i<a.lenf;i++){//a.lenf和b.lenf应该是相等的,写哪个都一样 
		int temp = a.f[i]+b.f[i]+carry;
		c.f[c.lenf++] = temp%10;
		carry = temp/10;
	}
	for(int i=0;i<a.lend||i<b.lend;i++){
		int temp = a.d[i]+b.d[i]+carry;
		c.d[c.lend++] = temp%10;
		carry = temp/10;
	}
	if(carry!=0) c.d[c.lend++] = carry;
	return c;
}
int main(){
	int n;
	while(scanf("%d", &n) != EOF){
		while(n--){
			char str1[150], str2[150];
			scanf("%s", str1);
			scanf("%s", str2);
			bign x = change(str1);
			bign y = change(str2);
			bign z = add(x, y);
			int tempi = 0;
			for(int i=0;i<z.lenf;i++){
				if(z.f[i]!=0){
					tempi = i;//获得第一个非0的位置 
					break;
				}
			} 
			for(int i=z.lend-1;i>=0;i--) printf("%d", z.d[i]);
			printf(".");
			for(int i=z.lenf-1;i>=tempi;i--) printf("%d", z.f[i]);
			printf("\n");
			memset(str1, 0, sizeof(str1));
			memset(str2, 0, sizeof(str2));
		}
	}
	return 0;
}

问题 D: 进制转换

题目描述
将M进制的数X转换为N进制的数输出。
输入
输入的第一行包括两个整数:M和N(2<=M,N<=36)。
下面的一行输入一个数X,X是M进制的数,现在要求你将M进制的数X转换成N进制的数输出。
输出
输出X的N进制表示的数。
样例输入

10 2
11

样例输出

1011

提示
注意输入时如有字母,则字母为大写,输出时如有字母,则字母为小写。
思路
这题和问题F类似(我先做的问题F),我直接沿用了F的函数(实在太多了记不住……),这里需要改一下的是change函数,如果读入的是字符A~Z,则转化成对应的10~35,另外,除N取余的时候,也需要判断一下,如果余数是10~35,则转化成a~z。
其他方面的大致思路就和普通的进制转换一样,先把M进制转换成10进制,再把10进制转换成N进制。转成10进制的时候需要用multi计算次方的问题,在这里,0次方的情况建议先直接加上去,否则循环条件也比较难写,计算完次方之后,还要用multi函数乘上当前的权值,然后把这些数加在一起就是对应的10进制数。10进制转N进制的话就要写得简单一些了,直接用while来除N取余一直循环即可,到商为0的时候break掉。
代码

#include
#include
#include
#include
#include
#include
using namespace std;
struct bign{
	int d[1001];
	int len;
	bign(){
		memset(d, 0, sizeof(d));
		len = 0;
	}
};
bign change(char s[]){
	bign a;
	a.len = strlen(s);
	for(int i=a.len-1, j=0;i>=0, j<a.len;i--, j++){
		if(s[i]>='A'&&s[i]<='Z') a.d[j] = s[i]-'A'+10;
		else a.d[j] = s[i]-'0';
	}
	return a;
}
bign divide(bign a, int b, int& r){//a是被除数,b是除数,r余数 
	bign c;
	c.len = a.len;
	for(int i=a.len-1;i>=0;i--){
		r = r*10+a.d[i];//r作为临时被除数 
		if(r<b) c.d[i] = 0;
		else{
			c.d[i] = r/b;//存放商 
			r = r%b;//存放余数 
		}
	}
	while(c.len-1>=1&&c.d[c.len-1]==0){
		c.len--;
	}
	return c;
}
bign multi(bign a, int b){
	bign c;
	int carry = 0;
	for(int i=0;i<a.len;i++){
		int temp = a.d[i]*b+carry;
		c.d[c.len++] = temp%10;
		carry = temp/10;
	}
	while(carry!=0){
		c.d[c.len++] = carry%10;
		carry /= 10;
	}
	return c;
}
bign add(bign a, bign b){
	bign c;
	int carry = 0;
	for(int i=0;i<a.len||i<b.len;i++){
		int temp = a.d[i]+b.d[i]+carry;
		c.d[c.len++] = temp%10;
		carry = temp/10;
	}
	if(carry!=0) c.d[c.len++] = carry;
	return c;
}
char str[1001]={0};
char tmp[1001]={0};//存放N进制数 
int main(){
	int M, N;
	while(scanf("%d%d", &M, &N) != EOF){
		scanf("%s", str);
		bign X = change(str);
		bign y;
		bign temp;
		temp.d[0] = X.d[0];
		temp.len = 1;
		y = add(y, temp);//先把最低位加上(因为不管是什么进制一定是0次方) 
		for(int i=1;i<X.len;i++){
			bign z;
			z.d[0] = 1;
			z.len = 1;
			for(int j=1;j<=i;j++){//计算M的i次方 
				z = multi(z,M);
			}
			z = multi(z,X.d[i]);//乘上权值 
			y = add(y, z);
		}
		int r = 0, index = 0;//r是余数 
		temp = divide(y, N, r);//除N取余
		if(r>=10&&r<=35) tmp[index++] = r-10+'a';
		else tmp[index++] = r+'0';
		r = 0;
		while(1){
			temp = divide(temp, N, r);
			if(r>=10&&r<=35) tmp[index++] = r-10+'a';
			else tmp[index++] = r+'0';
			r = 0;
			if(temp.len==1&&temp.d[0]==0) break;
		}
		for(int i=strlen(tmp)-1;i>=0;i--) printf("%c", tmp[i]);//从高位输出
		printf("\n");
		memset(str, 0, sizeof(str));
		memset(tmp, 0, sizeof(tmp)); 
	}
	return 0;
}

问题 E: 大整数排序

题目描述
对N个长度最长可达到1000的数进行排序。
输入
输入第一行为一个整数N,(1<=N<=100)。
接下来的N行每行有一个数,数的长度范围为1<=len<=1000。
每个数都是一个正数,并且保证不包含前缀零。
输出
可能有多组测试数据,对于每组数据,将给出的N个数从小到大进行排序,输出排序后的结果,每个数占一行。
样例输入

4
123
1234
12345
2345

样例输出

123
1234
2345
12345

思路
这题很简单,定义一个大整数的数组和字符串数组,把输入的字符串存放在字符串数组中,然后再用change函数转化成大整数类型放在大整数数组中,最后对这个大整数数组进行冒泡排序即可。
代码

#include
#include
#include
#include
#include
#include
using namespace std;
struct bign{
	int d[1001];
	int len;
	bign(){
		memset(d, 0, sizeof(d));
		len = 0;
	}
}tmp[101];
bign change(char s[]){
	bign a;
	a.len = strlen(s);
	for(int i=a.len-1, j=0;i>=0, j<a.len;i--, j++) a.d[j] = s[i]-'0';
	return a;
}
int judge(bign a, bign b){
	if(a.len>b.len) return 1;//a大
	else if(a.len<b.len) return -1;//b大
	else{
		for(int i=a.len-1;i>=0;i--){//长度相等从高位开始比较 
			if(a.d[i]>b.d[i]) return 1;
			else if(a.d[i]<b.d[i]) return -1;
		}
		return 0;
	} 
}
char str[101][1001];
int main(){
	int N;
	while(scanf("%d", &N) != EOF){
		for(int i=0;i<N;i++){
			scanf("%s", &str[i]);
			tmp[i] = change(str[i]);
		}
		///冒泡排序/// 
		for(int i=1;i<=N-1;i++){
			for(int j=0;j<N-i;j++){
				if(judge(tmp[j], tmp[j+1])==1){
					bign temp;
					for(int k=0;k<tmp[j].len;k++) temp.d[k] = tmp[j].d[k];
					temp.len = tmp[j].len;
					for(int k=0;k<tmp[j+1].len;k++) tmp[j].d[k] = tmp[j+1].d[k];
					tmp[j].len = tmp[j+1].len;
					for(int k=0;k<temp.len;k++) tmp[j+1].d[k] = temp.d[k];
					tmp[j+1].len = temp.len;
				}
			}
		}
		for(int i=0;i<N;i++){
			for(int j=tmp[i].len-1;j>=0;j--) printf("%d", tmp[i].d[j]);
			printf("\n");
		}
	}
	return 0;
}

问题 F: 10进制 VS 2进制

题目描述
对于一个十进制数A,将A转换为二进制数,然后按位逆序排列,再转换为十进制数B,我们称B为A的二进制逆序数。
例如对于十进制数173,它的二进制形式为10101101,逆序排列得到10110101,其十进制数为181,181即为173的二进制逆序数。
输入
一个1000位(即10999)以内的十进制数。
输出
输入的十进制数的二进制逆序数。
样例输入

985

样例输出

623

思路
这题要用到很多函数,虽然思路简单,但是代码比较复杂就是了。因为最多能到1000位,所以要用bign类型,但是,在10进制和2进制的互相转换之间,既需要乘法,又需要除法,而且最后在2进制转成10进制之后,还要用加法把所有大整数加起来。在读入的时候呢,又要把字符串转化成大整数,所以这题一共要四个函数:change、add、multi、divide(字符串类型转大整数类型、加法、乘法、除法)。

写完之后实现进制转换的方法就和低精度的数一样了,10进制转2进制需要除2取余,先得到的余数是低位,后得到的是高位,然后从高位输出。这里要注意,一次除法存储好余数之后,记得把余数清0,否则后续的结果会出错。此外,这里要的是逆序排列,所以直接从数组的i=0开始存放余数即可,得到的序列(从0~index-1)就是逆序后的序列。然后再对每一位1做下标次方的乘方运算(乘方运算就是用循环不断进行乘法运算),对0则直接跳过(否则会花费无用的时间),最后再加起来就是答案了。

另外,对于1需要特判输出为1,否则会多乘一次2,造成输入1的输出是2。
代码

#include
#include
#include
#include
#include
#include
using namespace std;
struct bign{
	int d[1001];
	int len;
	bign(){
		memset(d, 0, sizeof(d));
		len = 0;
	}
};
bign change(char s[]){
	bign a;
	a.len = strlen(s);
	for(int i=a.len-1, j=0;i>=0, j<a.len;i--, j++) a.d[j] = s[i]-'0';
	return a;
}
bign divide(bign a, int b, int& r){//a是被除数,b是除数,r余数 
	bign c;
	c.len = a.len;
	for(int i=a.len-1;i>=0;i--){
		r = r*10+a.d[i];//r作为临时被除数 
		if(r<b) c.d[i] = 0;
		else{
			c.d[i] = r/b;//存放商 
			r = r%b;//存放余数 
		}
	}
	while(c.len-1>=1&&c.d[c.len-1]==0){
		c.len--;
	}
	return c;
}
bign multi(bign a, int b){
	bign c;
	int carry = 0;
	for(int i=0;i<a.len;i++){
		int temp = a.d[i]*b+carry;
		c.d[c.len++] = temp%10;
		carry = temp/10;
	}
	while(carry!=0){
		c.d[c.len++] = carry%10;
		carry /= 10;
	}
	return c;
}
bign add(bign a, bign b){
	bign c;
	int carry = 0;
	for(int i=0;i<a.len||i<b.len;i++){
		int temp = a.d[i]+b.d[i]+carry;
		c.d[c.len++] = temp%10;
		carry = temp/10;
	}
	if(carry!=0) c.d[c.len++] = carry;
	return c;
}
char str[1001]={0};
int tmp[1001]={0};//存放二进制数 
int main(){
	while(scanf("%s", str) != EOF){
		if(strcmp(str, "1")==0) printf("1\n");//如果输入的是1,则特判输出为1 
		else{
			int r = 0, index = 0;
			bign x = change(str);
			bign y; 
			bign temp = divide(x, 2, r);
			tmp[index++] = r;//把余数存放在tmp里(从低位开始存放)
			r = 0;
			while(1){
				temp = divide(temp, 2, r);
				tmp[index++] = r;
				r = 0;
				if(temp.len==1&&temp.d[0]==0) break;//如果商为0,break 
			}
			for(int i=0, j=index-1;i<index, j>=0;i++, j--){
				if(tmp[i]==0) continue;//如果是0,直接进入下一轮循环 
				struct bign z;//定义一个大整数类型的z 
				z.d[0] = 1;//初始化为1 
				z.len = 1;
				for(int k=1;k<=j;k++) z = multi(z,2);//计算2的j次方 
				y = add(y, z);
			}
			for(int i=y.len-1;i>=0;i--) printf("%d", y.d[i]);
			printf("\n");
			memset(str, 0, sizeof(str));
			memset(tmp, 0, sizeof(tmp));	
		}
	}
	return 0;
}

小结

大整数类型的题目,如果是单纯的加减的话,需要定义一个结构体,然后再写一个add函数或者sub函数,如果有需要最多再配一个change函数就好了,还是比较简单的,但是如果遇到大整数类型的进制转换,涉及到加法、乘法、除法,这么多一起写就比较麻烦了,思路虽然简单,但是过程是复杂的,而且也容易出错,这个时候思路一定要清晰,把进制之间的转化方法想清楚,基本上不会出太大的错误。

我个人认为大整数类型题目的难点还是写出相关的运算函数(表示记一个还记得住,要是让我一下子写三个的话会忘掉……)。

你可能感兴趣的:(《算法笔记》学习日记)