首发于前端算法

[leetcode] 大数乘法

给定两个表示非负整数的字符串num1和num2,返回也一个表示为字符串的num1和num2的乘积。要求不能转换成数字直接相乘。

leetcode.com/problems/m

按照小学数学多位数乘法法则,其算法复杂度为O(n^2)。 程序如下:

  function mult(a, b){//by 司徒正美
    var cn = a.length + b.length;
    var c = new Array(cn).fill(0);
  /****
   * 两两相乘,并放进不同的格子里,如果里面有东西,则相加 by 司徒正美
   * 0 
   *   8 
   *     10
   *     4  
   *       5 
   */
    for(var i = 0; i < a.length; i++){
      for(var j = 0; j < b.length; j++){
         c[i+j+1] += Number( a[i] ) * Number( b[j] )
      }
    }
   // 处理进位   
    for(var i = cn-1; i >= 0; i--){
      var carry = Math.trunc(c[i] / 10);
      if(carry){
        c[i-1] +=  carry
      }
      c[i] = c[i] % 10
    }
    while(c[0] === 0){
        c.shift()
    }
    //处理最前面的零 by 司徒正美
    return c.join('') || '0'
  }

但显然这个太慢了,我们可以试一下Karatsuba算法, 其复杂度仅为3nlog3≈3n1.585(log3是以2为底的)。

Karatsuba算法的介绍可以看这里 mp.weixin.qq.com/s/GCCE



代码实现如下:

 function karatsuba(num1, num2) { //by 司徒正美
    if (num1.length == 1 || num2.length == 1) {
        //第1步 处理最简单的情况
        var s = multiply(num1, num2);
        // console.log("简单乘法:", num1, '*', num2, '=', s , s === (num1 * num2)+"")
        return s
    }
    //old1, old2 调试有

    var old1 = num1,
        old2 = num2
        ///第2步 将两个传参的长度补成相同的长度,用diffLen记录最后一步要减去的0
    var diffLen = num1.length - num2.length
    if (diffLen !== 0) {
        if (diffLen < 0) {
            diffLen = -1 * diffLen
            num1 = pad(num1, diffLen)
        } else {
            num2 = pad(num2, diffLen)
        }
        // console.log("diffLen", diffLen, num1, num2)
    }

    //第3步  拆分成更少的两个数,方便继续递归调用karatsuba算法
    //a, c分别存num1, num2的高半部分,b, d分别存num1, num2的低半部分
    //如果num1或num2的长为奇数,那么保证a,c的长度大于b, d
     var len = num1.length;
     var mid = len >> 1
     mid = len % 2 == 1 ? mid + 1 : mid;
    var a = num1.slice(0, mid)
    var b = trimZero(num1.slice(mid))
    var c = num2.slice(0, mid)
    var d = trimZero(num2.slice(mid))
    // console.log(num1 + "与" + num2 + "拆分成", a, b, c, d)
    //第4步  三次递归调用karatsuba算法
    var a_c = karatsuba(a, c);
    var b_d = karatsuba(b, d);
    var ab_cd = karatsuba(add(a, b), add(c, d));
    var diff = subtract(subtract(ab_cd, a_c), b_d);
    //第5步, 对a_c,diff 进行补零,然后相加
    var s1 = pad(a_c,  (len - mid) * 2);
    var s2 = pad(diff, len - mid );
    var s = add(add(s1, s2), b_d);
    //第6步, 去掉第2步补上的零
    if (diffLen) {
        s = s.slice(0, s.length - diffLen)
    }
    // console.log('s1,s2,s3:',a_c, diff, b_d)
    console.log("karatsuba", old1, '*', old2, '=', s, s === old1 * old2 + "");
    return s //by 司徒正美
}



//实现一个简单的乘法,有一方必须是一位数, 不考虑负数 by 司徒正美
function multiply(num1, num2) {
    if ("0" === num1 || "0" === num2) {
        return "0";
    }
    if ("1" === num1) {
        return num2;
    }
    if ("1" === num2) {
        return num1;
    }
    var longNumber, shortNumber;
    if (num1.length == 1) {
        longNumber = num2;
        shortNumber = num1 * 1; //转换为数字
    } else {
        longNumber = num1;
        shortNumber = num2 * 1; //转换为数字
    }
    var ret = "",
        carry = 0; //进位
    for (var i = longNumber.length - 1; i >= 0; i--) {
        var num = longNumber[i] * 1
        var temp = num * shortNumber + carry
        ret = temp % 10 + '' + ret;
        carry = Math.trunc(temp / 10);
    }
    if (carry > 0) {
        ret = carry + '' + ret;
    }
    return ret;
}

//相当于为它乘以 Math.pow(10, len)
function pad(num, len) {
    if (num == "0") {
        return num;
    }
    for (var i = 0; i < len; i++) {
        num += "0";
    }
    return num;
}

