完整代码请扒拉到最底下↓。
解题要求:掌握基础的判断、循环、数组、字符串,以及一些计算原理即可。
“高精度”是怎么一回事?
简单来说,就是我们计算的数字太大太大了,导致已经超出了我们可以定义的范围,以至于我们不能用正常的定义来定义和计算数字,不然只能计算我们能所容纳的数字部分,无法达到要求。
比如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;
}