《算法笔记》——第五章 大整数运算 学习记录
大整数运算
对一道A+B的题目,如果A和B的范围在int范围内,那么相信大家很快就能写出程序。但是如果A和B是有着1000个数位的整数,恐怕就没有办法用已有的数据类型来表示了,这时就只能老实去模拟加减乘除的过程。怎么样?听起来像是小学生学的东西吧?实际上原理就是小学的,所以不要去害怕这个看上去很高深的东西。此外,大整数又称为高精度整数,其含义就是用基本数据类型无法存储其精度的整数。
大整数存储
很简单,使用数组即可。例如定义int型数组d[1000],那么这个数组中的每一位就代表了存放的整数的每一位。如将整数235813存储到数组中,则有d[0]= 3, d[1]= 1, d[2]= 8, d[3]
=5, d[4]=3, d[5]=2,即整数的高位存储在数组的高位,整数的低位存储在数组的低位。
不反过来存储的原因是,在进行运算的时候都是从整数的低位到高位进行枚举,顺位存储和这种思维相合。但是也会由此产生一个需要注意的问题:把整数按字符串%s读入的时候,实际上是逆位存储的,即str[0] = '2', str[1]= '3',..., str[5]= '3',因此在读入之后需要在另存为至d[]数组的时候反转一下。
而为了方便随时获取大整数的长度,一般都会定义一个int 型变量len来记录其长度,并和d数组组合成结构体:
struct bignum
{
int d[1000];
int len;
};
显然,在定义结构体变量之后,需要马上初始化结构体。为了减少在实际输入代码时总是忘记初始化的问题,读者最好使用“构造函数”,即在结构体内部加上以下代码:
struct bignum
{
int d[1000];
int len;
bignum()
{
memset(d,0,sizeof d);
len=0;
}
};
“构造函数”是用来初始化结构体的函数,函数名和结构体名相同、无返回值,因此非常好写。
这样在每次定义结构体变量时,都会自动对该变量进行初始化。
而输入大整数时,一般都是先用字符串读入,然后再把字符串另存为至bignum结构体。由于使用char数组进行读入时,整数的高位会变成数组的低位,而整数的低位会变成数组的高位,因此为了让整数在bignum中是顺位存储,需要让字符串倒着赋给d[]数组:
bignum change(char str[])
{
bignum a;
a.len=strlen(str);
for(int i=0;i<a.len;i++)
a.d[i]=str[a.len-i-1]-'0';
return a;
}
如果要比较两个bignum变量的大小,规则也很简单:先判断两者的len大小,如果不相等,则以长的为大;如果相等,则从高位到低位进行比较,直到出现某一位不等,就可以判断两个数的大小。下面的代码直接依照了这个规则:
int compare(bignum a,bignum b)
{
if(a.len > b.len) return 1;
else if(a.len < b.len) return -1;
else
{
for(int i=a.len-1;i>=0;i--)
if(a.d[i] > b.d[i]) return 1;
else if(a.d[i] < b.d[i]) return -1;
}
return 0;
}
接下来主要介绍四个运算:
- 高精度加法
- 高精度减法
- 高精度与低精度的乘法
- 高精度与低精度的除法。
至于高精度与高精度的乘法和除法,考试一般不会涉及,因此留
给有兴趣的读者自行了解。
搞精度加法
以147+65为例,下面来回顾一下小学的时候是怎么学习两个整数相加的:
- 7+5=12,取个位数2作为该位的结果,取十位数1进位。
- 4+6,加上进位1为11,取个位数1作为该位的结果,取十位数1进位。
- 1+0,加上进位1为2,取个位数2作为该位的结果,由于十位数位0,因此不进位。
可以因此归纳出对其中一位进行加法的步骤:将该位上的两个数字和进位相加,得到的结果取个位数作为该位结果,取十位数作为新的进位。
高精度加法的做法与此完全相同,可以直接来看实现的代码:
bignum add(bignum a,bignum b)
{
bignum c;
c.len=max(a.len,b.len);
int carry=0;
for(int i=0;i<c.len;i++)
{
int t=a.d[i]+b.d[i]+carry;
c.d[i]=t%10;
carry=t/10;
}
if(carry) c.d[c.len++]=carry;
return c;
}
最后指出,这样写法的条件是两个对象都是非负整数。如果有一方是负的,可以在转换到数组这一步时去掉其负号,然后采用高精度减法;如果两个都是负的,就都去掉负号后用高精度加法,最后再把负号加回去即可。
高精度减法
以147-65为例,再来回顾一下小学的时候是怎么学习两个整数相减的:
- 5-7<0,不够减,因此从高位4借1,于是4减1变成3,该位结果为15-7=8。
- 3-6<0,不够减,因此从高位1借1,于是1减1变成1,该位结果为13-6=7。
- 上面和下面均为0,结束计算。
同样可以得到一个很简练的步骤:对某一步,比较被减位和减位,如果不够减,则令被减位的高位减1、被减位加10再进行减法;如果够减,则直接减。
最后一步要注意减法后高位可能有多余的0,要忽视它们,但也要保证结果至少有一位数。
高精度减法的完整代码即为把上面的sub函数替代高精度加法中add函数的位置即可,记得调用的时候也是用sub函数,这里就不再重复给出代码。
最后需要指出,使用sub函数前要比较两个数的大小,如果被减数小于减数,需要交换两个变量,然后输出负号,再使用sub函数。
bignum sub(bignum a,bignum b)
{
if(compare(a,b) == -1)
{
cout<<'-';
return sub(b,a);
}
bignum c;
c.len=a.len;
for(int i=0;i<c.len;i++)
{
if(a.d[i] < b.d[i])
{
a.d[i+1]--;
a.d[i]+=10;
}
c.d[i]=a.d[i]-b.d[i];
}
while(c.len > 1 && c.d[c.len-1] == 0)
c.len--;
return c;
}
高精度与低精度的乘法
所谓的低精度就是可以用基本数据类型存储的数据,例如int 型。这里讲述的就是bignum类型与int类型的乘法,其做法和小学学的有一点不一一样。以147x35为例,这里把147视为高精度bignum类型,而35视为int类型,并且在下面的过程中,始终将35作为一个整体看待。
- 7x35=245,取个位数5作为该位结果,高位部分24作为进位。
- 4x35= 140,加上进位24,得164,取个位数4为该位结果,高位部分16作为进位。
- 1x35=35,加上进位16,得51,取个位数1为该位结果,高位部分5作为进位。
- 没的乘了,此时进位还不为0,就把进位5直接作为结果的高位。
对某一步来说是这么一个步骤:取bignum的某位与int型整体相乘,再与进位相加,所得结果的个位数作为该位结果,高位部分作为新的进位。
完整的AxB的代码只需要把高精度加法里的add函数改成这里的mul函数,并注意输入的时候b是作为int型输入即可。
bignum mul(bignum a,int b)
{
bignum c;
c.len=a.len;
int carry=0;
for(int i=0;i<c.len;i++)
{
int t=a.d[i]*b+carry;
c.d[i]=t%10;
carry=t/10;
}
while(carry)//乘法的进位可能不止一位
{
c.d[c.len++]=carry%10;
carry/=10;
}
while(c.len > 1 && c.d[c.len-1] == 0) c.len--;//b == 0
return c;
}
另外,如果a和b中存在负数,需要先记录下其负号,然后取它们的绝对值代入函数。
高精度与低精度的除法
除法的计算方法和小学所学是相同的。以1234/7为例:
- 1与7比较,不够除,因此该位商为0,余数为1。
- 余数1与新位2组合成12,12与7比较,够除,商为1,余数为5。
- 余数5与新位3组合成53,53与7比较,够除,商为7,余数为4。
- 余数4与新位4组合成44,44与7比较,够除,商为6,余数为2。
归纳其中某一步的步骤:上一步的余数乘以10加上该步的位,得到该步临时的被除数,将其与除数比较:
- 如果不够除,则该位的商为0;
- 如果够除,则商即为对应的商,余数即为对应的余数。
最后一步要注意减法后高位可能有多余的0,要忽视它们,但也要保证结果至少有一位数。
bignum divide(bignum a,int b)
{
bignum c;
c.len=a.len;
for(int i=c.len-1;i>=0;i--)
{
r=r*10+a.d[i];
c.d[i]=r/b;
r%=b;
}
while(c.len > 1 && c.d[c.len-1] == 0) c.len--;
return c;
}
在上述代码中,考虑到函数每次只能返回一个数据,而很多题目里面会经常要求得到余数,因此把余数写成“引用”的形式直接作为参数传入,或是把r设成全局变量。引用在2.7.5节已有讲述,其作用是在函数中可以视作直接对原变量进行修改,而不像普通函数参数那样,在函数中的修改不影响原变量的值。这样当函数结束时,r的值就是最终的余数。