因为这道题细节太多,一处细节没考虑到就不能AC,以至于困扰了我三个晚上……看了其他大佬的代码,虽然简短,但是无法AC并且与我的解题方法有较大不同,恰好今天中午此题终于AC通过,决定写一下自己的解题思路以及解题过程中遇到的小问题,最后分享一下自己的AC代码,希望能帮助到更多的小伙伴。这是我写的第一篇文章,如有不足还望多多包涵。
题目描述
读入两个字符串,字符串除了数字还可能包括 '—'、'E'、'e'、'.',相加之后输出结果,如果是浮点型,要求用科学计数法表示(最多包含10个有效数字)。
输入
输入包含多组测试数据。
每组输入占两行,每行一个字符串,测试数据保证字符串的构成严格按照题目中的描述。
输出
输出两个数字相加的结果,每组输出占一行。
样例输入
34.56 2.45e2
样例输出
2.7956e2
首先带大家分析一下题目要求 。字符串除了数字还包括那四个字符(注意,第一个字符是“-”,也就是负号)的意思是说输入的字符串可能是负数,也可能是浮点数,也可能是标准的科学记数法表示的实数,其中如果用科学记数法表示,e后的数字可能会带有负号,也就是e后的数字可能是一个负数。可能会有小伙伴担心输入的字符串不符合科学记数法规范,比如输入一个“100e2”字符串,在这里我要消除一下这些小伙伴的顾虑,经过我的测试,是没有这种不符合科学记数法规范的输入样例的。
接下来说一下我的解题思路。我看完题目后,第一想法便是用之前在《算法笔记》中学过的大整数运算来解决问题,虽说是大整数,但思路可迁移到浮点数实现大浮点数运算,这样无论输入的字符串有多长指数有多大统统可以解决掉。具体的解题思路便是首先将输入的字符串转换成大浮点数,如果输入的是用科学记数法表示的实数则将其转换成一般表示方法的实数再转换成大浮点数,这样两个大浮点数便可以实现加法运算和减法运算,然后在输出大浮点数时考虑将浮点数转换成科学记数法表示的实数输出,但如果是整数就不用管它了,直接输出就OK,也要考虑是否输出负号。
思路听起来很简单,实现起来确稍稍有些难度,因为要考虑的细节有很多,接下来先给大家我的AC代码,然后逐步分析讲解来帮助大家理解。
#include <stdio.h>
#include <string>
#include <algorithm>
#include <iostream>
using namespace std;
struct bigf {
string d, f;
bool isPlus;
};
void deleteZero(bigf& a) {
while (a.d[a.d.length() - 1] == '0' && a.d.length() > 1) {
a.d.erase(a.d.length() - 1,1);
}
while (a.f.length() > 0 && a.f[a.f.length() - 1] == '0') {
a.f.erase(a.f.end() - 1);
}
}
bigf change(string str) {
bigf a;
if (str[0] == '-') {
a.isPlus = false;
str.erase(str.begin());
}
else a.isPlus = true;
int e = 0;
int eIndex=str.find('e');
bool eIsPlus = true;
if (eIndex == -1)eIndex = str.find('E');
if (eIndex!=-1) {
int i = eIndex + 1;
if (str[i] == '-') {
eIsPlus = false;
i++;
}
for (; i < str.length(); i++) {
e = e * 10 + (str[i] - '0');
}
str.erase(eIndex, str.length() - eIndex);
if (!eIsPlus)e = 0 - e;
}
int pointIndex = str.find('.');
if (pointIndex == -1) {
if (e > 0) {
for (int i = 0; i < e; i++) {
a.d += '0';
}
}
else if (e < 0) {
a.d += '0';
for (int i = -1; i > e; i--) {
a.f += '0';
}
a.f += str[0];
}
for (int i = str.length() - 1; i >= 0; i--) {
a.d += str[i];
}
}
else {
pointIndex += e;
if (e >= 0) {
for (int i = pointIndex; i >= 0; i--) {
if (i >= str.length())a.d += '0';
else if (str[i] != '.') a.d += str[i];
}
for (int i = pointIndex + 1; i < str.length(); i++) {
a.f += str[i];
}
}
else {
a.d += '0';
for (int i = pointIndex; i < 0; i++) {
a.f += '0';
}
for (int i = 0; i < str.length(); i++) {
if (str[i] != '.')a.f += str[i];
}
}
}
deleteZero(a);
return a;
}
int cmp(bigf a, bigf b) {
if (a.d.length() < b.d.length())return -1;
else if (a.d.length() > b.d.length())return 1;
else {
for (int i = a.d.length() - 1; i >= 0; i--) {
if (a.d[i] < b.d[i]) return -1;
else if (a.d[i] > b.d[i]) return 1;
}
for (int i = 0; i < a.f.length() && i < b.f.length(); i++) {
if (a.f[i] < b.f[i])return -1;
else if (a.f[i] > b.f[i])return 1;
}
if (a.f.length() < b.f.length())return -1;
else if (a.f.length() > b.f.length())return 1;
else return 0;
}
}
bigf add(bigf a, bigf b) {
bigf c;
for (int i = 0; i < a.f.length() || i < b.f.length(); i++) {
c.f += '0';
}
while (a.d.length() < b.d.length()) {
a.d += '0';
}
while (a.d.length() > b.d.length()) {
b.d += '0';
}
int carry = 0;
for (int i = a.f.length() >= b.f.length() ? a.f.length() - 1 : b.f.length() - 1; i >= 0; i--) {
if (i >= a.f.length())c.f[i] = b.f[i];
else if (i >= b.f.length())c.f[i] = a.f[i];
else {
int temp = (a.f[i] - '0') + (b.f[i] - '0') + carry;
c.f[i] = (temp % 10) + '0';
carry = temp / 10;
}
}
for (int i = 0; i < a.d.length() || i < b.d.length(); i++) {
int temp = (a.d[i] - '0') + (b.d[i] - '0') + carry;
c.d += (temp % 10) + '0';
carry = temp / 10;
}
if (carry)c.d += carry + '0';
deleteZero(c);
return c;
}
bigf sub(bigf a, bigf b) {
bigf c;
for (int i = 0; i < a.f.length() || i < b.f.length(); i++) {
c.f += '0';
}
while (a.f.length() < b.f.length()) {
a.f += '0';
}
while (b.f.length() < a.f.length()) {
b.f += '0';
}
while (b.d.length() < a.d.length()) {
b.d += '0';
}
for (int i = a.f.length() - 1; i >= 0; i--) {
if (a.f[i] - b.f[i] < 0) {
if (i > 0)a.f[i - 1] -= 1;
else a.d[0] -= 1;
a.f[i] += 10;
}
c.f[i] = a.f[i] - b.f[i] + '0';
}
for (int i = 0; i < a.d.length(); i++) {
if (a.d[i] - b.d[i] < 0) {
a.d[i + 1] -= 1;
a.d[i] += 10;
}
c.d += a.d[i] - b.d[i] + '0';
}
deleteZero(c);
return c;
}
void print(bigf a) {
reverse(a.d.begin(), a.d.end());
if (!a.f.length()) {
cout << a.d << endl;
}
else {
int e = a.d.length() - 1;
if(e>0||e==0&&a.d[0]!='0')cout << a.d[0] << "." << a.d.substr(1, a.d.length() - 1) << a.f << "e" << e << endl;
else {
for (int i = 0; i < a.f.length(); i++) {
if (a.f[i] != '0') {
cout << a.f[i];
if (i != a.f.length() - 1)cout << ".";
cout << a.f.substr(i + 1, a.f.length() - i - 1) << "e-" << i + 1 << endl;
break;
}
}
}
}
}
int main() {
string stra, strb;
while (cin >> stra >> strb) {
bigf a = change(stra);
bigf b = change(strb);
if (a.isPlus && b.isPlus)print(add(a, b));
else if (!a.isPlus && !b.isPlus) {
cout << "-";
print(add(a, b));
}
else if (a.isPlus) {
if (cmp(a, b) == 1)print(sub(a, b));
else if (cmp(a, b) == -1) {
cout << "-";
print(sub(b, a));
}
else cout << 0 << endl;
}
else {
if (cmp(a, b) == -1)print(sub(b, a));
else if (cmp(a, b) == 1) {
cout << "-";
print(sub(a, b));
}
else cout << 0 << endl;
}
}
return 0;
}
先从结构体开始讲解:
struct bigf {
string d, f;
bool isPlus;
};
这个结构体便是一个类比于大整数的大浮点数,其中字符串d,f分别保存浮点数的整数部分和小数部分,需要注意的是,这个整数部分的保存顺序是和正常整数的数位顺序相反的,也就是说字符串下标为0的字符保存的是整数的最低位,整数的最高位保存在字符串的最后(可参考“《算法笔记》5.6.1 大整数的存储”),而浮点数部分的保存顺序与正常数位顺序一致,总之两个字符串的第一位保存的都是离小数点最近的那个数字。bool型变量isPlus是保存该大浮点数是否是正数的,false就表示该大浮点数是负数。
void deleteZero(bigf& a) {
while (a.d[a.d.length() - 1] == '0' && a.d.length() > 1) {
a.d.erase(a.d.length() - 1,1);
}
while (a.f.length() > 0 && a.f[a.f.length() - 1] == '0') {
a.f.erase(a.f.end() - 1);
}
}
这个函数是用来去除大浮点数多余的0的。在计算过程中以及处理最后得到的运算结果都需要用到这个函数,并且该函数可以将0.000这种表示的0直接处理成0,以免输出结果错误。一定要注意形参是引用传递,不然是无法真正去除大浮点数多余的0的!第一个while循环是处理整数部分高位0的,我就不过多解释了,看过“《算法笔记》5.6 大整数运算” 的小伙伴应该都记得,关键就是如果整数部分全部为0不要忘记保留一个0当作最后的整数。第二个while循环是处理浮点数部分低位0的,要和整数部分高位去0类比理解,需要注意的是此处如果浮点数部分全部为0是必须要清除所有的0的,不要像整数部分处理一样保留一个0,不然6.000000处理后的结果就会是6.0了,这明显不是最简结果呀!
bigf change(string str) {
bigf a;
if (str[0] == '-') {
a.isPlus = false;
str.erase(str.begin());
}
else a.isPlus = true;
int e = 0;
int eIndex=str.find('e');
bool eIsPlus = true;
if (eIndex == -1)eIndex = str.find('E');
if (eIndex!=-1) {
int i = eIndex + 1;
if (str[i] == '-') {
eIsPlus = false;
i++;
}
for (; i < str.length(); i++) {
e = e * 10 + (str[i] - '0');
}
str.erase(eIndex, str.length() - eIndex);
if (!eIsPlus)e = 0 - e;
}
int pointIndex = str.find('.');
if (pointIndex == -1) {
if (e > 0) {
for (int i = 0; i < e; i++) {
a.d += '0';
}
}
else if (e < 0) {
a.d += '0';
for (int i = -1; i > e; i--) {
a.f += '0';
}
a.f += str[0];
}
for (int i = str.length() - 1; i >= 0; i--) {
a.d += str[i];
}
}
else {
pointIndex += e;
if (e >= 0) {
for (int i = pointIndex; i >= 0; i--) {
if (i >= str.length())a.d += '0';
else if (str[i] != '.') a.d += str[i];
}
for (int i = pointIndex + 1; i < str.length(); i++) {
a.f += str[i];
}
}
else {
a.d += '0';
for (int i = pointIndex; i < 0; i++) {
a.f += '0';
}
for (int i = 0; i < str.length(); i++) {
if (str[i] != '.')a.f += str[i];
}
}
}
deleteZero(a);
return a;
}
这个函数可以说是核心函数了,功能就是将样例输入的字符串转换成大浮点数保存起来,比较恶心的是e的处理,下面带大家细致分析。
第一个if……else……语句是用来判断该字符串是否是一个负数的,如果是负数就令大浮点数结构体的isPlus变量为false,然后删除掉字符串前面的“-”,方便接下来的字符串转换,以及不用考虑负号问题。
接下来的int型变量e用来保存科学计数法表示的指数,具体的用途接下来会讲,eIndex用来保存字符串中“e”这个字符的下标,bool型变量eIsPlus用来保存指数e是否是一个负数。如果字符串中没有找到“e”,那么根据题目描述可能会有一个“E”,所以我们接下来找“E”的下标,如果找到了“E”的下标就说明该字符串是用科学记数法表示的,需要我们去处理指数用正常表示方法存储数字,下面的if语句便是做这件事的。首先令int型变量i是字符“e”或“E”下标的下一位来判断字符是否是“-”,如果是则说明指数e是一个负数,需要在最后对变量e取相反数,然后我们对之后的指数进行计算,而这利用一个for循环便可以实现,最后删除掉字符串中“e”或“E”及其之后的所有字符,方便之后的字符串转换。
进行到这一步,字符串只剩下了数字也有可能有小数点,所以我们要判断字符串中是否有小数点还要进行指数的处理,pointIndex保存小数点的下标,如果指数为正就可以理解为小数点下标向右移动e位,指数为负数就是小数点下标向左移动e的绝对值位,超过字符串长度缺位则补0。如果没有小数点就简单多了,说明该字符串只有整数部分,对应于代码中if(pointIndex==-1)这个判断语句。如果变量e不等于0说明这是一个用科学记数法表示的整数,且整数只有一位,其中大于0说明指数为正数,我们需要在其后添加e位的0,由于大浮点数的整数部分保存是逆序保存的,所以我们要先进行补0,然后再保存唯一的一位整数;但如果变量e小于0,就说明指数是负数,我们需要令大浮点数的整数部分为0,然后假设下标数为-1向左寻找小数点的理论下标并令浮点数部分补0,最后浮点数部分添加字符串唯一的整数(比如6e-2,e的值为-2,整数添加0后便是0.6,小数部分因为i=-1>e所以要添加0,最后结果便是0.06)。e等于0就说明字符串是一个正常的整数,我们直接令整数部分逆序存储字符串就OK了。但如果有小数点相对而言就要复杂一些,首先如前文所述先令小数点下标根据e的数值向右或左移动,判断e是非负数的话,就让大浮点数整数部分逆序存储字符串小数点理论位置前的整数,其中i下标超出界限的话就说明应该补0,由于会读取到小数点在字符串的位置,所以for循环条件为i≥0,小数点理论位置后的数字均为小数部分,直接保存即可。如果e小于0,说明字符串的所有整数都是小数部分,所以令大浮点数整数存0,浮点数部分存字符串所有数字即可。
最后要对得到的大浮点数进行去多余0的操作,此时之前的deleteZero函数派上用场。
int cmp(bigf a, bigf b) {
if (a.d.length() < b.d.length())return -1;
else if (a.d.length() > b.d.length())return 1;
else {
for (int i = a.d.length() - 1; i >= 0; i--) {
if (a.d[i] < b.d[i]) return -1;
else if (a.d[i] > b.d[i]) return 1;
}
for (int i = 0; i < a.f.length() && i < b.f.length(); i++) {
if (a.f[i] < b.f[i])return -1;
else if (a.f[i] > b.f[i])return 1;
}
if (a.f.length() < b.f.length())return -1;
else if (a.f.length() > b.f.length())return 1;
else return 0;
}
}
这是一个比较大浮点数的函数,在之后减法运算前会用到,用来决定是a-b还是b-a以及是否输出负号。函数的比较思路和大整数运算的比较函数比较思路一致,先比整数部分长度,长度相等就从高位开始比较整数,如果还是相等就从高位开始逐位比较小数(注意不要比较小数部分长度,难道0.000001比0.9大吗?),如果还是相等就说明这两个大浮点数相等。
bigf add(bigf a, bigf b) {
bigf c;
for (int i = 0; i < a.f.length() || i < b.f.length(); i++) {
c.f += '0';
}
while (a.d.length() < b.d.length()) {
a.d += '0';
}
while (a.d.length() > b.d.length()) {
b.d += '0';
}
int carry = 0;
for (int i = a.f.length() >= b.f.length() ? a.f.length() - 1 : b.f.length() - 1; i >= 0; i--) {
if (i >= a.f.length())c.f[i] = b.f[i];
else if (i >= b.f.length())c.f[i] = a.f[i];
else {
int temp = (a.f[i] - '0') + (b.f[i] - '0') + carry;
c.f[i] = (temp % 10) + '0';
carry = temp / 10;
}
}
for (int i = 0; i < a.d.length() || i < b.d.length(); i++) {
int temp = (a.d[i] - '0') + (b.d[i] - '0') + carry;
c.d += (temp % 10) + '0';
carry = temp / 10;
}
if (carry)c.d += carry + '0';
deleteZero(c);
return c;
}
大浮点数运算的加法运算,计算思路和大整数加法运算一致,不同有三点,一是因为大浮点数保存数字的字符串是string类型,不像大整数用的是int数组可以提前初始化赋0,所以就需要对计算结果c的小数部分和a,b两个加数的整数部分先补0再运算(其实呢也可以不补零,但运算起来处理细节就会麻烦许多),二是先要进行小数部分的加法,三是大浮点数保存数字是string字符串,会涉及到字符和整数的转换。具体的计算过程就不详细解释了,不理解的小伙伴可以参考“《算法笔记》5.6.2节 大整数的四则运算 1.高精度加法”。最后不要忘记对大浮点数运算结果c去除多余的0。(估计会有小伙伴想问为什么不去除掉加数a和b的整数部分多余的0,因为a,b在这里是值传递呀!)
bigf sub(bigf a, bigf b) {
bigf c;
for (int i = 0; i < a.f.length() || i < b.f.length(); i++) {
c.f += '0';
}
while (a.f.length() < b.f.length()) {
a.f += '0';
}
while (b.f.length() < a.f.length()) {
b.f += '0';
}
while (b.d.length() < a.d.length()) {
b.d += '0';
}
for (int i = a.f.length() - 1; i >= 0; i--) {
if (a.f[i] - b.f[i] < 0) {
if (i > 0)a.f[i - 1] -= 1;
else a.d[0] -= 1;
a.f[i] += 10;
}
c.f[i] = a.f[i] - b.f[i] + '0';
}
for (int i = 0; i < a.d.length(); i++) {
if (a.d[i] - b.d[i] < 0) {
a.d[i + 1] -= 1;
a.d[i] += 10;
}
c.d += a.d[i] - b.d[i] + '0';
}
deleteZero(c);
return c;
}
大浮点数的减法运算思路也和大整数减法运算的思路一致,不同之处也是有三点,一是要对运算结果c的小数部分、被减数a的小数部分、减数b的整数和小数部分补0,二是要对小数部分进行减法运算,三是保存的数字是字符,不是int型整数,需要进行二者转换。具体的减法运算过程也不多做解释了,请参考“《算法笔记》5.6.2节 大整数的四则运算 2.高精度减法”,最后不要忘记对结果c去除多余0。
void print(bigf a) {
reverse(a.d.begin(), a.d.end());
if (!a.f.length()) {
cout << a.d << endl;
}
else {
int e = a.d.length() - 1;
if(e>0||e==0&&a.d[0]!='0')cout << a.d[0] << "." << a.d.substr(1, a.d.length() - 1) << a.f << "e" << e << endl;
else {
for (int i = 0; i < a.f.length(); i++) {
if (a.f[i] != '0') {
cout << a.f[i];
if (i != a.f.length() - 1)cout << ".";
cout << a.f.substr(i + 1, a.f.length() - i - 1) << "e-" << i + 1 << endl;
break;
}
}
}
}
}
这是打印大浮点数的函数,可以将带有小数点的大浮点数以科学记数法的形式打印。第一句的reverse()函数将大浮点数的整数部分字符串反转,调整成顺序存储整数的字符串,方便之后的打印。如果大浮点数的小数部分字符串长度为0则说明该大浮点数是一个整数,直接输出即可,完全不需要转换成科学计数法表示的形式!如果大浮点数保存的是一个浮点数,那么就需要用科学记数法的形式输出,先假设浮点数是大于1的,那么指数e的值就是整数部分字符串的长度再减一,也就是说e大于0或者e为0但整数部分不为0,这时直接输出大浮点数的整数部分第一位紧接着输出小数点然后再输出整数部分其余位数和小数部分,最后输出e和e的值便是用科学记数法表示的大浮点数。但若是大浮点数保存的浮点数小于1即e为0且整数部分为0,那么就需要从小数部分最高位开始寻找第一个不是0的数字,找到之后将其输出作为科学记数法的整数,接下来判断在其之后是否还有其他数位,如果还有的话就说明科学记数法的小数部分需要输出,所以输出一个小数点,再输出之后所有数位的数字以及e和e的值,e的值便是第一个不是0的数字前面所有的0的数量(包括整数的0)即下标数加1,又因为该大浮点数小于1,所以e的值理论上是一个负数,但只需要多输出一个“-”即可。
int main() {
string stra, strb;
while (cin >> stra >> strb) {
bigf a = change(stra);
bigf b = change(strb);
if (a.isPlus && b.isPlus)print(add(a, b));
else if (!a.isPlus && !b.isPlus) {
cout << "-";
print(add(a, b));
}
else if (a.isPlus) {
if (cmp(a, b) == 1)print(sub(a, b));
else if (cmp(a, b) == -1) {
cout << "-";
print(sub(b, a));
}
else cout << 0 << endl;
}
else {
if (cmp(a, b) == -1)print(sub(b, a));
else if (cmp(a, b) == 1) {
cout << "-";
print(sub(a, b));
}
else cout << 0 << endl;
}
}
return 0;
}
最后主函数这里就是处理是否输出负号以及是进行加法运算还是减法运算。如果大浮点数a,b都是正数,那么很简单直接做加法运算输出结果即可;如果a,b都是负数,那么就多输出一个负号,然后让a和b进行加法运算;但如果a和b一正一负,那么就需要比较二者绝对值大小决定是否输出负号,然后让绝对值大的当被减数,打印输出结果。
补充一些测试样例,方便小伙伴测试自己写的代码
样例输入
34.56
2.45e23.57e2
345999
11e2
3.54e11
-11e2
-1001e20
4e153.732e-54
-5.78e-5530.805573
-45.105871
样例输出
2.7956e2
702
1000
1.354e2
0
0
100004000000000000000
3.154e-54-1.4300298e1