P2142 高精度减法 详细题解 原理解释 + 代码注释

完整代码请扒拉到最底下↓。
解题要求:掌握基础的判断、循环、数组、字符串,以及一些计算原理即可。

“高精度”是怎么一回事?

简单来说,就是我们计算的数字太大太大了,导致已经超出了我们可以定义的范围,以至于我们不能用正常的定义来定义和计算数字,不然只能计算我们能所容纳的数字部分,无法达到要求。

比如longlong可以定义的范围在 -(2的63次方)到 2的63次方-1

如果此时数据过大,超过了longlong可以定义的范围,那么我们该如何计算它呢?这就需要我们运用到高精度的知识了。

题外话:Python没有这个限制,所以一行代码就解决了。

高精度减法计算的原理

这一点和之前写过的一篇关于高精度加法一样,我就懒得重写了,直接复制粘贴吧。

1.存储数据

你看既然我们数据太大,普通的定义都装不下了,那我们拿什么来装数据呢?没错,就是

数组

有同学可能会问:我们又不知道这个数有多长,我现在又不会用vector,(其实我现在也还是不会),那我们定义数组的时候应该定义多长的数组呢?

吸取了上一次计算高精度加法的教训之后,我看了看给定的数据范围是

0 到 10的10086次方

所以我们定义三个数组,分别存储被减数,减数和差

int anum[10086] = {0}, bnum[10086] = {0};

//由于是减法,所以差的位数小于等于被减数和减数
int dif[10086] = {0};

接下来一步就是要输入数据了,但是这里有个坑,如果按照正常的顺序输入,算起来会非常麻烦,具体原因是什么呢?我们先看看我们是如何计算的

2.计算原理

其实计算原理我们早在小学就已经学过了,那就是

竖式运算

	 96
-	 69
-----------
	 27

假设上面的数是第一个数组的数,下边的数是第二个数组的数,那么我们就从个位开始熟悉的计算:

1.将每一位相减的结果存入第三个数组

2.如果被减数的位数小于减数的位数,则需要向下一位借位

3.借位要保存一下,下一位相减的时候就要减去1,如此反复。

4.最终得到第三个数组排列的数值就是计算的结果

以下是实现代码:

