(前置技能:小学数学知识,C语言基础)
什么是高精度
我们知道,C/C++在64位编译器中支持的环境下,其各种整型数据类型表示是有范围的。
类型名 取值范围
signed char -128~127
unsiged char 0~255
short int -32768~32767
int -2147483648~2147483647
unsigned int 0~4294967295
long -2147483648~2141483647
unsigned long 0~4294967295
long long -9223372036854775808~9223372036854775807
unsigned long long 0~18446744073709551615
尽管long long类型(或者在有些编译器中表示为__int64.注意int64前面有2个下划线)能表示的数已经相当大了,但和一些天文数字比起来,它连屏幕的一行都占不到。很多实际数学问题其实是long long也无法解决的。
例如,围棋的走法有3的361次方,这个数字怎么显示出来?宇宙中原子的总和是10的80次方,怎么显示出来?这些都是语言本身的数据类型无法表示的。实际上,这些数字如果用二进制去储存的话,需要的空间也是特别大的。为了节约空间,同时让我们能够算出这些大数据,我们需要一种应用数组进行模拟计算的方法——高精度计算。
高精度算法
提醒:本博文该部分的所有代码均只用于展示计算原理,如果要整合在一起并进行更加复杂的符合操作,会产生诸多bug(其实直接复制粘贴任何一段都可能会有问题)总之就是,在这里找不到板子啦
高精度数的读写
- 读入高精度数
直观地输入一个高精度数时,我们需要存储一个有很多很多位的数字.
如何存储?最直接的方法就是将它存入一个字符串.
例如,我们可以这样:
char str[100] = {""};//100表示数据可能的最大位数.
scanf("%s, &str);
这种输入方式再简单不过了,我们输入“43983926969436339448266492\n”
则数据“43983926969436339448266492”就被存入其中了
(提示:彩蛋数据)
2. 存储高精度数
现在,数据的读取完成了,我们应该如何处理这个数,让他变得方便于计算呢?
我们不妨再设置一个数组,然后将输入的数逆序存入,也就是将str的最后一位存入数组a的第0位、将str的倒数第二位存入数组a的第1位……
#include <string.h>
int a[100] = {0};//100应该为这个数据可能的最大位数.
for(int i = 0; i < strlen(str); ++i)
a[i] = str[strlen(str) - i - 1] - '0';
这个时候就有人会问了:为什么要逆序存储?
这是因为,数据的长度毕竟不确定,高精度计算既然是利用数组计算,应当是需要每一位每一位对齐再处理运算. 如果顺序存储,两个数长度不一样,最后一位的下标也不同,最后处理运算时最后一位不能直接对齐,还要经过一个移位的操作,显然加大了计算机的操作量. 反之,如果我们采取逆序存储的方式,存入的数组从第0位开始依次是个位、十位、百位……这些对应关系不会随着数据的长度改变而改变,有利于后面对两个高精度数进行运算.
——————————————————————————
高精度加法
了解了高精度的输入与存储后,我们可以定义一个表示高精度数据的结构体:
#define LEN 1001//假定1001是我们处理的所有数据的最大位数
struct hp{
char str[LEN];
int a[LEN];
};
为了进行加法,我们可以先准备3个结构体,分别作为两个加数和和. 注意定义后,为了正确输入并在后续输出时检查前导0, 应该将结构体的字符数组初始化为空串,将a数组全部初始化为0.
void init(struct hp *x)//初始化函数
{
for(int i = 0; i < LEN; ++i)
{
x->a[i] = 0;
x->str[i] = '\0';
}
}
如何进行高精度的加法?
回想小学数学,那一个个竖式,逢十进一,简便美观的十进制表达……哦,我的上帝,多么纯粹简朴的方法啊!
于是,高精度加法,其实就是基于竖式运算原理的啦~正因如此,我们才需要对齐两个加数.
加法:
先获取两个加数中最大的位数len,则本次加法计算需要进行len次操作. 从第0位开始,每次将对应位相加,得到和在该位的数值. 如果和在该位的数值大于等于10,那么将其减掉10,并让高一位加1.
void add(struct hp x, struct hp y, struct hp *ans)
{
int len = 0;
if(strlen(x.str) > strlen(y.str))
len = strlen(x.str);
else len = strlen(y.str);
for(int i = 0; i < len; ++i)
{
ans->a[i] += x.a[i] + y.a[i];
while(ans->a[i] >= 10)
{
ans->a[i] -= 10;
ans->a[i + 1]++;
}
}
}
加法计算器完整代码:
#include <stdio.h>
#include <string.h>
#include <math.h>
#define LEN 1001//假定该程序处理的数据都是小于1000位的.
/*最长位数1001是为了安全.*/
struct hp{
char str[LEN];
int a[LEN];
};//创建结构体hp(high-precision)
//注意使用指针,这样来通过指针形参修改传入的实参.
void init(struct hp *x)//hp初始化函数
{
for(int i = 0; i < LEN; ++i)
{
x->a[i] = 0;
x->str[i] = '\0';
}
}
void in(struct hp *x)//输入函数.
{
scanf("%s", &x->str);
int len = strlen(x->str);
for(int i = 0; i < len; ++i)
x->a[i] = x->str[len - i - 1] - '0';
}
void add(struct hp x, struct hp y, struct hp* ans)//加法函数
{
int len = 0;
if(strlen(x.str) > strlen(y.str))
len = strlen(x.str);
else len = strlen(y.str);
for(int i = 0; i < len; ++i)
{
ans->a[i] += x.a[i] + y.a[i];
while(ans->a[i] >= 10)
{
ans->a[i] -= 10;
ans->a[i + 1]++;
}
}
}
void show(struct hp ans)//输出函数
{
int j = LEN - 1;//数组有LEN位,其最后一个下标是LEN-1.
while(ans.a[j] == 0 && j >= 1)
--j;//跳过所有前导0.
/*注:为了确保能输出结果0,前导0最多跳到第一位,
不检查第0位是否为0,因此加了条件j >= 1.*/
//现在,j的值是我们要输出的这个结果的最大位的下标.
while(j >= 0)
{
printf("%d", ans.a[j]);
--j;
}
printf("\n");
}
int main()
{
struct hp x, y, ans;
init(&x);
init(&y);
init(&ans);
in(&x);
in(&y);
add(x, y, &ans);
show(ans);
return 0;
}
以上就是进行高精度加法的过程了.
高精度减法
实现高精度减法,我们同样可以用三个结构体. 原理与加法相似,仍然是竖式原理:
先获取被减数的位数len,则本次减法计算需要进行len次操作. 从第0位开始,每次将对应位相减,得到和在该位的数值. 如果和在该位的数值小于0,那么将其加上10,并让高一位减1.
void minus(struct hp x, struct hp y, struct hp* ans)
{
int len = strlen(x.str);
for(int i = 0; i < len; ++i)
{
ans->a[i] += x.a[i] - y.a[i];
while(ans->a[i] < 0)
{
ans->a[i] += 10;
ans->a[i + 1]--;
}
}
}
但是这样的减法是无法计算结果为负数的情况. 例如1 - 10.为了解决减法出现负数的问题,我们需要往结构体中添加一个表示正负的符号. 例如,添加一个bool值,并定义一个比较函数,利用其比较被减数和减数的大小(此处比较函数就交给你们啦). 当减数大于被减数,让设置的bool值为0,反之为1. 当bool值为1时直接进行减法运算. 如果bool值为0,就返回用减数减去被减数的结果.
同样地,利用这个表示符号位的布尔值,我们还可以定义含有负数的加法,等等……
高精度乘法
要进行高精度乘法,我们会如何去操作?
竖式乘法?我们很容易想到这样的方式:
计算123 * 52,
首先列竖式,让123乘以2,也就是让每一位乘以2,得到246.
然后让123每一位乘以5,得到615(这里隐含了进位操作).
最后,因为5是十位,它的位权是10,所以615修正为6150.
让6150+246,得到结果6396.
这个算法是可行的,让我们来看它的步骤应该如何设计:
- 首先获取被乘数和乘数的长度len1,len2;
- 定义len2个高精度数 a[len2],它们作为被乘数与乘数每一位分别相乘的结果;
- 每个 a[i] 拥有一个位权 j ,j 决定在后续处理中将所有 a[i] 相加时每个a[i] 要乘以10的幂数;
(说白了就是为了模拟竖式乘法最后相加的过程) - 取出乘数的第 i 位,让被乘数的每一位乘以这个数 ,并把乘得的结果存入a[i] 对应的位.
(例如上方例子,123乘以5的时候,位权j为1,我们应当将结果15、 10、 5分别存入 a[1] 的第1(j + 0)位,第2(j + 1)位,第3(j + 2)位. 这里的a[1] 就是 {0,5,10,15} 了. 同理,我们知道 a[0] 为 {2,4,6}.) - 最后一步和自然的竖式乘法的最后一步一致,让所有的 a[i] 相加. 因为我们在之前的讨论中已经封装了高精度的加法,并且进位判定都是用的while语句,因此无论 a[i] 的第几位有多大,最后相加的过程中也能利用进位得到正确结果了.
代码实现:
void multi(struct hp x, struct hp y, struct hp* ans)
{
int len1 = strlen(x.str);
int len2 = strlen(y.str);
struct hp A[len2];//乘数有len2位.
/*这里应该还有一个使所有A.a[i]初始化为0的操作.
这里只展示乘法原理,略去.*/
for(int i = 0; i < len2; ++i)
{
//每个因子进i位.
int n = y.a[i];//注意取n为被乘数的第i位.
//让A[i]的每一位都乘上这个数.
int u = i;
for(int j = u; j < u + len1; ++j)
A[i].a[j] = x.a[j - u] * n;
}
//让所有因子相加.
for(int i = 0; i < len2; ++i)
{
struct hp tmp;
ans = add(ans, A[i], tmp);
}
return ans;
}
高精度除法及求余
高精度除法的原理仍然是基于竖式除法……只不过,为了方便代码更加容易理解,我们不妨换一种方式解释.
譬如,我们现在要计算1234除以9.
- 首先,获取最大位是4.将其减1得到3.
- 9乘以10的3次幂,得到9000.
- 用1234减去9000,结果小于0,有效的减法为0次. 在结果的千位放置0.
- 将9000降幂变为900,用1234减去900,得到334. 再减一个900,小于0. 有效的减法为1次,在结果的百位放置1.
- 将900降幂得到90,用334减去90,可以减3次,得到64. 有效的减法为3次,在结果的十位放置3.
- 将90降幂得到9,用64减去9,可以减7次,得到1.在结果的个位放置7.
- 9已经是9乘以10的零次幂了,最后这个1小于除数,那么我们除法得到的余数是1.
- 故:1234除以9等于137,余1.
那么,我们将其转化为可以被机器执行的描述:
- 如果被除数的位数(len1)小于除数的位数(len2),直接返回0. (余数为被除数.)
- 否则的话,获取除数位数,让其乘以10的(被除数位数-除数位数)次方,得到一个数b.
- 定义一个长度最长为被除数位数数组cnt[len1].
- 将被除数减去b. 每减去一次b,cnt[位数]++.
- 当被除数即将在减去后小于0时,不执行该步减法,让b缩小为原来的十分之一.
- 继续减,直到b和原来的除数相等.
该算法的实现依赖于高精度乘法和高精度减法,只有这两种计算方法已经封装了才可以实现除法. 另外,同样需要一个比较函数.
利用这个算法,我们就可以求出两个数的商和余数了.
感受:ACM的日子,每周还是要学点东西的~