C语言丨小 学 数 学(一):高精度加法

计算机,顾名思义,是用于高速计算的电子计算机器。它可以进行数值计算,又可以进行逻辑计算,还具有存储记忆功能。是能够按照程序运行,自动、高速处理海量数据的现代化智能电子设备。[1]但是,计算机的功能毕竟是有限的,再强大的计算机在面对一些问题时也有它的局限性,本系列文章就来具体阐述其中的一种问题:高精度运算。

先从一个问题说起:编程从键盘输入n,然后计算并输出1+2+3+…+n的值。

这应该是每个人初学循环控制结构与循环语句时都会遇到的经典问题,最初我们接触到的代码如下:

#include <stdio.h>
int main
{
    int i, n, sum;
    printf("Input n:");
    scanf("%d", &n);
    sum = 0;//累加和变量初始化为0
    for (i = 1; i <= n; i++)
    {
        sum = sum + i;//做累加运算
    }
    printf("sum = %d\n", sum);
    return 0;
}

但其实这个代码是有局限性的,它只能解决n比较小时的累加求和问题,当n很大时,计算机的精度就会下降。这是因为int型变量在内存中只占4个字节,所表示的范围仅为-2^{31}~2^{31}-1,即为-2147483648~2147483647。又有人会说,那int不够用,用long int、long long int总可以吧?但是这些数据类型始终有它们的最大取值范围,当n足够大,大到无法用任何数据类型存储时,这些方法便失去了价值,此时计算机的强大功能甚至比不上一个三年级的小学生的数学水平。

那有没有什么通用的方法呢?我们以一道例题来作为例子探究:

使用一维数组实现两个正整数相加,输出它们的和。两个正整数位数最少为20位,不超过50位,注意两个加数的位数不一定相等。


其实题目给的提示已经足够多了,就是利用加法的竖式运算原理,模拟进位运算,实现两个大数相加,具体数学原理可以参见《人民教育出版社数学三年级上册》第四章:万以内的加法和减法(二)[2],这里不作展开。(看看谁是九年义务教育漏网之鱼🤪🤪)

首先,我们需要三个数组,一个数组的每一位元素分别存储一个大数的每一位数,于是我们可以先定义三个整型数组:

#define N 50
int num1[N] = {0}, num2[N] = {0}, sum[N+1] = {0};

为什么sum需要N+1位呢?这是因为如果这两个50位的加数都是的首位都是9,那么和的首位将会进一,届时用50位的数组便无法存储下这么多的数。那为什么要全部清零呢?这是为了避免未赋值的元素为系统产生的随机值。

定义完数组之后,接下来要思考的就是,怎么样把正整数的每一位拆开,分别输入进数组num1与数组num2中?能否使用以下的方法?

for (int i=0; i<N; i++)
{
    scanf("%d", &num1[i]);
}
for (int i=0; i<N; i++)
{
    scanf("%d", &num2[i]);
}

如果这样的话,倘若我们输入:

567↙

100↙

会发生什么事情?

结果是num1[0]=567num1[1]=100,这时相信我不说您也知道问题出现在哪里了,于是我们将以上代码改为:

for (int i=0; i<N; i++)
{
    scanf("%1d", &num1[i]);
}
for (int i=0; i<N; i++)
{
    scanf("%1d", &num2[i]);
}

但是这样真的就可以了吗?

并不行,因为如果第一个输入的数还没有到50位,第一个循环并不会结束,因此第二个数也会被录入到第一个数组中。

所以我们应该用getchar()代替函数scanf()getchar()读取到的字符减去'0'的ASCII码值,得到相应的十进制数。也即:

for (int i=0; i<N; i++)
{
    num1[i] = getchar() - '0';
    if ((num1[i]==' '-'0')||(num1[i]=='\n'-'0'))
    {
        num1[i] = 0;
        break;
    }//当第一个数输入完后,输入空格或回车以跳到第二个数
}
for (int i=0; i<N; i++)
{
    num2[i] = getchar() - '0';
    if ((num2[i]==' '-'0')||(num2[i]=='\n'-'0'))
    {
        num2[i] = 0;
        break;
    }
}

但现在有个很头疼的问题,就是我要如何找到这两个数的个位在哪里?相加的时候如何使个位互相对齐、十位互相对齐……?于是我们需要引入一个变量count来帮助我们计算我们输入数字的位数,并使个位与最高位交换,十位与次高位交换……直到把数翻转过来(如使12345变为54321),因为这样的话数组的第0个元素都是个位,更有利于我们算加法的时候实现对齐:

