接上文
2.试商法
就是模拟竖式除法。实现较为复杂,具体思想如下:
假设被除数n位,除数m位。约定n≥m
首先,从被除数最高位开始,向低位截取m+1位,假设截得的数为t,用二分法试商,找到最大的q使得q * m ≤ t,并用t - q * m取得余数r,将它复制到被除数对应的位中。注意如果r的长度小于m+1则要在被除数对应位上补0。然后从被除数的次高位开始重复同样的运算,直至t的最低位对应被除数最低位为止。
将每次得到的q组合起来即为最终的商。最后一次得到的r为余数。
举例:
复杂度:每次相乘可以不必用大数相乘,而是使用大数乘int的方式,这样一次乘法的复杂度是线性的。每次循环要进行的乘法次数由使用的进制BASE决定。
共执行n-m次循环。总的复杂度为
O
(
l
o
g
2
B
A
S
E
∗
n
(
n
−
m
)
)
O(log_2BASE*n(n-m))
O(log2BASE∗n(n−m))
3.Knuth除法
这也是Java中BigInteger使用的除法之一。具体算法参见此处
这里要注意的是,由于Knuth除法要求除数最高位不小于进制的一半,因此要先进行调整:
int highest = divisor.data[divisor.eff_len - 1];
int d = MOD_BASE / (highest + 1);
//调整除数,下面使用Knuth除法
divided = divided.mulWithInt(d);
divisor = divisor.mulWithInt(d);
//注意长度可能改变。以下一切均以调整后为准
int aLen = divisor.eff_len,len = divided.eff_len;
highest = divisor.data[a.eff_len - 1];
不难看出,在调整后,如果两数位数相同,则最终结果为1.否则,可以按上面的办法,只不过将试商改为估商即可。完整实现如下:
Biginteger Divide(const Biginteger &a) const {
if(sign == 2||a.sign == 2) throw NullException();
//除数为0
if(a.sign == 0) throw DivByZeroException();
Biginteger divided(absolute()),divisor(a.absolute());
//被除数更小
if(divisor > divided) return Biginteger("0");
int highest = divisor.data[divisor.eff_len - 1];
int d = MOD_BASE / (highest + 1);
//调整除数,下面使用Knuth除法
divided = divided.mulWithInt(d);
divisor = divisor.mulWithInt(d);
//注意长度可能改变。以下一切均以调整后为准
int aLen = divisor.eff_len,len = divided.eff_len;
highest = divisor.data[aLen - 1];
Biginteger ret(len - aLen + 1);
if(len == aLen) {
ret.eff_len = 1;
ret.data[0] = 1;
}
for(int i = len - aLen - 1; i >= 0; i--) {
//从第i位开始向高位取aLen+1位。循环每执行一次,相当于进行一次试商(这里是估商)
Biginteger part = divided.getBits(i,1 + aLen);
//估计的商,一开始估为下界
int q = ((int64_t)part.data[aLen] * MOD_BASE + part.data[aLen - 1]) / highest - 2;
//注意取完后再去前导零,否则产生不可预见的后果
part.removeZero();
//当前余数
Biginteger rmder = part.Subt(divisor.mulWithInt(q));
//调整
while(rmder >= divisor) {
rmder = part.Subt(divisor.mulWithInt(q));
q++;
}
int j,k;
//拷贝数组,注意清0
for (j = i,k = 0; k < rmder.eff_len; j++, k++)
divided.data[j] = rmder.data[k];
for(; j < i + aLen; j++)
divided.data[j] = 0;
ret.data[i] = q % MOD_BASE;
ret.data[i+1] += q / MOD_BASE; //余数的处理
}
ret.eff_len = len - aLen;
if(ret.data[len-aLen] > 0) ret.eff_len++;
ret.sign = (sign == a.sign) ? 1 : -1;
return ret;
}
上面的代码while循环可以优化:
1.循环内部:不难发现q每加1,rmder就减divisor。由此可减少乘法操作。
2.循环条件:可以先估为上界,不难发现q每-1,rmder就加divisor,直到rmder>0为止。而rmder与0的比较可直接使用sign在常数时间内完成。
优化后如下:
while(rmder.sign < 0) {
rmder = rmder.Add(divisor);
q--;
}
相比上一种方法,估商最多进行3次,虽然复杂度不变,但常数大大减小。