大整数相乘问题总结以及Java实现
最近在跟coursera上斯坦福大学的算法专项课,其中开篇提到了两个整数相乘的问题,其中最简单的方法就是模拟我们小学的整数乘法,可想而知这不是比较好的算法,这门课可以说非常棒,带领我们不断探索更优的算法,然后介绍可以通过使用分而治之的思想来解决这个问题。下面对该问题的方法以及实现进行介绍。
问题定义
输入:2个n位的整数x和y
输出:x * y
如求: 1234567891011121314151617181*2019181716151413121110987654 的结果
求解该问题要注意的是由于整数的位数可能超过基本类型的表示范围,所以一种方式是将其转化为字符串进行表示,另一种方式是可以使用一些语言自带的大整数类型(如Java的BigInteger)。参考一些资料才发现,该问题的解法其实有很多种,主要列举以下:
1.模拟小学乘法: 竖式乘法累加。
2.分治乘法: 最简单的是Karatsuba乘法,一般化以后有Toom-Cook乘法.
3.快速傅里叶变换
4.中国剩余定理
我们主要介绍模拟乘法累加以及使用分治思想的Karatsuba乘法,最后使用Java进行实现。
模拟小学乘法
7 8 9 6 5 2
× 3 2 1 1
-----------------
7 8 9 6 5 2 <---- 第1趟
7 8 9 6 5 2 <---- 第2趟
.......... <---- 第n趟
-----------------
? ? ? ? ? ? ? ? <---- 最后的值用另一个数组表示
如上所示,需要将乘数与被乘数逐位相乘,最后再进行累加,时间复杂度为{O(n^2)}.模拟乘法累加还有一个改进版,上述方法在实现时,每次计算乘法都需要考虑进位,最后在加法时也需要进位,比较麻烦。一种改进的版本如下:
9 8
× 2 1
-------------
(9)(8) <---- 第1趟: 98×1的每一位结果
(18)(16) <---- 第2趟: 98×2的每一位结果
-------------
(18)(25)(8) <---- 这里就是相对位的和,还没有累加进位
改进的方法先不算任何的进位,也就是说,将每一位相乘,相加的结果保存到同一个位置,到最后才计算进位。我们可以先将结果保存到一个数组中(不考虑进位),最后对数组从右向左进行遍历,大于10进行进位。Java实现如下:
public class BigNumMul {
//simple method 模拟乘法累加
public static String bigNumberMul(String num1, String num2) {
// 分配一个空间,用来存储运算的结果,num1长的数 * num2长的数,结果不会超过num1+num2长
int[] res = new int[num1.length() + num2.length()];
// 先不考虑进位问题,根据竖式的乘法运算,num1的第i位与num2的第j位相乘,结果应该存放在结果的第i+j位上
for (int i=0; i<num1.length(); i++) {
int a = num1.charAt(i) - '0';
for (int j=0; j<num2.length(); j++) {
int b = num2.charAt(j) - '0';
res[i+j] += a * b; //max: num1.length()+num2.length()-2
}
}
StringBuilder sb = new StringBuilder();
//单独处理进位
int carry = 0;
//最多就到res.length-2, 最后一个元素没有被占用,还是初始值0
for (int k=res.length-2; k >= 0; k--) {
int digit = (res[k] + carry) % 10;
carry = (res[k] + carry) / 10;
sb.insert(0, digit);
}
if (carry > 0) {
sb.insert(0, carry);
}
String str = sb.toString().replaceFirst("^0*", "");
return str.substring(0,str.length()-1);
}
}
显然使用O(n^2)的算法是不够好的,我们应该想一下有没有更好的算法,就像这门课上经常说的一句:Can we do better ?
分治:Karatsuba算法
分治算法的主要思想是能将问题分解为输入规模更小的子问题,然后递归求解子问题,最后将子问题的结果合并得到原问题的结果,最典型的如归并排序算法。为了得到规模更小的子问题,就要将较大的整数拆分为位数较少的两个整数,参考coursera上的算法专项课,主要计算过程如下:
如上图所示,将每个数分别拆分为两部分,分别计算ac, bd, 以及(a+b)(c+d),最后再减去前面两个,将其组合成最终的结果。我们采用更一般的方式将其表达出来,相应计算方法如下:
上述给出了更通用的写法,将x和y同分解后的更小的整数进行表示,最后通过递归的计算ac, ad, bc, bd就可以得到x*y的结果。上述是没有优化过的分治算法,每次递归需要4次乘法,合并结果需要O(n)时间复杂度,所以可以得到时间复杂度的表示:
{T(n) = 4T(n/2) + O(n)}
通过主方法,可以求得上述时间复杂度为O(n^2),并没有得到好的改善。
Karatsuba算法将上述的4次乘法优化为3次从而减少了时间复杂度。具体过程如下:
可以看到上述利用(a+b)(c+d)的结果减去ac和bd得到ad+bc的结果,从而只需要计算三次乘法,其时间复杂度可以表示为:
T(n)=3T(n/2)+6n=O(n^{log_{2}3})
根据上述算法,使用Java进行实现代码如下:
//Karatsuba乘法
//此种情况使用long时,数过大可能出现越界,应考虑使用BigInteger
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 z1 = karatsuba(a, c);
long z2 = karatsuba(b, d);
long z3 = karatsuba((a + b), (c + d)) - z1 - z2;
return (long)(z1 * Math.pow(10, 2*halfN) + z2 + z3 * Math.pow(10, halfN));
}
注意上述递归的终止条件以及如何表示a, b, c, d. 上述实现使用的是Java中的long类型,但是当整数变大时,使用long类型可能会发生溢出,这里可以使用String来模拟整数的加法及乘法,或者使用Java的BigInteger类型,其实BigInteger内部也是使用的String进行存储,我使用的是BigInteger类型,实现代码如下:
//使用BigInteger的karatsuba算法
//注意BigInteger的运算没有操作符重载
//参考: coursera算法专项1
public static BigInteger karatsuba(BigInteger num1, BigInteger num2) {
if (num1.compareTo(BigInteger.valueOf(10)) < 0 || num2.compareTo(BigInteger.valueOf(10)) < 0) {
return num1.multiply(num2);
}
int n = Math.max(num1.toString().length(), num2.toString().length());
int halfN = n / 2 + n % 2; //另一种划分方法
//返回num1 / halfN 和 num1 % halfN
BigInteger[] a_b = num1.divideAndRemainder(BigInteger.valueOf(10).pow(halfN));
BigInteger a = a_b[0];
BigInteger b = a_b[1];
BigInteger[] c_d = num2.divideAndRemainder(BigInteger.valueOf(10).pow(halfN));
BigInteger c = c_d[0];
BigInteger d = c_d[1];
BigInteger step1 = karatsuba(a, c);
BigInteger step2 = karatsuba(b, d);
BigInteger step3 = karatsuba(a.add(b), c.add(d));
BigInteger step4 = step3.subtract(step2).subtract(step1); //step3-step2-step1
BigInteger res = step1.multiply(BigInteger.valueOf(10).pow(2*halfN)).add(step2)
.add(step4.multiply(BigInteger.valueOf(10).pow(halfN)));
return res;
}
最后的测试代码如下:
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String a = sc.next();
String b = sc.next();
// 开始计算
//String str = BigNumMul.bigNumberMul(a, b);
//long res = BigNumMul.karatsuba(Long.valueOf(a), Long.valueOf(b));
//String str = Long.toString(res);
BigInteger res = BigNumMul.karatsuba(new BigInteger(a), new BigInteger(b));
String str = res.toString();
System.out.println(a + " * " + b + " = " + str);
}
总结与感想
(1)对一个问题要深入调研和分析,多尝试不同的解决方法。
(2)可以多分析一些诸如此类的经典问题,还是比较有意思的。
参考资料:
1.coursera算法专项
2.大数乘法问题及其高效算法
3.https://stackoverflow.com/questions/17531042/karatsuba-algorithm-without-biginteger-usage
4.https://chenyvehtung.github.io/2017/03/02/about-multiplication.html
大整数相乘问题总结以及Java实现的更多相关文章
- 【老鸟学算法】大整数乘法——算法思想及java实现
算法课有这么一节,专门介绍分治法的,上机实验课就是要代码实现大整数乘法.想当年比较混,没做出来,颇感遗憾,今天就把这债还了吧! 大整数乘法,就是乘法的两个乘数比较大,最后结果超过了整型甚至长整型的最大 ...
- 华为OJ机试题目:两个大整数相乘(纯C语言实现两个大整数相乘,两种方法实现大数相乘)
题目描述: 输出两个不超过100位的大整数的乘积. 输入: 输入两个大整数,如1234567 123 输出: 输出乘积,如:151851741 样例输入: 1234567 123 样例输出: 1518 ...
- 大整数相乘的C实现
//之前有个测试这个题没做完,现在把它做完,通过这个程序可以对乘法了解更深刻.分析:运用整数乘法,当然进制越高越好,考虑到乘法不要越界,故考虑进制底数N应该满 //足,N^2<2^32次方.所以 ...
- 算法笔记_034:大整数乘法(Java)
目录 1 问题描述 2 解决方案 2.1 蛮力法 1 问题描述 计算两个大整数相乘的结果. 2 解决方案 2.1 蛮力法 package com.liuzhen.chapter5; import ...
- Coefficient Computation (大整数、Java解决)
Coefficient Computation UVALive8265 题意:计算组合数C(n,k)的值并将值按给定的进制输出. 思路:Java大整数类硬上. PS:刚刚学完Java的大整数类,结果却 ...
- Java实现大整数乘法
1 问题描述 计算两个大整数相乘的结果. 2 解决方案 2.1 蛮力法 package com.liuzhen.chapter5; import java.math.BigInteger; publi ...
- 大整数乘法python3实现
因为python具有无限精度的int类型,所以用python实现大整数乘法是没意义的,可是思想是一样的.利用的规律是:第一个数的第i位和第二个数大第j位相乘,一定累加到结果的第i+j位上,这里是从0位 ...
- 剑指offer第12题打印从1到n位数以及大整数加法乘法
字符和数字加减就是字符的ASCII码和数字直接加减. 方法一: 1)在字符串操作中给一个整形数字加(字符0)就是把它转化为字符,当然给一个字符减去(字符0)就可以把它转化为数字了:如果确实是最后 ...
- 【九度OJ】题目1190:大整数排序 解题报告
[九度OJ]题目1190:大整数排序 解题报告 标签(空格分隔): 九度OJ 原题地址:http://ac.jobdu.com/problem.php?pid=1190 题目描述: 对N个长度最长可达 ...
随机推荐
- VS2017、VS2019没有Setup安装项目(Visual Studio Installer)_解决方案
前言: VS2010中有一个自带的安装部署项目,叫:Visual Studio Installer ,我们通常称为:setup项目,是一个用于自定义安装部署的项目方案.但是在VS2017,VS2019 ...
- Python动态绑定属性slots的使用
当我们定义了一个class,创建了一个class的实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性.废话不多说,我们看一个例子: class Person(object): pass ...
- CSS入门知识汇总
1.CSS认识 在谈论CSS的概念之前,我们先说一说web标准的目的——其在于创建一个统一的用于web表现层的技术标准,以便通过不同浏览器或终端设备向最终用户展示信息内容.一个网页的呈现是由三部分组成 ...
- Git:九、删除项目
1.删除远程仓库 1)打开有绿色客隆按钮的仓库代码页面,选择Settings 2)把页面拉到最下边 2.删除本地仓库 1)先删.git隐藏文件 2)强行删除仓库文件夹 显示所有文件,包括隐藏的:ls ...
- Hyper-v虚拟机联网配置
最近想做点练手的项目部署到虚拟机的服务器上,然后关于虚拟机联网问题着实把贫道坑了一把.下面做一下记录防止以后忘了.... 1.新建虚拟交换机 输入交换机名称和选择外部网络,可以看到外部网络的下拉框的选 ...
- Storm入门(十四)Trident API Overview
The core data model in Trident is the "Stream", processed as a series of batches. A stream ...
- elasticsearch常用命令备注
1.检查集群健康状态 curl 'localhost:9200/_cat/health?v' 2.检查节点健康状态 curl 'localhost:9200/_cat/nodes?v' 3.新增一条索 ...
- 跟我一起学opencv 第五课之图像的混合
*理论-线性混合操作 g(x) = (1-α)f0(x)+αf1(x) α的取值范围位0-1之间 f0(x)为图像1,f1(x)表示第二张图像 α是混合系数 g(x)是生成的图像,对每一个像素 ...
- python --- 插入排序算法
先上一张图,看看能不能从里面悟出些什么: 问题的解决思路: 就是当插入第i个的时候,前面的[i- 1]个已经排好了,这时候lst[i]就倒过来逐个和前面的关键字顺序进行比较,找到插入位置即将lst[i ...
- 关于 Docker 镜像的操作,看完这篇就够啦 !(下)
紧接着上篇<关于 Docker 镜像的操作,看完这篇就够啦 !(上)>,奉上下篇 !!! 镜像作为 Docker 三大核心概念中最重要的一个关键词,它有很多操作,是您想学习容器技术不得不掌 ...