int count = 0;
for (int i=0; i<N; i++)
{
    num1[i] = getchar() - '0';
    if ((num1[i]==' '-'0')||(num1[i]=='\n'-'0'))
    {
        num1[i] = 0;
        break;
    }//当第一个数输入完后,输入空格或回车以跳到第二个数,并重新把被赋值为空格或回车的元素赋值为0
    else
    {
        count++;
    }//如果还没输入完成,便使count++
}
for (int i=0; i<count/2; i++)//无论count是奇数还是偶数,通过举例子我们可以得出只需i<count/2
{
    int temp;
    temp = num1[i];
    num1[i] = num1[count - 1 - i];
    num1[count - 1 - i] = temp;
}//首尾交换
count = 0;
for (int i=0; i<N; i++)
{
    num2[i] = getchar() - '0';
    if (num2[i]=='\n'-'0')
    {
        num2[i] = 0;
        break;
    }//当第二个数输入完后,输入回车跳出循环,并重新把被赋值为回车的元素赋值为0
    else
    {
        count++;
    }//如果还没输入完成,便使count++
}
for (int i=0; i<count/2; i++)//无论count是奇数还是偶数,通过举例子我们可以得出只需i<count/2
{
    int temp;
    temp = num2[i];
    num2[i] = num2[count - 1 - i];
    num2[count - 1 - i] = temp;
}//首尾交换

这样就没有任何问题啦~那么当我们把两个加数都录入后,就可以开始进行最核心的部分:加法计算了。首先让我们重新学习一下加法:一个加法的实现,主要包括了两个步骤——相加-进位。[3]那么我们先来解决相加的问题:

for (int i=0; i<N; i++)
{
    sum[i] = num1[i] + num2[i];
}

如果sum[i]>=10,就需要进位,进位的核心就是把十位取出来,加到下一位上,并把个位取出来,留在本位,但鉴于当sum[i]<10时,sum[i]/10=0(整数除法)sum[i]%10=sum[i],我们可以不用单独判断是否需要进位,于是我们有:

for (int i=0; i<N; i++)
{
    sum[i] = num1[i] + num2[i];
    sum[i+1] = sum[i] / 10;
    sum[i] %= 10;
}

这么写是不是就没问题了呢?通过测试,我们发现:假如是99+99,那么num1[0]=9,num2[0]=9,sum[0]=9+9=18,进位后sum[1]=1,sum[0]=8;而当第二轮循环开始后,sum[1]=num1[1]+num2[1]=18,sum[2]=1,sum[1]=8。那是不是意味着我们最终的结果为99+99=188?

答案很明显是有问题的,问题就在于第二轮循环开始后,sum[1]原来的值被新的值覆盖了,而不是在原来的基础上再加上num1[1]与num2[1],于是我们修改代码:

for (int i=0; i<N; i++)
{
    sum[i] += num1[i] + num2[i];
    sum[i+1] = sum[i] / 10;
    sum[i] %= 10;
}//代码的核心所在

那么就到了最后一步:输出我们的结果啦!由于此时sum[0]储存的是个位,而最高位位于整个数字的最后面,因此我们需要倒序输出这个数组;而由于最高位后面的数字都是0,我们想要跳过0,就需要设置一个标志变量flag,因此我们有:

int flag = 0;
for (int i=N; i>=0; i--)
{
    if (sum[i]!=0)
    {
        flag = 1;
    }
    if (flag)
    {
        printf("%d", sum[i]);
    }
}

OK,到此为止,我们就实现了大数加法的目的了。


大数加法的核心问题就在于如何编程模拟相加-进位的过程,只要把这个问题解决了,一切问题都能迎刃而解啦~在两数相加的时候,如果我们能巧妙利用两个加数的位数,跳过最高位之前0+0的一些繁琐的计算,还能使计算效率更高;另外在输入两个正整数的时候,出现了极多重复重复再重复的代码,如果能利用子函数包装一下,可以大大增加代码的可读性,减少代码量,大家不妨思考一下如何优化。


参考文献:

[1]百度百科.

[2]人民教育出版社 课程教材研究所 小学数学课程教材研究开发中心编著,数学三年级上册,P36-49.

[3]RG04,高精度加法丨超越限制的美丽,SCNU2021软工4班公众号Fourever with you,2021. 11-15.(C++)

  • 13
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值