[leetcode] 大数乘法
给定两个表示非负整数的字符串num1和num2,返回也一个表示为字符串的num1和num2的乘积。要求不能转换成数字直接相乘。
https://leetcode.com/problems/multiply-strings/
按照小学数学多位数乘法法则,其算法复杂度为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算法的介绍可以看这里 https://mp.weixin.qq.com/s/GCCEJWbNscJyf4K5nleOnA
代码实现如下:
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);
}
网上提到还有更多好办法 https://blog.csdn.net/u010983881/article/details/77503519, 一并列举如下
编辑于 2019-05-06 11:54