相信大家被特别大的两个数据做运算折磨过。当两个操作数或者运算结果超过类型的表示范围后会有意想不到的错误,这时候我们的电脑还不如我们高中用过的科学计算器,这是作为一个程序员所不能忍受的。所以我们得找到其他的方式来计算。这就是我们今天要讨论的字符串模拟大数运算。
我们的运算一般使用int类型来算的,那么首先我们先复习一下各种int类型的数据表示范围:
unsigned int 0~4294967295
int -2147483648~2147483647
unsigned long 0~4294967295
long -2147483648~2147483647
long long的最大值:9223372036854775807
long long的最小值:-9223372036854775808
unsigned long long的最大值:1844674407370955161
__int64的最大值:9223372036854775807
__int64的最小值:-9223372036854775808
unsigned __int64的最大值:18446744073709551615
可以看到,在64位操作系统下,long long int表示的最大范围是-9223372036854775808–9223372036854775807所以当我们的两个操作数或者运算结果超过这个范围我们就定义它已经溢出,得用字符串来模拟运算。所以我们得有一个_IsINT64OverFlow()函数,用来判断是否溢出:
bool BigData:: _IsINT64OverFlow()const
{
if (_value >= Min_INT64 && _value <= Max_INT64)
return false;
return true;
}
我们是用字符串来模拟的,用一个类来封装大数运算的加减乘除这些功能,所以先设计一下BigData这个类的基本构架。
#ifndef BIGDATA1_H
#define BIGDATA1_H
#include<iostream>
#include<string>
#include<assert.h>
#define Max_INT64 9223372036854775807
#define Min_INT64 (-9223372036854775807-1)
//不能直接用-9223372036854775808,当编译器看到9223372036854775808时直接判定
//9223372036854775808>INT64_MAX,直接用unsigned int64表示。当编译器看到负号时,
//直接对9223372036854775808取反,直接是它本身,编译器存不了那么大的数,报错
#define INT64 long long int
using namespace std;
class BigData
{
public:
BigData(INT64 data);
BigData(const char *str);
BigData operator+(BigData& d);//加法
BigData operator-(BigData& d);//减法
BigData operator*(BigData& d);//乘法
BigData operator/(BigData& d);//除法
private:
friend string Add(string& left, string& right);
friend string Sub(string& left, string& right);
friend string Mul(string& left, string& right);
friend string Div(string& left, string& right);
bool _IsINT64OverFlow()const;//判断数据是否溢出
friend ostream& operator<<(ostream& _cout, const BigData& d);
void _INT64ToString();//将long long int数据转换成字符串
private:
string _strvalue;
INT64 _value;
};
#endif
这里有一个问题就是在用-9223372036854775807表示INT64_MIN时出现了一些问题;
error C4146: 一元负运算符应用于无符号类型,结果仍为无符号类型。
那时候各种搞不懂,然后就查了一下各位大神的解释,大体意思就是不能直用-9223372036854775808表示。当编译器看到9223372036854775808时直接判定9223372036854775808>INT64_MAX,直接用unsigned int64表示。当编译器看到负号时,直接对9223372036854775808取反,直接是它本身,编译器存不了那么大的数,编译器就报错。详细解释见一元负运算符。
现在大体的框架已经搭好了。来看详细的实现过程:
(一)两个构造函数
BigData::BigData(INT64 data)
:_value(data)
{
_INT64ToString();
}
BigData::BigData(const char *str)
: _value(0)
{
if (str == NULL)
{
assert(false);
return;
}
char symbol;
if (str[0] == '+')
{
symbol = '+';
str++;
}
else if (str[0] == '-')
{
symbol = '-';
str++;
}
else if (str[0] >= '0'&&str[0] <= '9')
{
symbol = '+';
}
else
{
return;
}
char* tmpstr = (char*)str;
while (*tmpstr == '0')//跳过前面的‘0’
tmpstr++;
int i = 0;//剩下字符串的长度
while (*tmpstr >= '0'&& *tmpstr <= '9')
{
i++;
_value = _value * 10 + *tmpstr - '0';
tmpstr++;
}
if (symbol == '-')
{
_value = 0 - _value;
}
_strvalue.resize(i + 1);//相当于给_strvalue开辟空间
_strvalue[0] = symbol;
int j = 1;
while (i--)
{
_strvalue[j++] = *str++;
}
}
void BigData::_INT64ToString()
{
INT64 tmp = _value;
INT64 sym = tmp;
string str;
if (sym >= 0)
{
str.push_back('+');
}
else
{
str.push_back('-');
tmp = 0 - tmp;
}
while (tmp)
{
char ctmp = tmp % 10 + '0';
str.push_back(ctmp);
tmp /= 10;
}
int right = str.size()-1;
int left = 1;
while (left < right)
{
swap(str[left++], str[right--]);
}
_strvalue = str;
}
使用字符串构造比较麻烦,我们在构造_strvalue的时候还要把字符串数据转换为long long int类型的_value,方便以后计算,如果字符串表示的数据没有溢出的话直接用内置的long long int来计算。字符串转换为int的重点就是要从字符串的最后一个字符开始转化,每次循环数据乘以10。最后可以算出整个字符串的值,如果是负数,用0-_value即可。
还有long long int类型转换为字符串函数。算法不难,只是字符串的第一个字符统一保存数据的符号,方便以后好计算。
(二)加法
BigData BigData::operator+( BigData& d)
{
if (!_IsINT64OverFlow() && !d._IsINT64OverFlow()
&& (_value + d._value) <= Max_INT64 && (_value + d._value) >= Min_INT64)
{
_value += d._value;
}
else
{
OverflowFlag = true;
_strvalue = Add(_strvalue, d._strvalue);
}
return *this;
}
string Add(string& left, string& right)
{
if (left[0] != right[0])//符号不等
{
if (left[0] == '+')
{
right[0] = '+';
return Sub(left, right);
}
else
{
left[0] = '+';
return Sub(right, left);
}
}
else
{
int lsize = left.size();
int rsize = right.size();
if (lsize == rsize)
{
int carry = 0;
while (--lsize && --rsize)
{
char tmp = left[lsize];
left[lsize] = (left[lsize] - '0' + right[rsize] - '0') % 10 + carry + '0';
carry = (tmp - '0' + right[rsize] - '0') / 10;
}
if (carry == 1)
{
left.insert(1, "1");
}
return left;
}
else
{
if (lsize > rsize)
{
int carry = 0;//进位
while (--lsize && --rsize)//不能为--rsize&&-lsize
{
char tmp = left[lsize];
left[lsize] = (left[lsize] - '0' + right[rsize] - '0') % 10 + carry + '0';
carry = (tmp - '0' + right[rsize] - '0') / 10;
}
while (carry == 1)
{
left[lsize] = left[lsize] + carry;
carry = (left[lsize] - '0' + carry) / 10;
lsize--;
}
return left;
}
else
{
int carry = 0;
while (--rsize && --lsize)//注意不能为--lsize&&--rsize,
//当lsize为1时不执行--lsize直接跳出
{
char tmp = right[rsize];
right[rsize] = (left[lsize] - '0' + right[rsize] - '0') % 10
+ '0' + carry;
carry = (tmp - '0' + left[lsize] - '0') / 10;
}
while (carry == 1)//当进位为1就一直往前加进位
{
right[rsize] = right[rsize] + carry;
carry = (right[rsize] - '0' + carry) / 10;
rsize--;
}
return right;
}
}
}
}
加减乘除法都是用+-*/的重载来实现,实现时自己写的ADD,SUB,MUL,DIV。+调用ADD,-调用SUB,*调用MUL,/调用DIV。以后+-*/的重载函数我就不贴出来了,换个调用函数就行。这样的话方便以后的相互调用,只需要修改一下符号位。因为乘法是用加法模拟的,除法使用减法模拟的,减法用加法模拟的,按理来说我们使用加法就可以实现所有的运算。但是那个效率真的是惨不忍睹。
在这里,ADD的算法核心就是要保存低位向高位的进位。和我们手算是一样的。从两个字符串的最后一位开始往前相加,直到有一个字符串遇到_strvalue[0]的字符位为止,最后还要记得把最后的进位加上。在这里要考虑被加数加上进位以后还有进位的情况,所以在这我们使用了while来循环加。不用担心字符串的空间不够,因为两个位数一样的数相加,最多进位为1.
(三)减法
string Sub(string& left, string& right)
{
if (left[0] != right[0])
{
if (left[0] == '+')
{
right[0] = '+';
return Add(left, right);
}
else
{
right[0] = '-';
return Add(left, right);
}
}
else
{
int lsize = left.size();
int rsize = right.size();
if (lsize == rsize)
{
int borrow =0;
while (--lsize && --rsize)
{
if (left[lsize] < right[rsize])
{
left[lsize] = left[lsize] + 10 - right[rsize] - borrow + '0';
borrow = 1;
}
else
{
left[lsize] = left[lsize] - right[rsize] - borrow + '0';
borrow = 0;
}
}
return left;
}
else if (lsize > rsize)
{
int borrow = 0;
while (--lsize && --rsize)
{
if (left[lsize] < right[rsize])
{
left[lsize] = left[lsize] + 10 - right[rsize] - borrow + '0';
borrow = 1;
}
else
{
left[lsize] = left[lsize] - right[rsize] - borrow + '0';
borrow = 0;
}
}
while ( borrow==1 )
{
if (left[lsize] == '0')
{
left[lsize] = left[lsize] - '0' + 10 - borrow + '0';//若借位为0,
//向更高位借位,eg:1000-10
lsize--;
}
else
{
left[lsize] = left[lsize] - '0' - borrow + '0';
borrow = 0;
}
}
return left;
}
else
{
int borrow = 0;
while (--rsize && --lsize)
//得先让rsize--,若--lsize为0;将不会执行--rsize;
{
if (right[rsize] < left[lsize])
{
right[rsize] = right[rsize] + 10 - left[lsize] - borrow + '0';
borrow = 1;
}
else
{
right[rsize] = right[rsize] - left[lsize] - borrow + '0';
borrow = 0;
}
}
while (borrow == 1)
{
if (right[rsize] == '0')
{
right[rsize] = right[rsize] - '0' + 10 - borrow + '0';//若借位为0,
//向更高位借位,eg:1000-10
rsize--;
}
else
{
right[rsize] = right[rsize] - '0' - borrow + '0';
borrow = 0;
}
}
return right;
}
}
}
减法的算法核心和加法差不多,每次从两个字符串的最后一位开始计算。要定义一个借位,低位向高位的借位。只要借位borrow为1就一直循环借位。
加法和减法之间可以相互调用,当一个正数加一个负数时就可以调用减法,会很方便,而且易懂。这里就体现了我们封装ADD,SUB,MUL,DIV的好处。
还有要注意的就是要用最大的字符串(最长的字符串)来减小的字符串。这样可以保证结果用最长的字符串就可以保存,不用考虑空间的问题。
(四)乘法
string Mul(string& left, string& right)
{
string newstr;//创建一个临时sting存放相乘后的结果
int lsize = left.size();
int rsize = right.size();
newstr.resize(lsize + rsize);
int newsize = newstr.size();
while (--newsize)//初始化string,如果不初始化,string里存的是‘\0’
{
newstr[newsize] = '0';
}
if (left[0] != right[0])//符号不等
{
newstr[0] = '-';
}
else
{
newstr[0] = '+';
}
int flag = 0;//标志每次积的最低位
int carry = 0;
if (lsize <= rsize)
{
while (--lsize)
{
newsize = newstr.size() - flag;
rsize = right.size();
while (--rsize)
{
char tmp = left[lsize];
newstr[--newsize] = ((left[lsize] - '0') * (right[rsize] - '0')
+ newstr[newsize]-'0') % 10 + carry + '0';
carry = ((tmp - '0') * (right[rsize] - '0')) / 10;
}
newstr[--newsize] = carry + '0';//把最后的进位存起来
flag++;
}
}
else
{
while (--rsize)
{
newsize = newstr.size() - flag;
lsize = left.size();
while (--lsize)
{
char tmp = left[lsize];
newstr[--newsize] = ((left[lsize] - '0') * (right[rsize] - '0')
+ newstr[newsize] - '0') % 10 + carry + '0';
carry = ((tmp - '0') * (right[rsize] - '0')) / 10;
}
newstr[--newsize] = carry + '0';
flag++;
}
}
return newstr;
}
乘法的话就略微抽象一点,只要把握住一点,保存进位就会非常简单。在写之前应该想清楚的是进位的最大值,乘法中进位的最大值为9,所以也不用考虑空间的问题。最长的字符串完全可以存下来。
乘法中要注意的是不能破环两个乘数的值,如果修改了会产生意想不到的结果。所以定义一个newstr来存放结果而不像加减法那样直接在最长的串上操作。
(四)除法
string Div(string& left, string& right)
{
string newstr;//创建一个临时sting存放相除后的结果
int lsize = left.size();
int rsize = right.size();
newstr.resize(lsize);
if (left[0] != right[0])
{
newstr[0] = '-';
}
else
{
newstr[0] = '+';
}
if (lsize < rsize)
{
newstr.push_back('0');
return newstr;
}
else
{
left[0] = '+';
right[0] = '+';
int i = 0;
int flag = rsize;
int j = 0;
string tmp;
tmp.resize(rsize);
while (j < flag)//将left的高位复制给临时变量
{
tmp[j] = left[j];
j++;
}
j--;
while (j < lsize)
{
newstr[j] = '0';
while (Compare(tmp, right))
{
newstr[j]++;
tmp = Sub(tmp, right);
}
tmp.push_back(left[++j]);
}
return newstr;
}
}
除法说难也难,说简单也简单。要想简单的话我们直接用一个循环就可以搞定,循环相减,直到被减数小于减数。但是程序员总是不会屑于写这种效率低到爆的代码的。现在限于个人的知识范围,能想到效率最高的算法就是从被除数字符串截下和除数字符串一样长的字符串相减,使用一个newstr来标记商,newstr长度和被除数长度一样,全部初始化为‘0’。每次在与被除数相同下标的值++。直到被除数小于除数,再将原字符串的下一位push_back()到newstr,重复以上步骤。
其他函数较为简单,在这就不一一详述了,现在一个字符串模拟大数运算就写好了,可以丢弃手中的科学计算器,让我们的代码跑起来。