用C/C++实现基本高精度计算

(前置技能:小学数学知识,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(其实直接复制粘贴任何一段都可能会有问题)总之就是,在这里找不到板子啦

高精度数的读写

  1. 读入高精度数
    直观地输入一个高精度数时,我们需要存储一个有很多很多位的数字.
    如何存储?最直接的方法就是将它存入一个字符串.
    例如,我们可以这样:
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.
这个算法是可行的,让我们来看它的步骤应该如何设计:

  1. 首先获取被乘数和乘数的长度len1,len2;
  2. 定义len2个高精度数 a[len2],它们作为被乘数与乘数每一位分别相乘的结果;
  3. 每个 a[i] 拥有一个位权 j ,j 决定在后续处理中将所有 a[i] 相加时每个a[i] 要乘以10的幂数;
    (说白了就是为了模拟竖式乘法最后相加的过程)
  4. 取出乘数的第 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}.)
  5. 最后一步和自然的竖式乘法的最后一步一致,让所有的 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.

  1. 首先,获取最大位是4.将其减1得到3.
  2. 9乘以10的3次幂,得到9000.
  3. 用1234减去9000,结果小于0,有效的减法为0次. 在结果的千位放置0.
  4. 将9000降幂变为900,用1234减去900,得到334. 再减一个900,小于0. 有效的减法为1次,在结果的百位放置1.
  5. 将900降幂得到90,用334减去90,可以减3次,得到64. 有效的减法为3次,在结果的十位放置3.
  6. 将90降幂得到9,用64减去9,可以减7次,得到1.在结果的个位放置7.
  7. 9已经是9乘以10的零次幂了,最后这个1小于除数,那么我们除法得到的余数是1.
  8. 故:1234除以9等于137,余1.

那么,我们将其转化为可以被机器执行的描述:

  1. 如果被除数的位数(len1)小于除数的位数(len2),直接返回0. (余数为被除数.)
  2. 否则的话,获取除数位数,让其乘以10的(被除数位数-除数位数)次方,得到一个数b.
  3. 定义一个长度最长为被除数位数数组cnt[len1].
  4. 将被除数减去b. 每减去一次b,cnt[位数]++.
  5. 当被除数即将在减去后小于0时,不执行该步减法,让b缩小为原来的十分之一.
  6. 继续减,直到b和原来的除数相等.

该算法的实现依赖于高精度乘法和高精度减法,只有这两种计算方法已经封装了才可以实现除法. 另外,同样需要一个比较函数.
利用这个算法,我们就可以求出两个数的商和余数了.

感受:ACM的日子,每周还是要学点东西的~

  • 22
    点赞
  • 79
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值