今天看汇编语言时了解到DIV指令可能会导致除法溢出,王爽老师提供了一种不会溢出的算法,一时间突发奇想,想到了大整数除法的问题.

一、DIV指令除法溢出以及解决方案

我们知道在进行16位除法时,也就是被除数为32位除数位16位的除法,运算结果是AX存放除法操作的商,DX存放的是除法操作的余数,举一个最简单的例子:若被除数大于65536,除数恰好为1,此时AX无法存放大于65536的值,除法溢出.

那么,如何解决这个问题呢?王爽老师给出一个公式:X/n = int(H/n)65536+(rem(H/n)65536+L)/n 除法操作的商将存在两个寄存器中,现假设A,B寄存器,A寄存器存放高16位,B寄存器存放低16位[其中,int代表取整,rem代表求余数,H代表被除数X的高16位,L代表被除数X的低16位],下面给出证明:

  • 注意到 X/n = (H*65536 + L) / n = (H*65536)/n + L/n [此处不是整数除法,故等式成立]
  • 我们知道 H = int(H/n)*n + rem(H/n) 
  • 带入步骤一的等式,我们得到:X/n = (H*65536)/n + L/n = [(int(H/n)*n + rem(H/n)) * 65536] / n + L/n
  • 化简等式得到:X/n = int(H/n)*65536+(rem(H/n)*65536+L)/n 公式证毕
  • 现在证明不会出现溢出:
  • 这里会进行两次除法 H/n 和 (rem(H/n)*65536+L)/n
  • H/n 显然不会溢出,此时可将操作结果放在A寄存器
  • 因为 rem(H/n) < n,L < 65536,要证明除法不会溢出只需证明被除数的最大值((n-1)*65536+65535)/n除n不会溢出
  • 显然该结果一定小于65536,不会溢出 证毕

汇编算法实现(AX,DX分别存放商的低位和高位,CX存放余数,算法为DX,AX除CX)

divdw:
    push bx
    push ax
    mov ax,dx
    mov dx,0
    div word ptr cx 
    mov bx,ax
    pop ax
    div word ptr cx
    mov cx,dx
    mov dx,bx
    pop bx
    ret

二、大整数除法

根据上面的思路,我们很容易有推广的想法在更大数据的除法上(虽然效率并不如现有算法),若大整数除法的除数过小,我们也可以进行上述的算法,用同样的方法可以证明算法的正确性.

static class R{
    ArrayList<Integer> ret = new ArrayList<Integer>();
    int rem = 0 ;
    @Override
    public String toString() {
        return "R [ret=" + ret + ", rem=" + rem + "]";
    }
}
public static R calculate(ArrayList<Integer> arr,int num){
    R r = new R() ;
    int t,l ,f = 1;
    for (int i = 0; i < arr.size(); ++i) {
        r.rem *= 10;
        l = ( arr.get(i) + r.rem ) ;
        t = l / num ;
        if(t == 0 && f == 1 ) {}
        else {
            r.ret.add(t);
            f = 0 ;
        }
        r.rem = l % num ;
    }
    return r ; 
}

calculate函数用一个数组结构arr存放大整数除法被除数的每一位(十进制存储),而num代表除数,采用上面同样的算法.

我们知道Java中一个BigInteger类.点击它的除法源代码,我们可以看到:

    public BigInteger[] divideAndRemainder(BigInteger val) {
        if (val.mag.length < BURNIKEL_ZIEGLER_THRESHOLD ||
                mag.length - val.mag.length < BURNIKEL_ZIEGLER_OFFSET) {
            return divideAndRemainderKnuth(val);
        } else {
            return divideAndRemainderBurnikelZiegler(val);
        }
    }

这里根据数的大小采用两种不同的算法,当被除数小于80位或者当两个数位数相差小于80位采用Knuth算法,其他情况采用Burnikel-Ziegler算法

  • Knuth 大概是一种估算商的减法算法(商比较小的情况)
  • Burnikel-Ziegler 与上述算法类似,只是划分方式不同(商比较大的情况)