//定义借位的数
    int digit = 0;

    //找出两个数中最长的数字位数
    int len = max(al, bl);

    //从头开始遍历,直至最大的数字位数
    for (int i = 0; i < len; i++)
    {
        //如果被减数减去上上一位的借位大于等于减数,则直接计算,并将进位设置为0
        if ((anum[i] - digit) >= bnum[i])
        {
            dif[i] = (anum[i] - digit) - bnum[i];
            digit = 0;
        }
        //如果被减数减去上上一位的借位小于减数,则需要向下一位借10再计算,并将借位设置为1
        else
        {
            dif[i] = (anum[i] + 10 - digit) - bnum[i];
            digit = 1;
        }

    

3.输入数据

细心的同学已经发现了问题:

我们输入数据是从高位往低位输入的,换句话说,如果按照输入顺序存储,那我们输入的两个数据是“最高位对齐”的:

123

123456

但是我们竖式计算的要求是“个位对其”:

000123

123456

如果按照输入顺序存储数据,这样对我们计算有着很大的影响,由于每次输入的数据长度不一,想把下标对其非常困难,那我们怎么解决这个问题呢?

聪明的大佬想到了一种方法,就是将输入的数据倒序存入数组,这样一来,两个数的第一位不就对其了吗?

321000

654321

细心的同学似乎又注意到一点:短一点的数字后边是用0补齐的,为什么不直接空着呢?

我们回顾一下上边关于计算部分的代码,就发现在整个循环中,始终都是要访问anum[i]和bnum[i]这两个元素的,如果其中一个数比另一个数短,那么短的那个数计算完之后,也仍然会访问它剩下的元素,所以我们不能让它空着,就拿0过来填位置。

接下来先讲怎么输入数据

由于int、longlong这些都装不下了,我们又不能直接往数组里边存,所以需要一个中介来暂存一下数据,接下来就轮到伟大的字符串登场了:

//由于int、longlong无法容纳足够多的数字,只能用string来存储:
    string a, b;
    cin >> a >> b;

然后咱们就开始把string里的字符倒序存入数组

//获取两个字符串的长度:
    int al = a.length();
    int bl = b.length();

    //倒序将两个数字存入一个int数组里
    //目的是将两个数从个位对其
    for (int i = 0; i < al; i++)
    {
        //字符串a是可以变成a[i]的,每一个元素就是对应的字符
        //-'0'可以将数字字符转换为数字,原理是ASCII码,这里不赘述了
        anum[i] = a[al - i - 1]-'0';//利用al - i - 1实现倒序
    }
    for (int i = 0; i < bl; i++)
    {
        bnum[i] = b[bl - i - 1]-'0';
    }

现在看似基本的输入、存储、计算都完成了,只剩下输出了,胜利似乎眼前在望啊。但是减法不同于加法,两个数相加是可以交换顺序的,但是减法可以吗?我们来看看接下来的一种特殊情况:

特殊情况

我们之前的计算只符合最常见的一种情况,但是如果减数比被减数大呢?

		 111
-		 222
-------------

这里你就会发现,如果还是按照上边的代码进行借位处理,那么结果就会出错,所以我们减法的顺序一定要是 数值大的数减取数值小的数 ,我们就需要在计算之前建立一个判断条件:

我们的顺序是a-b:

假如a>b,则顺序不变

如果a<b,则需要将a,b两数交换一下,再进行计算

可是这里又又又出现了一个问题减法可真麻烦,那就是我们怎么比较两个数的大小呢????我们输入的不是像int、longlong,这样“正经”的数据,它们是可以通过">“,”<"来直接比较数据的,但是字符串或者数组可以这样比较吗?

我们首先看数组,毕竟里边的每一个元素都是int类型的嘛,是我们比较熟悉的

我们会发现如果利用数组比较两个数的大小时:

当两个数长度不同时,这个倒是可以很方便的比出来,直接比较两个字符串的长度就行了不是说好的用数组吗

然鹅当两个数的长度相同时,我们需要从最高位开始遍历并逐个比较,直到找到不同的数,然后比较,其实实现起来也没有那么的麻烦,就是要多写一个for循环和判断条件了,但是不急,我们先继续看看字符串,万一还有更好的方法呢?其实就是字符串好一点不卖关子了

字符和字符串也可以比较大小,不过这个规则稍稍有些绕,请容我先简答科普一下

如果是单个字符比较大小,那么就直接比较ASCII码的值的大小,比如A的ASCII值为65,B的ASCII值为66,则A < B。

如果是字符串呢?它也是比较ASCII码的值,只不过是有一个规律的,规律就是我刚才介绍数组的时候就说过的:从最高位(第一个字符)开始遍历并逐个比较,直到找到不同的数,然后比较,只不过字符串比较将这个过程自动化了而已。

只不过这里也有一个bug,就是它只会傻傻的逐个遍历,它并不会放眼全局观察字符串的长度,所以无论字符串长短如何时,只要它遍历比较到了一个不一样的数,它就会舍弃剩下未遍历的内容,然后火急火燎的根据第一个不一样的数判断谁大谁小

比如654和653333比较的时候,它只会看到4和3不一样,但是不会关心第二个数后边还有一坨3,这就是它的问题。

所以我们在设置判断条件的时候不仅要关注两个数的大小,还有关注两个数的长度,所以判断条件如下:

1.b的字符长度比a大时,需要交换a,b

2.当a,b字符长度相同时,b比a大(相当与给遍历加了一个前提条件),也需要交换a,b

注意如果a,b交换了,那么a,b的长度也要交换一下,不然后续遍历会出现问题的。

另外我这里还设置了一个标记,因为交换了顺序,说明计算结果肯定是个负数,这样就先要输出一个负号,通过对标记的判断我就能知道是否需要输出负号了

 //这里定义一个标记,用于标记是否交换过顺序,默认未交换
    int flagswap = 0;


    //因为我们的顺序是a-b,如果b的位数比a大,或者位数相同时,b的数值比a大,就需要交换顺序
    if (bl > al || (al == bl && b > a))
    {
        swap(a, b);//交换两数
        swap(al, bl); //交换两数长度,不然后续存入数组会出现数据缺失的问题
        flagswap = 1;//标记为交换过
    }

输出结果

终于来到了万众瞩目的输出环节。

我们首先对是否输出符号进行一个判断:

 //如果发现交换过数,则先输出负号
    if (flagswap == 1)
    {
        cout << "-";
    }

然后就是输出的时候遍历顺序,由于我们是倒序存储的,所以输出的时候就需要倒序输出,这样输出来就是正序了。

for (int i = len - 1; i >=0; i--)

最后考虑的就是出现0的情况。我们发现如果两个三位数相减,结果为两位数时,第三位的数就变成0了,那么我们输出的时候就需要把这个0跳过,但是也要注意两点:

1.如果计算的结果为0时,不要把最后的0跳过。

2.不要把所有的0跳过,非零数后边的0都要保留。

同时这里我也设置了一个标记,当发现非零数的时候更新标记,这样之后对标记判断一下,就能避免把非零数后边的0都跳过了


//定义另一个标记,用于标记数字前多余的零,默认未找到非零数
    int flagzero = 0;
    
    
//若前边几位都是0,且直到最后一位之前没有找到非零数,则跳过这段循环
//不判断到最后一位的原因是,是如果计算结果为0,能保证输出计算结果
        if (dif[i] == 0 && flagzero == 0 && i != 0)
        {
            continue;
        }
        flagzero = 1;//发现非零数之后就将标记设位1

以上就是高精度减法的基本实现了,但是这样不是全部,比如没有考虑到小数部分,这里题目也没有要求,各位有兴趣可以下去自己实现以下。

完整代码

#include <iostream>
using namespace std;
int anum[10086] = {0}, bnum[10086] = {0};
//由于是减法,所以差的位数小于等于被减数和减数
int dif[10086] = {0};

int main() {
    //由于int无法容纳足够多的数字,只能用string来存储
    string a, b;
    cin >> a >> b;

    //获取两个字符串的长度:
    int al = a.length();
    int bl = b.length();


    //这里定义一个标记,用于标记是否交换过顺序,默认未交换
    int flagswap = 0;


    //因为我们的顺序是a-b,如果b的位数比a大,或者位数相同时,b的数值比a大,就需要交换顺序
    if (bl > al || (al == bl && b > a))
    {
        swap(a, b);//交换两数
        swap(al, bl); //交换两数长度,不然后续存入数组会出现数据缺失的问题
        flagswap = 1;//标记为交换过
    }



    //倒序将两个数字存入一个int数组里
    //目的是将两个数从个位对其,才能计算
   
    for (int i = 0; i < al; i++)
    {
        //字符串a是可以变成a[i]的,每一个元素就是对应的字符
        //-'0'可以将数字字符转换为数字
        anum[i] = a[al - i - 1]-'0';//利用al - i - 1实现倒序
    }
    for (int i = 0; i < bl; i++)
    {
        bnum[i] = b[bl - i - 1]-'0';
    }



    //定义借位的数
    int digit = 0;

    //找出两个数中最大的数字位数
    int len = max(al, bl);

    //从头开始遍历,直至最大的数字位数
    for (int i = 0; i < len; i++)
    {
        //如果被减数减去上上一位的借位大于等于减数,则直接计算,并将进位设置为0
        if ((anum[i] - digit) >= bnum[i])
        {
            dif[i] = (anum[i] - digit) - bnum[i];
            digit = 0;
        }
        //如果被减数减去上上一位的借位小于减数,则需要向下一位借10再计算,并将借位设置为1
        else
        {
            dif[i] = (anum[i] + 10 - digit) - bnum[i];
            digit = 1;
        }

    }

    
    //定义另一个标记,用于标记数字前多余的零,默认未找到非零数
    int flagzero = 0;

    //如果发现交换过数,则先输出负号
    if (flagswap == 1)
    {
        cout << "-";
    }

    //因为我们是反向计算,所以最后需要反向输出,才能使数字为正序
    for (int i = len - 1; i >=0; i--)
    {
        //若前边几位都是0,且直到最后一位之前没有找到非零数,则跳过这段循环
        //不判断到最后一位的原因是,是如果计算结果为0,能保证输出计算结果
        if (dif[i] == 0 && flagzero == 0 && i != 0)
        {
            continue;
        }
        flagzero = 1;//发现非零数之后就将标记设位1
       
        cout << dif[i];
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值