前言
这篇文章主要是对于大整数类的设计过程中,如何实现并改进长除法(模拟竖式法)的一个总结。
高精度除法的分类和比较
虽然有些文章在讨论大整数的除法运算时,喜欢分成高精度除以高精度和高精度除以低精度(短除法)两种情况并分开实现,但在本文中将不做这种区分。事实上,短除法就是长除法的一个缩略。
按照wiki百科,除法大致可以分为以下两类:
- 慢速算法:每步只能确定一位的算法。长除法属于此类,时间复杂度为O(N2)。
- 快速算法:每步可以确定多位的算法。牛顿迭代法属于此类,配合用FFT实现的乘法,时间复杂度为O(NlogN)。
一些讨论
这里讨论的是为了实现大整数类所必需的一些基础。厘清它们对提升算法的效率是有意义的。
1. 关于底层
标准C++的语法中基本的数据类型都带有基本的四则运算,这里的底层主要指硬件或者编译器。
要实现大整数的除法运算,至少需要用到加、减两种基本运算,乘、除和取余虽然不是必须的,但我们仍然假定底层(硬件或者软件模拟)已经实现,因为后面的优化将用到它们。
2. 关于数据结构
数据结构有多种实现方式,互有优劣。数组最简单,但长度固定,需要考虑溢出。STL的vector不用担心溢出,但不是线程安全的。链表线程安全,但操作复杂且效率低下。
本文中我们将使用数组,其中数组的低位存储数值的低位,数组的最高位存储数值的符号。
3. 关于数据类型
数据类型的选择,取决于存储效率和处理能力。用1个int型存储1个十进制有效数字显然是低效的,但用long型存储9个十进制有效数字也未必最佳(假如系统是16位的,则long型的四则运算本身就需要软件模拟)。有些类型的大小也依赖于编译器的实现,这也是在设计过程中需要考虑的。
本文先从1个char型存储1位十进制有效数字开始设计,暂将优化(压位)放到后面讨论。这样的优点是可以使用字符串进行存储,在输入输出上有所便宜。
长除法的实现
1. 除法的基本原理
本质上,慢速算法都是通过减法来实现除法。不考虑效率的话,下面的代码就可以实现除法。
Integer Integer::operator/(const Integer& itg)const{
//...
//prod=*this;
//div=itg;
result=zero;
while(!(prod<div)){
prod=prod-div;
result=result+one;
}
//...
}
显然,这个实现的时间复杂度是指数级的。现实中大概只有幼儿园的小朋友会使用。
2. 优化策略
如何高效率地完成减法,是优化的一个方向。直观的办法是通过减去除数的某个已知的倍数,来达到提高效率的目的。下面的代码是一个最简单的实现。
Integer Integer::operator/(const Integer& itg)const{
//...
//prod=*this;
//div=itg;
result=zero;
quot=one;
base=div;
while(!(prod<div)){
if(prod<base){
quot=one;
base=div;
}else{
prod=prod-base;
result=result+quot;
quot=quot+quot;
base=base+base;
}
}
//...
}
这个实现的时间复杂度已经达到O(N2),但无奈常数太大。因为每次采用的是翻倍,所以这里计算N是以2为底取的对数,而常用的笔算方法是以10为底取的对数。而且每次计算都要调用大整数的加减法,这也是一笔不小的开销。
3. 长除法的直减版本
下面的代码是采用移位策略的实现,已是相当接近于列竖式笔算的版本。
Integer Integer::operator/(const Integer& itg)const{
Integer result=0,prod,div,base;
int loop,offset,quot,reg,trans=0;
/*处理除0异常*/
prod=*this;
if(*(prod.p+Length-1)=='-') *(prod.p+Length-1)='+';
div=itg;
if(*(div.p+Length-1)=='-') *(div.p+Length-1)='+';
while(!(prod<div)){
base=div;
quot=1;
//确定移位的具体值
loop=prod.index-1;
offset=base.index-1;
while(*(prod.p+loop)==*(base.p+offset)){
if(offset>0){
loop--;
offset--;
}else break;
}
if(char2int(*(prod.p+loop))>=char2int