当然calculate算法是完全比不过这两种算法的,不过也算是一个简单的算法吧,经过测试与JDK的算法相差一个系数相差5-10倍的样子

测试代码:

import java.math.BigInteger;
import java.util.ArrayList;

public class Main {
static class R{
    ArrayList<Integer> ret = new ArrayList<Integer>();
    int rem = 0 ;
    @Override
    public String toString() {
        return "R [ret=" + ret + ", rem=" + rem + "]";
    }
}
    public static int randomInt(){
        return (int)(Math.random() * 100) % 10  ;
    }
    public static ArrayList<Integer> randomNumber(int len){
        ArrayList<Integer> ret = new ArrayList<Integer>();
        int t = randomInt() ;
        while(t == 0 ) t = randomInt() ;
        ret.add(t);
        while(--len != 0) ret.add(randomInt());
        return ret ; 
    }
    public static String randomNumber(ArrayList<Integer> arr){
        StringBuffer sb = new StringBuffer() ;
        for (Integer integer : arr) {
            sb.append(integer) ;
        }
        return sb.toString() ;
    }
    public static R calculate(ArrayList<Integer> arr,int num){
        R r = new R() ;
        int t,l ,f = 1;
        for (int i = 0; i < arr.size(); ++i) {
            r.rem *= 10;
            l = ( arr.get(i) + r.rem ) ;
            t = l / num ;
            if(t == 0 && f == 1 ) {}
            else {
                r.ret.add(t);
                f = 0 ;
            }
            r.rem = l % num ;
        }
        return r ; 
    }
    public static BigInteger[] caculate(BigInteger a,BigInteger b){
        return a.divideAndRemainder(b);
    }
    public static boolean accuracyTest(BigInteger[] b,R r){
        return b[0].toString().equals(randomNumber(r.ret)) && (r.rem+"").equals(b[1].toString()) ;
    }
    public static void main(String[] args) {
        System.out.println("preparing data...");
        ArrayList<ArrayList<Integer>> testSet1 = new ArrayList<ArrayList<Integer>>() ;
        ArrayList<BigInteger> testSet2 = new ArrayList<BigInteger>();
        ArrayList<Integer> testSet3 = new ArrayList<Integer>() ;
        ArrayList<BigInteger> testSet4 = new ArrayList<BigInteger>() ;
        ArrayList<R> retSet1 = new ArrayList<Main.R>();
        ArrayList<BigInteger[]> retSet2 = new ArrayList<BigInteger[]>();
        for (int i = 0; i < 10; i++) {
            testSet1.add(randomNumber(40000));
        }
        for (int i = 0; i < testSet1.size(); i++) {
            testSet2.add(new BigInteger(randomNumber(testSet1.get(i))));
        }
        for (int i = 0; i < testSet1.size(); i++) {
            testSet3.add((int) (Math.random() * Integer.MAX_VALUE / 10) ) ;
            testSet4.add(new BigInteger(""+testSet3.get(i)));
        }
        System.out.println("------------------");
        System.out.println("start testing..");
        long start = System.currentTimeMillis() ; 
        for (int i = 0; i < testSet1.size(); i++) {
            retSet2.add(testSet2.get(i).divideAndRemainder(testSet4.get(i)));
//          retSet1.add(calculate(testSet1.get(i), testSet3.get(i)));

        }
        long end = System.currentTimeMillis() ; 
        System.out.println("calculate algorithm time-elapse : "+ (end - start ));
//      start = System.currentTimeMillis() ;
//      for (int i = 0; i < testSet1.size(); i++) {
//          retSet2.add(testSet2.get(i).divideAndRemainder(testSet4.get(i)));
//      }
//      end = System.currentTimeMillis() ; 
//      System.out.println("jdk algorithm time-elapse : "+ (end - start ));
//      System.out.println("accuracy testing...");
//      int errors = 0 ; 
//      for (int i = 0; i < testSet1.size(); i++) {
//          if(!accuracyTest(retSet2.get(i), retSet1.get(i))){errors++;}
//      }
//      System.out.println("Error count:"+errors);
    }
}

 

 

分类: 编程