function trimZero(str) {
    while (str[0] === '0') {
        str = str.slice(1)
    }
    return str || "0"
}

function add(num1, num2) {
    if (num2[0] === '-') {
        return subtract(num1, num2.slice(1))
    }
    var a, b; //a的长度较长
    if (num1.length >= num2.length) {
        a = num1;
        b = num2;
    } else {
        a = num2;
        b = num1;
    }

    var ret = '',
        carry = 0,
        diff = a.length - b.length;
    for (var i = a.length - 1; i >= 0; i--) {
        var addend = a.charAt(i) * 1; //加数
        var adder = i - diff < 0 ? 0 : b.charAt(i - diff) * 1; //被加数
        var temp = addend + adder + carry;
        ret = temp % 10 + '' + ret;
        carry = Math.trunc(temp / 10);
    }
    if (carry > 0) {
        ret = carry + '' + ret;
    }
    //  console.log("简单加法:", num1, '+', num2, '=', ret, '!')
    return ret;
}

function subtract(num1, num2) { //减法,小心负号 by 司徒正美
    //console.log(num1, num2)
    if (num2[0] === '-') {
        return add(num1, num2.slice(1))
    }
    if (num1[0] === '-') {
        return '-' + add(num1.slice(1), num2)
    }
    var len1 = num1.length;
    var len2 = num2.length;
    var negative = false;
    // 相等
    if (num1 == num2) {
        return "0";
    }
    var a = null, //bigNumber
        b = null; //smallNumber
    //先确定谁大谁少
    if (len1 > len2) {
        a = num1;
        b = num2;
    } else if (len1 < len2) {
        a = num2;
        b = num1;
        negative = true
    } else {
        for (var i = 0; i < len1; i++) {
            if (num1[i] * 1 > num2[i] * 1) {
                a = num1;
                b = num2;
                break;
            } else if (num2[i] * 1 > num1[i] * 1) {
                a = num2;
                b = num1;
                negative = true
                break;
            }
        }
    }
    var carry = 0,
        ret = '',
        diff = a.length - b.length;
    for (var i = a.length - 1; i >= 0; i--) {
        var subtrahend = a.charAt(i) * 1;
        var subtractor = i - diff < 0 ? 0 : b.charAt(i - diff) * 1;
        var temp = subtrahend - subtractor - carry;
        if (temp < 0) {
            temp += 10;
            carry = 1;
        } else {
            carry = 0;
        }
        ret = temp + '' + ret;
    }
    while (ret[0] === '0') {
        ret = ret.slice(1)
    }
    ret = trimZero(ret)
    if (negative) {
        ret = '-' + ret;
    }
    // console.log("简单减法:", num1, '-', num2, '=', ret,  (num1 - num2)+"" == ret)
    return ret
}
//  20与145
 karatsuba("55", "44")
 console.log(55 * 44, "普通相乘")
 karatsuba("20", "145")
 console.log(20 * 145, "普通相乘")
 karatsuba("155", "5788")
 console.log(155 * 5788, "普通相乘")
 karatsuba("49", "54")
 console.log(49 * 54, "普通相乘")
 karatsuba("24566", "452053")
 console.log(24566 * 452053, "普通相乘")
 karatsuba("12345001", "1006789")
 console.log(12345001 * 1006789, '普通相乘')

奇怪的是,第二种实现比第一种慢?!难道是因为JS不是 long , 弄得大数相加,相减,相除等子函数需要重新实现拖慢性能了吗?下面是 JAVA的实现

 /**
 * Karatsuba乘法
 */
public static long karatsuba(long num1, long num2){
    //递归终止条件
    if(num1 < 10 || num2 < 10) return num1 * num2;
    // 计算拆分长度
    int size1 = String.valueOf(num1).length();
    int size2 = String.valueOf(num2).length();
    int halfN = Math.max(size1, size2) / 2;
    /* 拆分为a, b, c, d */
    long a = Long.valueOf(String.valueOf(num1).substring(0, size1 - halfN));
    long b = Long.valueOf(String.valueOf(num1).substring(size1 - halfN));
    long c = Long.valueOf(String.valueOf(num2).substring(0, size2 - halfN));
    long d = Long.valueOf(String.valueOf(num2).substring(size2 - halfN));
    // 计算z2, z0, z1, 此处的乘法使用递归
    long z2 = karatsuba(a, c);
    long z0 = karatsuba(b, d);
    long z1 = karatsuba((a + b), (c + d)) - z0 - z2;
    return (long)(z2 * Math.pow(10, (2*halfN)) + z1 * Math.pow(10, halfN) + z0);
}

网上提到还有更多好办法 blog.csdn.net/u01098388, 一并列举如下

编辑于 2019-05-06 11:54