在计算机中,任何一个数据类型的大小都是有限制的。
数据类型 | 长度(字节) | 识别方式 | 范围 |
---|---|---|---|
char | 1 | ASCII码 | -2^7(-128) ~ 2^7-1(127) |
short | 2 | 补码 | -2^15(-32 768) ~ 2^15-1(32 767) |
int | 4 | 补码 | -2^31(-2 147 483 648) ~ 2^31-1(2 147 483 647) |
long | 4 | 补码 | -2^31(-2 147 483 648) ~ 2^31-1(2 147 483 647) |
float | 4 | 浮点 | -/+3.4e38(精确到6位小数) |
double | 8 | 浮点 | -/+1.7e308(精确到15位小数) |
那么,在日常生活中,数的大小是不确定的。 当我们需要表示一个很大的数时,如何使用有限的空间存储无限的数呢?
这里提出一种方法:万进制
其实他和十进制、二进制、八进制、十六进制原理是一样的。只是他是逢万进一。
为什么采用万进制?
——》 起初,我首先想到使用字符串数组进行存储数据,这样确实比较好存储,也比较容易输出。但是进行相加时就会出现一系列问题,我们需要将每个字符先转换为数字,更麻烦的就是进位的问题。加减法是还比较好控制,到了乘法,就非常难以控制了。并且,完成之后,又得不字符转换为字符储存起来,这又是一次赋值操作。
——》然后,我想的是用一个char数组分别存储每一个字符转化的数,但是这样乘法所涉及的进位也是一个麻烦的事。
——》最后,我想到了采用万进制来进行数的存储。
只需要多申请一个长度去处理进位即可。
微易码反码是我们老师(教主)多年来编程经验所总结出来的。在进行相关运算时,是真的香。
这是关于原码补码反码的简单介绍:原码、补码、反码介绍
在万进制下:正数的反码就是本身,而负数的反码是9999 - 这个负数的绝对值。下面用图来介绍微易码反码!
总结:两数相加时,申请万进制下的n(n为最大数的值)+1个空间。正数不变,负数用9999 - 绝对值;
结果的符号为:第一个数的符号位 ^ 第二个数的符号位 ^ 进位的数值(0或1)
结果为:最后一位加进位的数值,再根据符号位进行转换。
为什么要再多申请一位空间呢?忽略数值内的进位问题,将进位拿出来单独处理。否则就像上面的例子一样,结果出现偏差。
在不确定数值的长度下,我们该如何将它切分为四位一组呢?
n / 4 ?这明显存在一个漏洞。n不是四的整数倍时,n /4 的余数是无法计算到的。那我们可以使(n + 3) / 4 。这样不会使得超出一个长度,也会将 n / 4 的余数也一起照顾到。
万进制应该使用哪个数据结构来储存呢? char? short? int ! 前面两个的数值表示大小不足以暂时存储后面乘法的进位问题。
还有一个问题,我们是应该从头开始存储还是从尾开始存储。有些人就开始问了,我们一般表示数不都是从头部开始的吗?
这里注意手工过程!若从头开始存储,我们相加的时候怎么办呢?我们怎么进行从最后一位开始相加呢(这里指的是万进制下的个位)。
若我们从个位依次向前存储,那么就很轻松的解决了一个问题,位数对齐。这就使得后面的运算简单了起来。
根据前面的说法,我们需要这个数的符号,存储值的数组,还需要数组的个数。
现给出巨大数的存储结构
typedef struct HUGE {
boolean sign; //符号
int *huge; //存储数据
int count; //数组大小
}HUGE;
由于我们采用从文件里面读取数据,我们先确定数据的长度length,根据length去申请空间。同时我们也不确定这个数据是否存在符号位。所以,在读取数据之前我们先判断符号位,如果存在的话length–,并设置符号位;然后将文件读写指针移至文件末尾,使文件指针指向最后一位数据,使用fgetc()h获得这个字符,之后四位一组读取,依次存入数组中。共读取length个长度。
boolean initHuge(HUGE **hugeer, FILE *fp) {
int i;
int j;
int index;
int count;
int ch;
int scale = 1;//因为四位一组的数据中需要逐次*10
int tmp = 0;
int hugeindex = 0;//巨大数数组下标
//判断传入的数据是否符合规定
if(NULL == hugeer || NULL != *hugeer || NULL == fp) {
return FALSE;
}
*hugeer = (HUGE *)calloc(sizeof(HUGE), 1);
fseek(fp, 0, SEEK_END);
count = ftell(fp);//数据长度
fseek(fp, 0, SEEK_SET);
ch = fgetc(fp);//获取第一个字符,并且判断是否是'+'、'-'
if(ch == '-') {
(*hugeer)->sign = 1;
i = count;
--count;
} else if(ch == '+') {
(*hugeer)->sign = 0;
i = count;
--count;
} else {
(*hugeer)->sign = 0;
i = count;
}
//申请数组空间,申请失败,释放申请的hugeer空间,并返回错误
(*hugeer)->huge = (int *)calloc(sizeof(int), (count + 3) / 4);
if(NULL == (*hugeer)->huge) {
free(*hugeer);
return FALSE;
}
//获取4的整数倍的数据
for(index = 0; index < (count / 4); index++) {
for(j = 0; j < 4; j++){
fseek(fp, (--i) * sizeof(char), SEEK_SET);
ch = getc(fp) -'0';
ch *= scale;
scale *= 10;
tmp += ch;
}
(*hugeer)->huge[hugeindex++] = tmp;
scale = 1;
tmp = 0;
}
//若长度不是4的整数倍,执行下面代码。
index = count % 4;
if(index > 0) {
while(index){
fseek(fp, (--i) * sizeof(char), SEEK_SET);
ch = getc(fp) -'0';
ch *= scale;
scale *= 10;
tmp += ch;
index--;
}
(*hugeer)->huge[hugeindex++] = tmp;
}
(*hugeer)->count = (count + 3) / 4;
return TRUE;
}
因为我们是从尾开始存储的,那么,我们只需要反着输出即可,在输入的过程中,若某个位是1,会直接输出1。但是在数字中间,应该是0001,那么只需要%40d输出即可。
void ShowHuge(HUGE *hugeer) {
int i = hugeer->count - 1;
for(i; i >= 0; i--) {
if(i == hugeer->count -1) {
//从数组最后一位开始输出,并且他是几就输出几。
printf("%d",hugeer->huge[i] * (hugeer->sign == 0 ? 1 : -1));
} else {
printf("%04d",hugeer->huge[i]);
}
}
printf("\n");
}
根据前面微易码反码的铺垫,我们已经得知了具体的实现过程。先直接给出加法的代码。
//获得微易码反码
int getMECcode(int data, boolean sign) {
return (sign == 0 ? data : (9999 - data));
}
boolean add(HUGE *hugeerone, HUGE *hugeerother, HUGE **hugeresult) {
int i;
int rescount;
int result;
int tmp = 0;
int realresult;
if(NULL == hugeresult || NULL != *hugeresult) {
return FALSE;
}
*hugeresult = (HUGE *)calloc(sizeof(HUGE), 1);
//申请两个巨大数中最大count + 1的数组空间。
rescount = ((hugeerone->count) > (hugeerother->count) ? hugeerone->count : hugeerother->count) + 1;
(*hugeresult)->huge = (int *)calloc(sizeof(int), rescount);
//如果i大于一个巨大数的conut,那么,将这个巨大数i后面的数组值赋值为0
for(i = 0; i < rescount; i++) {
if(i >= hugeerone->count) {
hugeerone->huge[i] = 0;
}
if(i >= hugeerother->count) {
hugeerother->huge[i] = 0;
}
result = getMECcode(hugeerone->huge[i], hugeerone->sign)
+ getMECcode(hugeerother->huge[i], hugeerother->sign)
+ tmp;//+tmp表示需要加上次相加后的进位。
(*hugeresult)->huge[i] = result % 10000;//只将10000内的值赋值给数组
tmp = result / 10000;//获取进位。
}
//得到符号位
(*hugeresult)->sign = (hugeerone->sign) ^ (hugeerother->sign) ^ tmp;
for(i = 0; i < rescount; i++) {
realresult = (*hugeresult)->huge[i] + tmp;//加进位的数,完成最后的部分
(*hugeresult)->huge[i] = getMECcode(realresult % 10000, (*hugeresult)->sign);//最后根据结果的符号将结果再通过微易码反码变回来。
tmp = realresult / 10000;
}
//这里表示,若数组的最后一个数组值为0,说明没有存储有效数据,那么有效数据rescount--即可;
if(0 == ((*hugeresult)->huge[rescount - 1])) {
rescount--;
}
(*hugeresult)->count = rescount;
}
减法建立在加法的基础上,只需要将被减数符号变换即可。
boolean sub(HUGE *hugeerone, HUGE *hugeerother, HUGE **hugeresult) {
hugeerother->sign = hugeerother->sign == 0 ? 1 : 0;
add(hugeerone, hugeerother, hugeresult);
hugeerother->sign = hugeerother->sign == 0 ? 1 : 0;
}
乘法的基础是加法,是第二个数最后一位和第一个数全部进行相乘。然后第二个数倒数第二位和第一个数全部进行相乘。依次类推,结果相加。这时不需要再管符号位了。只是在最后把符号的符号赋值即可。
这里需要申请的数组大小是一个比较困难的问题。(我只能得出大概得出数字,但不能完全申请好,如果你有更好方案,欢迎可以和我交流交流)。
boolean mul(HUGE *hugeerone, HUGE *hugeerother, HUGE **hugeresult) {
int i;
int j;
int rescount;
int tmp = 0;
int result;
if(NULL == hugeresult || NULL != *hugeresult) {
return FALSE;
}
rescount = (hugeerone->count) * (hugeerother->count);//不成熟的申请方式
*hugeresult = (HUGE *)calloc(sizeof(HUGE), 1);
(*hugeresult)->huge = (int *)calloc(sizeof(int), rescount);
(*hugeresult)->sign = hugeerone->sign ^ hugeerother->sign;
//从第二个的最后一位开始
for(i = 0; i < hugeerother->count; i++) {
tmp = 0;
for(j = 0; j < hugeerone->count; j++){
result = hugeerone->huge[j] * hugeerother->huge[i] + tmp;
tmp = (result + (*hugeresult)->huge[i + j]) / 10000;
(*hugeresult)->huge[i + j] = (result + (*hugeresult)->huge[i + j]) % 10000;//给i+j位赋值,原因是i不变,j持续加一,当i变化时,对应的数组下标也开始变化,且每次j循环结束后,刚好数组向后移动一位,即第二次赋值从倒数第二位开始,符合乘法的手工过程。
}
(*hugeresult)->huge[i + j] = tmp;
}
//解决多申请的空间。并不是释放,而是在显示时不显示。
while((*hugeresult)->huge[rescount - 1] == 0) {
rescount--;
}
(*hugeresult)->count = rescount;
}
完成巨大数本身并不算太难(当然是因为有教主的微易码反码支持),在思考时,一切从手工过程出发,将手工过程变为代码,这是非常重要的一步。如果手工过程没有好好过一遍,编写代码真的是一件非常痛苦且困难的事。
因为时间的限制,没有完成巨大数的除法。这也是个小小的遗憾吧。
最后说一句,编代码之前必须先思考,想清楚了,再进行编程。